/**
 * Mount an overlayfs: reads fall through to `lower` (real, zero copy); writes
 * copy-up into `upper` (the landable changeset). `work` is overlayfs scratch on
 * the same fs as `upper`. Returns true iff `mount` exited 0.
 */
export declare function mountOverlay(opts: {
    lower: string;
    upper: string;
    work: string;
    merged: string;
}): boolean;
/** True iff `path` is currently a mountpoint (host-side overlay still mounted). */
export declare function isMounted(path: string): boolean;
/** Unmount an overlay merged dir. Best-effort: lazy-umount (`-l`) if a normal
 *  umount fails (busy fd from a still-draining child). No-op if not a mount. */
export declare function unmountOverlay(merged: string): void;
export interface SandboxPlan {
    /** In-sandbox path the project is bound at — equals the original workingDir the
     *  CLI was given, so the CLI's existing path args resolve. Also the child's chdir. */
    projectMount: string;
    /** Host path of the merged project overlay (reads=real lower, writes=upper). */
    projectMerged: string;
    /** In-sandbox path the home overlay is bound at — equals the real home path so
     *  every CLI's hardcoded `~/.<cli>` resolves there. */
    home: string;
    /** Host path of the merged home overlay. */
    homeMerged: string;
    /** Daemon-mediated `botmux send` outbox — bound LAST so it wins over any mask. */
    outbox: string;
    /** Per-bot privacy masks: directories blanked with an empty tmpfs. */
    hideDirs: string[];
    /** Per-bot privacy masks: files blanked with a read-only empty placeholder. */
    hideFiles: {
        path: string;
        empty: string;
    }[];
    /** CLI auth/login paths kept REAL + writable (bound rw over the isolated home so
     *  the CLI's token refresh / login persists — unlike project edits which are
     *  isolated). Resolved + existence-filtered by prepareSandbox. */
    authReal?: string[];
    /** Keep network egress. File-only scope ⇒ default true (npm/pip/git work). */
    net?: boolean;
}
/**
 * Build the bwrap argv prefix. Final spawn becomes:
 *   bwrap <these args> -- <cliBin> <cliArgs...>
 *
 * Mount order matters (later mounts win): the whole real fs read-only first, then
 * the home + project merged overlays bind over it (so writes there are isolated),
 * then privacy masks blank specific paths, then the outbox binds LAST so it stays
 * writable even if a mask covers a parent dir.
 */
export declare function buildSandboxArgs(plan: SandboxPlan): string[];
/**
 * After `buildSandboxArgs` masks `/run` with a fresh tmpfs, any executable whose
 * resolved path lives UNDER `/run` (the common case: fnm/nvm/volta expose the
 * active toolchain's bin dir as a per-session symlink farm under
 * `/run/user/<uid>/fnm_multishells/<hash>/bin`, and `which codex` / the daemon's
 * own `process.execPath` for node land there) would VANISH inside the sandbox →
 * bwrap `execvp` fails → the CLI exits instantly → Botmux's crash-loop guard
 * trips after 4 retries. This re-exposes each such bin dir read-only at its real
 * path so the binary (and the node interpreter its `#!/usr/bin/env node` shebang
 * needs, which lives in the same fnm bin dir) survive the tmpfs.
 *
 * The caller feeds in EVERY path that will be exec'd inside the sandbox, not just
 * the direct bwrap target: the cliBin, the daemon's own node (process.execPath),
 * AND each adapter-declared SECOND-STAGE executable (CliAdapter.sandboxExtraExecPaths)
 * — e.g. the codex-app adapter's resolved `codex` (its resolvedBin is the daemon
 * node running the runner, which spawns the real codex later for the app-server,
 * so without this the codex path would still be masked). We deliberately do NOT
 * scan raw cliArgs: a path arg like `--cwd /run/user/<uid>/proj` would re-bind its
 * PARENT `/run/user/<uid>`, shadowing the project overlay mounted there and
 * exposing sibling files / IPC sockets — re-exposing must be limited to declared
 * executables.
 *
 * Pure: returns the `--ro-bind-try <dir> <dir>` args (deduped, `/run/`-subpaths
 * only — NEVER `/run` itself, which would clobber the tmpfs and the relay shim
 * mounted at /run/sbxbin). `-try` so a stale/racing path can't fail the spawn.
 * Returns [] for binaries already outside /run (system node, npm/pnpm globals) —
 * non-fnm users are unaffected.
 */
export declare function reexposeRunBinArgs(binPaths: (string | undefined)[]): string[];
/** Is file-sandbox enabled for this session? Spike gate = env; the real
 *  per-bot BotConfig.sandbox flag is decided by the caller. */
export declare function sandboxEnabled(): boolean;
export interface SandboxSpawn {
    /** Replace the CLI binary with this (always 'bwrap'). */
    bin: string;
    /** bwrap args + '--' + original (bin, ...args). */
    args: string[];
    /** Env overrides to merge into childEnv (HOME, PATH, BOTMUX_SEND_RELAY, proxies). */
    env: Record<string, string>;
    /** Outbox dir the daemon watcher must service. */
    outbox: string;
    /** Project overlay UPPER dir — THE LANDABLE CHANGESET (used by sandbox-land). */
    workDir: string;
    /** HOME overlay UPPER dir (/var/tmp/botmux-sbx/<sid>/home-upper). The sandboxed
     *  CLI's $HOME writes — INCLUDING its session jsonl under CLAUDE_CONFIG_DIR —
     *  land here (invisible at the real path). The worker redirects its bridge/idle
     *  watch into this via sandboxedClaudeDataDir() so it sees the CLI's turns. */
    homeUpper: string;
    /** Unmount the overlays + remove the per-session sandbox tree. */
    cleanup: () => void;
}
/** The path where a sandboxed session's CLI actually writes a $HOME-relative
 *  data dir (e.g. CLAUDE_CONFIG_DIR / `.claude-runtime`): the HOME overlay's
 *  ephemeral UPPER copy. The worker redirects its jsonl/bridge watch here so it
 *  sees the sandboxed CLI's writes (which are invisible at the real host path).
 *  Mirrors prepareSandbox's homeUpper layout — keep in sync. */
export declare function sandboxedClaudeDataDir(sessionId: string, realDataDir: string): string;
/**
 * Build the sandboxed spawn for a CLI session, or return null when sandboxing
 * is off / unsupported / a required overlay mount fails (fail-safe = the worker
 * treats null as a hard error and does NOT silently run unsandboxed).
 *
 * Layout under <dataDir>/sandboxes/<sessionId>/: outbox, shimbin, proj-upper
 * (the landable changeset), proj-work, proj-merged, home-merged. The HOME
 * overlay's upper/work live under /var/tmp/botmux-sbx/<sessionId>/ because
 * overlayfs forbids upper/work inside the lower (= the real home).
 */
export declare function prepareSandbox(opts: {
    /** Whether the sandbox is on for THIS session (per-bot BotConfig.sandbox OR
     *  the BOTMUX_SANDBOX env force). Decided by the caller — prepareSandbox does
     *  NOT re-read the env, so the dashboard per-bot toggle actually takes effect. */
    enabled: boolean;
    cliId: string;
    sessionId: string;
    sourceWorkingDir: string;
    dataDir: string;
    cliBin: string;
    cliArgs: string[];
    /** Per-bot privacy masks (opt-in, no defaults). Paths existing as dirs are
     *  blanked with a tmpfs; files with an empty read-only placeholder. */
    hidePaths?: string[];
    /** This CLI's auth/login paths (CliAdapter.authPaths) to keep real+writable so
     *  token refresh / login persists. `~` expanded; missing paths skipped. */
    authPaths?: readonly string[];
    /** Adapter-declared SECOND-STAGE executables (CliAdapter.sandboxExtraExecPaths)
     *  spawned inside the sandbox beyond cliBin — re-exposed if under /run. ONLY
     *  executable paths (never cwd/path args). undefined → none. */
    extraExecPaths?: readonly string[];
}): SandboxSpawn | null;
/**
 * Re-attach the daemon/worker side to an ALREADY-spawned sandbox session WITHOUT
 * touching the overlays. Used on daemon-restart reattach to a persistent
 * (tmux/herdr/zellij) pane whose bwrap'd CLI is still alive: the CLI is bound to
 * its own namespace-pinned overlay, so we must NOT unmount/remount (that would
 * leave a duplicate host-side mount the CLI isn't using). We only need the outbox
 * path back so the watcher can keep servicing the live CLI's `botmux send`, plus
 * the workDir (upper changeset for landing) and a cleanup that tears the residue
 * down at close/exit. Returns null if the session has no sandbox tree on disk
 * (never sandboxed). Linux-only, mirrors prepareSandbox's layout.
 */
export declare function attachSandboxOutbox(opts: {
    sessionId: string;
    dataDir: string;
}): {
    outbox: string;
    workDir: string;
    cleanup: () => void;
} | null;
/**
 * Reclaim leaked sandbox residue.
 *
 * Two classes of leak are reclaimed:
 *  1. NON-ACTIVE orphans — sid not in `activeSessionIds`: the session is gone, so
 *     any leftover mount/dir is pure residue (the original startup-sweep case,
 *     guarding against a daemon crash/kill that skipped killCli()).
 *  2. ACTIVE-but-DEAD — sid IS in `activeSessionIds`, yet NEITHER of its merged
 *     overlays is still mounted. This closes the blind spot where a sandboxed
 *     worker was SIGKILL'd (straggler reaper) or crashed: the session stays
 *     status='active' on disk, so the old "skip if active" rule would let the
 *     leaked upper/work dirs survive across restarts indefinitely. We only GC an
 *     active sid when its mounts are ALREADY gone — we NEVER tear down a live
 *     mount (a CLI persisting in a tmux/herdr/zellij pane is still bound to it),
 *     so a genuinely-live persistent session keeps its changeset.
 *
 * Safe to call repeatedly: wire once at daemon bootstrap AND on a periodic timer
 * (the SIGKILL/straggler path can't run worker-side killCli(), so a startup-only
 * sweep would let a crashed-active session's mount survive for the whole next
 * daemon lifetime — one daemon per bot can run for days).
 */
export declare function sweepOrphanSandboxes(dataDir: string, activeSessionIds: Set<string>): void;
export interface RelayRequest {
    contentFile?: unknown;
    attachments?: unknown;
    flags?: unknown;
}
export interface ValidatedRelay {
    contentName: string;
    attachmentNames: string[];
    flags: string[];
}
/**
 * PURE validation of an outbox relay request (schema + flag allowlist only — no
 * filesystem access, so it's deterministically testable):
 *  - contentFile/attachments must be plain basenames (no `/`, `\`, `..`).
 *  - only allowlisted presentation flags pass; any other flag → reject (this
 *    rejects raw `--content-file`/`--session-id`/path flags etc.).
 * The TOCTOU-safe filesystem read is handled separately by materializeOutboxFile,
 * NOT here — this function deliberately resolves no paths.
 */
export declare function validateRelayRequest(req: RelayRequest): {
    ok: true;
    value: ValidatedRelay;
} | {
    ok: false;
    error: string;
};
/**
 * TOCTOU-safe copy of an outbox file (`outbox/<name>`, name already validated as
 * a plain basename) into a host-private `dest`. Opens with O_NOFOLLOW so a
 * symlink swapped in by the sandbox AFTER validation is rejected at open time,
 * and O_NONBLOCK so opening a FIFO/special the sandbox dropped into the (rw-bound)
 * outbox returns IMMEDIATELY instead of blocking the synchronous watcher tick
 * forever (a FIFO with no writer would otherwise hang the whole worker event
 * loop — an agent-triggerable DoS). After open we fstat-reject anything that
 * isn't a regular file (a FIFO opened O_NONBLOCK|O_RDONLY succeeds but isFile()
 * is false → rejected here). Reads from the fd (not the path), so the inode can't
 * be swapped under us. Returns false (reject) on symlink / non-regular / any error.
 */
export declare function materializeOutboxFile(outbox: string, name: string, dest: string): boolean;
/**
 * Daemon/worker-side outbox watcher. The sandboxed `botmux send` (relay mode)
 * drops `<id>.req.json`; we validate (validateRelayRequest) and then MATERIALIZE
 * the content/attachments into a host-private staging dir that is NOT bound into
 * the sandbox — closing the TOCTOU window where the sandbox could swap an outbox
 * file for a symlink between check and the host-side read. We then re-exec THIS
 * build's `send` OUTSIDE the sandbox (full creds) against the private copies,
 * with the session-id FORCED. This keeps every Lark credential out of the sandbox.
 */
export declare function startOutboxWatcher(outbox: string, baseEnv: NodeJS.ProcessEnv, sessionId: string): () => void;
//# sourceMappingURL=sandbox.d.ts.map