/** * Unified file-sandbox policy (FsPolicy) — the single source of truth for BOTH * platform sandbox engines. * * Model (three-tier whitelist, per product decision 2026-07-16, design doc * "botmux 文件沙盒重构方案"): every path gets one of three access levels — * readWrite / readOnly / deny — and EVERYTHING NOT COVERED BY A RULE IS * INACCESSIBLE (deny-by-default). Nested black/white lists are supported: the * DEEPEST matching rule wins (longest-prefix), so `readOnly ~/Library` + * `deny ~/Library/Keychains` and `deny bots/` + `readWrite bots/` both * work. This replaces the previous "read-everything + enumerated blocklist" * model whose failure mode was silent secret exposure; here a missing baseline * entry fails loud (CLI error), never silent. * * Architecture: this module is PURE (no fs / no spawn — fully unit-testable). * buildFsPolicy(ctx) merges baseline preset + botmux-internal + adapter * + user rules into one ordered rule list * compileToSeatbelt(...) → macOS sandbox-exec profile text * compileToBwrap(...) → Linux bwrap argv prefix * Both engines resolve conflicts by "last emitted wins" (Seatbelt last-match / * bwrap mount order), so emitting rules sorted shallow→deep yields * longest-prefix-wins on BOTH platforms by construction — cross-platform * parity needs no hand-synced rule lists. * * The worker resolves every impure input up front (realpath, existence * filtering, sibling-free by design) and passes canonical absolute paths. */ export type FsAccess = 'readWrite' | 'readOnly' | 'deny'; export type FsRuleSource = 'baseline' | 'adapter' | 'internal' | 'user' | 'mandatory'; export interface FsRule { /** Canonical absolute path (no trailing slash). */ path: string; access: FsAccess; /** Where the rule came from — for the dashboard policy viewer / path tester. */ source: FsRuleSource; } export interface FsPolicy { /** Deduped rules sorted shallow→deep (emission order = precedence order). */ rules: FsRule[]; /** Keep network egress (bwrap-only knob; Seatbelt does not confine net here). */ net: boolean; /** Extra Seatbelt write-allow regexes (e.g. ~/.claude.json.tmp.* atomic-save * siblings when the CLI data dir is NOT redirected into BOT_HOME). */ writeRegexes: string[]; /** Final host-security denies that cannot be represented as path prefixes. */ denyRegexes?: string[]; /** Narrow read-only exceptions emitted after denyRegexes (macOS gateway socket). */ finalReadOnlyPaths?: string[]; /** No-transport turns: caller-supplied allow paths (extraWrite / readonlyRoots / * user RW+RO) that fell inside a Feishu-authority root and were dropped before * merge (fail-closed). Empty/absent otherwise. The worker LOGS these so a * silently-suppressed grant is diagnosable (codex: "至少要记录被抑制项"). */ suppressedAuthorityPaths?: string[]; } export interface FsPolicyUserPaths { readWrite?: readonly string[]; readOnly?: readonly string[]; deny?: readonly string[]; } export interface FsPolicyContext { platform: 'darwin' | 'linux'; /** All paths below must be CANONICAL (realpath'd by the worker). */ homeDir: string; botmuxHome: string; sessionDataDir: string; workingDir: string; currentAppId: string; /** This session's id — scopes the turn-sends dedup marker to a single file * (`turn-sends/.jsonl`) so a sandboxed CLI cannot rewrite ANOTHER * session's markers. Optional: when absent, no turn-sends grant is emitted * (a non-session policy has no marker to write). */ sessionId?: string; /** This bot's BOT_HOME (`/bots/`) — always readWrite. */ botHome: string; /** This bot's OWN role-library subtree (`/`) — readWrite. * Optional: absent when the subtree does not exist (bot never used roles) or * when building a non-session policy. See the emission site for why the whole * subtree — not just the active role dir — has to be writable. */ roleLibrarySubtree?: string; /** True when the CLI's data root is redirected into BOT_HOME * (CLAUDE_CONFIG_DIR / CODEX_HOME). False → cliDataPaths are exposed rw. */ redirectedCliData: boolean; /** The CLI's REAL data paths (e.g. ~/.claude, ~/.claude.json, ~/.codex) to * keep readWrite when NOT redirected. Ignored when redirectedCliData. */ cliDataPaths?: readonly string[]; /** Adapter auth/login paths kept readWrite (token refresh must persist). */ authPaths?: readonly string[]; /** Directories of every executable spawned inside the sandbox (cliBin dir, * node dir, adapter second-stage bins) — exposed readOnly. */ execPaths?: readonly string[]; /** Trusted runtime read-only roots (skill/plugin dirs, botmux dist). */ readonlyRoots?: readonly string[]; /** The botmux install/checkout root (dir containing dist/ + node_modules). * Exposed readOnly so the agent's `botmux` CLI and the claude hooks (which * exec `node /dist/cli.js …`) can load — without this a sandboxed * `botmux send` / SessionStart+AskUserQuestion hooks EPERM on cli.js. */ botmuxInstallRoot?: string; /** Daemon-mediated relay outbox (Linux) — readWrite. */ outbox?: string; /** Extra writable roots (resolved TMPDIR, admin extras). */ extraWritePaths?: readonly string[]; /** Per-bot user config (bots.json sandboxPaths) — highest precedence. */ userPaths?: FsPolicyUserPaths; /** Host-owned boundaries that user policy may not override. */ mandatoryDenyPaths?: readonly string[]; mandatoryDenyRegexes?: readonly string[]; mandatoryReadOnlyPaths?: readonly string[]; net?: boolean; /** Seatbelt write-allow regex passthrough (see FsPolicy.writeRegexes). */ writeRegexes?: readonly string[]; /** FALSE = a no-Lark-transport session (core-only apiOnly bot or HTTP virtual * chat). Generic read-isolation is NOT a credential boundary: it still grants * the bot's own lark-cli identity readWrite AND, when workingDir defaults to * `~`, re-opens $HOME (incl. bots.json + sibling BOT_HOMEs) readWrite. When * false, buildFsPolicy freezes the botmux authority ROOTS (configured + default * `~/.botmux`, `~/.lark-cli(-bots)`, macOS lark-cli store) as WHOLE-DIR denies, * withholds this bot's own lark-cli identity / keystore carve-out, and * FAIL-CLOSES any caller allow path (workingDir / userPaths / readonlyRoots) * that would fall inside an authority root — dropped BEFORE merge so a deeper * grant can't re-open the deny (deepest-prefix-wins). The model CLI's own * authPaths (`~/.codex` etc.) are NOT Feishu creds and stay granted readWrite. * Absent/true = normal behavior. */ larkTransportEnabled?: boolean; /** No-transport ONLY: the SECOND botmux authority root to freeze, when the * daemon runs with a custom SESSION_DATA_DIR so `configuredBotmuxHome` * (= dirname(dataDir), passed as `botmuxHome`) differs from the default * `~/.botmux`. BOTH must be denied wholesale — the default root still holds * the live `.dashboard-secret` HMAC + bots.json even when the data dir moved * (codex P1: custom SESSION_DATA_DIR leaked the default `~/.botmux`). Ignored * when equal to `botmuxHome`. MUST be canonical + host-frozen (never derived * from agent-controllable env). */ defaultBotmuxHome?: string; /** No-transport ONLY: the ACTUAL loaded bots-config path, frozen by the daemon * (`getLoadedConfigPath()`), NOT guessed from BOTS_CONFIG env by the worker. * When it lives INSIDE a frozen authority root the parent mask already covers * it (no extra rule needed). When it lives OUTSIDE every authority root, * buildFsPolicy THROWS {@link FsPolicyConfigError} — a no-transport turn must * not silently mask an arbitrary parent dir (`/tmp`, `/etc`, a project root), * which would break the core CLI (codex P1). Absent = default `~/.botmux/bots.json`. */ loadedBotsConfigPath?: string; /** LINUX ONLY: the CANONICAL lark-cli data store dir that holds EVERY bot's * `appsecret_.enc` + the shared `master.key` (the Linux analogue of the * macOS `~/Library/Application Support/lark-cli` keystore). lark-cli resolves it * to `${LARKSUITE_CLI_DATA_DIR}/lark-cli` (when that env var is ABSOLUTE) else * `$HOME/.local/share/lark-cli` — it does NOT consult XDG_DATA_HOME (verified by * strace on v1.0.76). So it is NOT purely a function of homeDir. The worker * resolves it from the FROZEN host env (`resolveLarkCliLinuxStoreDir`) and * canonicalizes it (this fn stays pure — it must never read env). Under the * baseline `ro(~/.local/share)` grant the whole store is otherwise exposed * read-only → a sandboxed bot could read its SIBLINGS' ciphertext + the shared * master key and impersonate them (the pre-refactor cross-bot leak macOS already * closes). buildFsPolicy DENIES the store and, for a transport-enabled turn, * re-opens ONLY this bot's own `master.key` + `appsecret_.enc` * read-only at a deeper path (longest-prefix-wins), mirroring the darwin carve-out. * A no-transport turn freezes it as an authority root with NO carve-out. Absent on * Linux → falls back to the default `${homeDir}/.local/share/lark-cli` (correct * whenever LARKSUITE_CLI_DATA_DIR is unset/relative — the common case — and still * protective otherwise). Ignored on darwin. */ larkCliLinuxStore?: string; } /** Normalize: require absolute, strip trailing slashes, reject `..` segments. * Returns null for anything unusable (silently-dropped relative paths are a * fail-open trap — callers log dropped entries). */ export declare function normalizeFsPath(p: string): string | null; /** Is `p` equal to or an ancestor of `child`? (both normalized) */ export declare function coversPath(p: string, child: string): boolean; /** * Which adapter `authPaths` survive a CLI-data redirect into BOT_HOME. * * When a bot redirects its CLI data (CLAUDE_CONFIG_DIR/CODEX_HOME → BOT_HOME), * the adapter's REAL host data dir is rehomed: the isolated copy under BOT_HOME * is what the CLI reads/writes, and the host dir is denied-by-default (its * exposure was the whole point of read isolation). Any authPath that lives * INSIDE such a rehomed host root is therefore either (a) redundant — its * BOT_HOME equivalent is provisioned + covered readWrite by the botHome rule * (claude `.credentials.json`), or (b) an active leak we must NOT expose (codex's * whole `~/.codex`: history.jsonl, sessions/, state_*.sqlite). Both are dropped. * * But authPaths that live OUTSIDE every rehomed root are external login sources * the redirect does NOT rehome — e.g. Seed/Relay's `~/.local/share/bytedcli` SSO * dir (bytedcli login reuse). Dropping those regresses login (a fresh BOT_HOME / * expired token cold-start would have no credential source). They MUST survive. * * NOTE this is a SUPPRESSION-vs-KEEP decision only. A path inside a rehomed root * whose BOT_HOME copy is NOT provisioned (e.g. `/byted-cloud-auth.json` * — never seeded, and never the redirected read location since the CLI resolves * it under $CLAUDE_CONFIG_DIR=BOT_HOME) is still dropped: keeping the host path * would not help the redirected read anyway. Provisioning such files into * BOT_HOME is a separate concern (see provisionIsolatedBotHome), orthogonal to * closing the host-dir leak this filter exists for. * * @param authPaths adapter authPaths, already `~`-expanded + normalized absolute * @param rehomedRoots host data roots being redirected to BOT_HOME, normalized * (claude family: the host claudeDataDir; codex: `~/.codex`) */ export declare function authPathsSurvivingCliDataRedirect(authPaths: readonly string[], rehomedRoots: readonly string[]): string[]; /** * The COMPLETE authPaths the fs-policy should expose for a (possibly redirecting) * adapter. This is the single source of truth the worker calls — extracted so the * worker's call site and the tests exercise the SAME code, not a re-implementation * (a test that recomputes the rule would stay green if the worker stopped calling * this, which is exactly the wiring blind spot this avoids). * * - not redirected → expose the adapter's declared authPaths verbatim. * - redirected → drop the ones inside a rehomed host data root * (authPathsSurvivingCliDataRedirect), keeping data-root-external * login sources (Seed/Relay bytedcli SSO). * * rehomedHostRoots is assembled from the adapter's ORIGINAL host data dir * (claude family) + the codex host root when applicable — passed in by the worker * (it owns `~`-expansion / existence-filter / canonicalization); this fn is pure. */ export declare function resolveRedirectedAdapterAuthPaths(input: { declaredAuthPaths: readonly string[]; willRedirectCliData: boolean; rehomedHostRoots: readonly string[]; }): string[]; /** * Merge candidate rules into the final ordered list: * - normalize paths, drop unusable entries * - dedupe same-path conflicts: higher source rank wins (user > internal > * adapter > baseline); tie → the MORE RESTRICTIVE access wins (deny > * readOnly > readWrite) so a duplicated entry can never widen access * - sort shallow→deep (stable within a depth) — the emission order both * compilers rely on for longest-prefix-wins */ export declare function mergeFsRules(candidates: readonly FsRule[]): FsRule[]; /** * The effective access for `path` under `rules`: the DEEPEST rule whose path * covers it; no match → 'none' (inaccessible). This one function IS the policy * semantics — the dashboard path tester and the unit tests both call it, and * both compilers are tested to agree with it. */ export declare function accessForPath(rules: readonly FsRule[], path: string): { access: FsAccess | 'none'; rule?: FsRule; }; /** * A no-transport (apiOnly / HTTP-virtual) session whose layout cannot be safely * confined — e.g. an external `BOTS_CONFIG` sitting outside every botmux * authority root, or a `workingDir` that IS a Feishu-authority root. buildFsPolicy * throws this instead of silently masking an arbitrary parent dir (which would * hide `/tmp`/`/etc`/a project root and brick the core CLI) or silently dropping * the working dir. The worker turns it into a hard spawn-abort with a diagnostic — * fail-closed, never fail-open (codex P1). Carries `.kind` so callers/tests can * branch without string-matching the message. */ export declare class FsPolicyConfigError extends Error { readonly kind: 'external-bots-config' | 'working-dir-is-authority' | 'bots-config-in-carveout'; constructor(kind: FsPolicyConfigError['kind'], message: string); } /** * The Linux lark-cli keystore dir. lark-cli stores keys at * `${LARKSUITE_CLI_DATA_DIR}/lark-cli` (absolute) else `$HOME/.local/share/lark-cli`. * The worker resolves the real (env-aware, cleaned) path via * resolveLarkCliLinuxStoreDir + a nearest-existing-ancestor canonicalize, passing * it as `override`; when absent this falls back to the default under homeDir. PURE * — never reads env (that would break single-testability). Exported so the worker's * real assembly and the unit matrix share ONE resolver. * * `override` is expected to already be lexically clean (resolveLarkCliLinuxStoreDir * Cleans it); a defensive normalizeFsPath still rejects a `..`-bearing value — but * that must NEVER silently fall back to the default store (the pitfall this guards: the * default gets denied while the real `..`-cleaned store lark-cli reads stays exposed). * So a non-null-but-unnormalizable override returns null → the caller (buildFsPolicy) * emits NO Linux carve-out, and the store stays denied-by-default rather than * mis-anchored. In practice the worker always Cleans first, so this path is defensive. */ export declare function larkCliLinuxStorePath(homeDir: string, override?: string): string | null; /** * The CANONICAL lark-cli data root to pin into the sandbox child as * LARKSUITE_CLI_DATA_DIR, given the fully-canonical keystore dir the policy protects * (`canonStore` = worker's larkCliLinuxStore, leaf symlinks already followed). The * in-sandbox lark-cli opens `/lark-cli`, so the pin must be `dirname(canonStore)` * — but that is same-source with the policy-protected store ONLY when the canonical * store's basename is still `lark-cli` (the common case, incl. a symlink that resolves * to another dir also named `lark-cli`). If the `lark-cli` leaf was a symlink to a * DIFFERENTLY-named dir (basename !== 'lark-cli'), `/lark-cli` would NOT equal * canonStore, so pinning it would point the child at an unbound/foreign path. There is * no leak either way (the policy denies the real store, and both candidate stores are * locked), so return null → the worker pins nothing and the child falls back to the * default store (also locked). Pure + total. (handles the symlinked keystore leaf case.) */ export declare function larkCliChildDataRoot(canonStore: string | null | undefined): string | null; /** * Lexically clean an ABSOLUTE POSIX path the way Go's `filepath.Clean` does * (resolve `.`/`..` segments, collapse `//`, drop trailing `/`, clamp `..` at root) * — WITHOUT touching the filesystem. Returns null for a non-absolute input or one * containing NUL / control chars (lark-cli's `SafeEnvDirPath` rejects those). This * mirrors lark-cli's own env-path handling so the policy anchors the SAME dir the * in-sandbox CLI will open (a `..`-bearing `LARKSUITE_CLI_DATA_DIR` is * Clean'd by lark-cli to a real store, but an un-cleaned policy path was rejected by * normalizeFsPath and silently fell back to the default — leaving the real store * exposed). Pure + total. */ export declare function cleanPosixAbsPath(p: string): string | null; /** * Resolve the lark-cli Linux store dir BEFORE canonicalization, replicating * lark-cli's OWN resolution (`internal/keychain/keychain_other.go::StorageDir` + * `SafeEnvDirPath`, verified by strace against v1.0.76): the keystore dir is * `/lark-cli` when `LARKSUITE_CLI_DATA_DIR` is a VALID * ABSOLUTE path (lexically Cleaned — `..`/`.`/`//` resolved, control chars rejected), * else `$HOME/.local/share/lark-cli`. lark-cli does NOT consult `XDG_DATA_HOME` for * the keystore at all (strace: setting it has zero effect), and it IGNORES a relative * / tilde / empty / control-char `LARKSUITE_CLI_DATA_DIR` (spec-invalid → falls back * to $HOME). PURE (exported so the "absolute-cleaned / relative-ignored / unset / * `..`-cleaned / control-char-rejected" matrix is unit-locked against strace). * * CRITICAL: the `..` Clean must happen HERE, lexically, so the returned * path is already normalized (no `..`) — otherwise the worker's realpath throws on a * not-yet-existent store leaf, keeps the raw `..` path, normalizeFsPath rejects it, * and the policy silently anchors the DEFAULT store while lark-cli (which Cleans) * opens the real one → the real store stays exposed. * * The worker passes `process.env.LARKSUITE_CLI_DATA_DIR` + the (lexical) home, then * nearest-existing-ancestor `canonical()`s the result (a full-path realpath would * throw on the nonexistent leaf), and hands it to buildFsPolicy as `larkCliLinuxStore`. * This is the AUTHORITATIVE value the in-sandbox lark-cli reads: bwrap has NO * `--clearenv`, so the sandboxed child INHERITS `LARKSUITE_CLI_DATA_DIR` from the * worker's process env (redactChildEnv doesn't strip it), and the sandbox pins the * child's value to the SAME cleaned resolution (`--setenv`/`--unsetenv` in sandbox.ts) * so policy == CLI by construction. */ export declare function resolveLarkCliLinuxStoreDir(rawDataDir: string | undefined, homeDir: string): string; /** * Compute the frozen Feishu-authority roots for a no-transport turn. PURE + * exported so the worker's real path assembly and the unit matrix lock the SAME * provenance logic (codex P2: prior tests hand-fed roots and never touched this). * * - ALWAYS freezes BOTH the configured botmux root (`botmuxHome` = dirname of the * data dir) AND the canonical default `~/.botmux` — a custom SESSION_DATA_DIR * moves the data dir but the default root still holds the live `.dashboard-secret` * HMAC + bots.json, so denying only one leaves the sibling-daemon escalation * open (codex P1). * - freezes the lark-cli identity/key stores: bare `~/.lark-cli` (repo marks it * sensitive), `~/.lark-cli-bots`, macOS lark-cli store, AND the Linux lark-cli * keystore (`$HOME/.local/share/lark-cli` or `/lark-cli` — every bot's * appsecret ciphertext + the shared master key; on Linux a no-transport turn * must get NO carve-out into it, unlike the transport-enabled own-key carve-out). * - the loaded bots-config path: OUTSIDE every root → THROW here (fail-closed); * never mask its parent dir. Being INSIDE a root is necessary but NOT * sufficient — a deeper trusted carve-out (own BOT_HOME / bin / attachments / * outbox) can re-open it via deepest-prefix-wins, so buildFsPolicy ALSO runs a * post-merge `accessForPath` self-check that the config + its dirname resolve * to `deny`, else throws `bots-config-in-carveout` (codex P1). */ export declare function computeNoTransportAuthorityRoots(input: { platform: 'darwin' | 'linux'; homeDir: string; botmuxHome: string; defaultBotmuxHome?: string; loadedBotsConfigPath?: string; larkCliLinuxStore?: string; }): string[]; /** * Build the unified FsPolicy: baseline preset (platform) + adapter-declared * paths + botmux-internal injections + user sandboxPaths (highest precedence). * Pure — the worker canonicalizes and existence-filters all ctx paths first. */ export declare function buildFsPolicy(ctx: FsPolicyContext): FsPolicy; /** Every strict ancestor of every non-deny rule path. Under read-deny-default, * realpath()/stat of an intermediate dir fails and crashes CLIs that * canonicalize their config dir — so each ancestor needs file-read-metadata * (literal, NOT subpath: no listing/enumeration is granted). */ export declare function ancestorsNeedingTraverse(rules: readonly FsRule[]): string[]; /** * Compile the policy to a macOS Seatbelt profile. `(deny default)` + Apple's * bsd.sb base makes it deny-by-default for BOTH files and other operations, * then we re-allow the non-file operation classes the CLI needs (process/mach/ * ipc/sysctl/signal/iokit-open, network gated on policy.net) and the three file tiers * emitted shallow→deep (Seatbelt applies the LAST matching rule → deepest rule * wins, matching accessForPath()). */ export declare function compileToSeatbelt(policy: FsPolicy): string; export interface CompileBwrapOpts { /** Top-level symlinks to replicate inside the tmpfs root (usrmerge /bin → * usr/bin etc.). Resolved by the worker (impure readlink). */ symlinks?: readonly { path: string; target: string; }[]; /** A single MODE-000 EMPTY directory (worker-created, kept empty) ro-bound * over DIRECTORY-shaped deny rules. `--ro-bind emptyDir ` masks the * real contents, makes the mountpoint read-only (unlike `--tmpfs `, * which left a WRITABLE tmpfs), AND — because the source is mode 000 — a * non-root process can't even list it (`ls`/`cat` fail, not just "returns * empty"). Root bypasses DAC and can traverse it, but the source is EMPTY, so * no real content leaks regardless of uid — emptiness is the guarantee, the * 000 mode only hardens listing for non-root. A real deny is neither readable * (content), listable (non-root), nor writable. */ emptyDir: string; /** Directory that will hold MODE-000 empty placeholder files for FILE-shaped * deny rules (dirs are masked with the empty ro-bind above; files need an * empty ro-bind source). The worker creates the returned `emptyFiles` * (mode 000) first. */ emptiesDir: string; /** Rule paths that are FILES (not dirs) on the host — deny compiles to an * empty-file bind, readOnly/readWrite file binds work as-is. Resolved by * the worker (impure stat). A deny path NOT in this set is masked as a * DIRECTORY (covers both existing dirs and not-yet-existing paths). */ filePaths?: ReadonlySet; /** chdir target (the project working dir). */ chdir: string; /** Drop `--unshare-pid` (keep the fresh `--proc /proc` mount, which works * without a new pid namespace). Set ONLY in a nested sandbox that can't * mount proc inside a new pid ns AND where no sibling secret is exposed via * /proc — see sandbox.coreOnlyPidNamespaceDegrade. The filesystem deny/allow * masks (the on-disk credential seal) are unaffected; this drops only the * process-isolation defense-in-depth. Default false = full isolation. */ skipPidNamespace?: boolean; } export interface BwrapCompilation { /** bwrap argv prefix (caller appends --setenv pairs and `-- cli args`). */ args: string[]; /** Mode-000 empty placeholder files the worker must create before spawn * (the ro-bind SOURCE for file-shaped deny masks). */ emptyFiles: { path: string; maskedPath: string; }[]; /** Every deny mountpoint that was masked, with its shape. The worker must * ensure each exists on the host BEFORE spawn (bwrap cannot bind onto a * missing target, and a missing target under a read-write parent would make * bwrap materialise it on the host anyway) — creating a mountpoint the host * can write to even when the policy makes its PARENT read-only in-sandbox, * which is exactly what closes the "host/other process creates the secret * mid-session" TOCTOU. Mountpoints the worker itself had to create (didn't * pre-exist) are removed at teardown IFF still empty (never a recursive rm — * content written by the host/a concurrent process is preserved). */ maskMounts: { path: string; kind: 'dir' | 'file'; }[]; } /** * Compile the policy to a bwrap argv prefix. Deny-by-default is bwrap's * natural shape: a fresh tmpfs root, then ONLY the rule paths are bound in * (later mounts win → emitting shallow→deep gives deepest-rule-wins, matching * accessForPath()). deny rules materialize as READ-ONLY empty masks with the * real content HIDDEN (mode-000 empty-dir ro-bind for dirs, mode-000 empty-file * ro-bind for files; mode 000 additionally blocks listing for a non-root uid, * while root may list the empty mask — no real content leaks either way) and are * emitted whenever they sit under an exposed tree — outside it * they're unreachable already (deny-by-default), and bwrap would fail mounting * onto a void path. * * Two properties this must preserve (both cost real blockers in review): * 1. The mask is `--ro-bind` of an EMPTY source, NOT `--tmpfs`: a `--tmpfs * ` mount is WRITABLE inside the sandbox, so it hid the real contents * but still let the CLI write into the denied path — not a real deny. * 2. A deny path is masked EVEN WHEN IT DOES NOT EXIST YET. Skipping absent * denies left the path inside the read-write parent bind, so the sandbox * could `mkdir`+write it straight onto the host, and anything the host * created there mid-session became readable (a stat→exec TOCTOU). The * worker pre-creates the mountpoint on the host so the mask always binds. */ export declare function compileToBwrap(policy: FsPolicy, opts: CompileBwrapOpts): BwrapCompilation; export interface LegacySandboxFields { sandbox?: boolean; readIsolation?: boolean; sandboxReadonlyPaths?: readonly string[]; sandboxHidePaths?: readonly string[]; readDenyExtraPaths?: readonly string[]; } export interface MigratedSandboxFields { sandbox: boolean; sandboxPaths?: { readWrite?: string[]; readOnly?: string[]; deny?: string[]; }; } /** * old→new field mapping (lossless: the old fields' expressiveness is a subset * of the three-tier model). Used by the registry's load-time auto-migration, * which writes the NEW fields while KEEPING the old ones in bots.json — a * downgraded daemon reads the untouched old fields, so downgrade needs no * reverse script (design doc §6.2). Returns null when nothing to migrate. */ export declare function migrateLegacySandboxFields(entry: LegacySandboxFields & { sandboxPaths?: unknown; }): MigratedSandboxFields | null; //# sourceMappingURL=fs-policy.d.ts.map