From bbead98c1d28787db04245c56890d664bed08319 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:42:22 +0800 Subject: [PATCH] docs: align sandbox extraction with prose standard --- docs/config-catalog.md | 33 ++---- packages/bash/bash-sandbox/src/index.ts | 89 +++----------- packages/sandbox/sandbox-local/README.md | 6 +- packages/sandbox/sandbox-local/src/index.ts | 122 +++++--------------- 4 files changed, 56 insertions(+), 194 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f23f2eef0e..06b1396ebf 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -167,7 +167,7 @@ export interface Config extends LocalConfig { Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md) -Source: [`packages/bash/bash-sandbox/src/index.ts:61`](../packages/bash/bash-sandbox/src/index.ts) +Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts) ## `@deepseek-ai/dsh-code-runtime-worker` @@ -489,20 +489,10 @@ Source: [`packages/guard/repeat-tool-guard/src/index.ts:27`](../packages/guard/r /** Plugin config. All optional — `static Config` supplies the defaults. */ export interface Config { /** - * Override the sandbox runner argv (the bwrap-shaped profile arguments are - * appended). A NON-EMPTY argv is the operator's assertion that this runner - * exists and FULLY enforces the profile (confinement reports - * `enforcement: 'full'`, and — the runner's kernel mechanism being unknown - * — carries both Linux file-denial dialects as its denial signatures) — - * the runner chain and its probes are skipped, - * and a broken runner fails loudly at execution time. The operator also - * supplies {@link runnerFailureSignatures}, which distinguish the runner - * refusing its profile from the wrapped command failing normally. - * Absent (or empty — the schema normalizes an omitted array to `[]`): the - * built-in platform chains — Linux `bwrap` then the Landlock launcher - * (probed in that order), darwin `sandbox-exec` (the sole candidate, - * selected without a probe). Used for custom/alternative runners and - * for deterministic fake runners in keyless test tiers. + * Override the runner argv; bwrap-shaped profile arguments are appended. A + * non-empty override asserts full enforcement and skips built-in selection and + * probing; a broken runner then fails at execution and must be identifiable by + * {@link runnerFailureSignatures}. */ runnerCommand?: string[] /** @@ -514,21 +504,12 @@ export interface Config { * own failure dialect. */ runnerFailureSignatures?: string[] - /** - * Per-probe timeout in milliseconds for the chain's functional probes - * (default: 5000; must be a positive finite number — Node treats a 0 - * `spawnSync` timeout as UNBOUNDED, so 0 is rejected at construction). A - * probe that exceeds it reads as an unusable rung, so a - * host slow enough to trip the default — cold NFS mounts, heavily loaded - * CI — would otherwise be misclassified `SANDBOX_UNAVAILABLE` with no - * config escape. Bounds ONE probe, and the chain walk runs each at most once - * per provider lifetime. - */ + /** Positive timeout for each functional probe; zero would mean unbounded to Node. */ probeTimeoutMs?: number } ``` -Source: [`packages/sandbox/sandbox-local/src/index.ts:35`](../packages/sandbox/sandbox-local/src/index.ts) +Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts) ## `@deepseek-ai/dsh-session-persistence-jsonl` diff --git a/packages/bash/bash-sandbox/src/index.ts b/packages/bash/bash-sandbox/src/index.ts index 64c28f2009..7239d0e260 100644 --- a/packages/bash/bash-sandbox/src/index.ts +++ b/packages/bash/bash-sandbox/src/index.ts @@ -1,43 +1,9 @@ /** - * `SandboxBashExecutor`: the sandbox-consuming implementation of the - * `@deepseek-ai/dsh-bash` executor seam. Every spawned command is wrapped by - * the `ctx.sandbox` provider (`@deepseek-ai/dsh-sandbox`) according to the - * configured {@link SandboxMode}: the executor hands the provider the exact - * `['bash', '-c', command]` argv it is about to spawn and spawns the wrapped - * argv instead. WHICH platform runner confines it — and whether one is - * usable at all (the provider fails CLOSED with a structured - * `SANDBOX_UNAVAILABLE` error rather than passing the argv through) — is the - * provider's concern (`@deepseek-ai/dsh-sandbox-local` first). - * - * Extends `LocalBashExecutor` so all process mechanics — spawn, process-group - * kills, timeout escalation, output collection and spill files, background - * tasks, the credential scrub — are the local implementation's, verbatim. - * This package adds only the seam consumption and the result facts, which is - * exactly the split the capability seam was designed for (a sandboxing - * executor replaces `dsh-bash-local` without touching `dsh-tool-bash`, and - * swapping the confinement backend never touches this package). - * - * A failed run whose stderr carries the selected backend's own denial - * dialect (the signatures the provider stamps on every wrap) is classified - * as a sandbox denial on `BashRunResult.sandbox`, and every confined result - * also carries how completely the selected runner enforces the mode - * (`sandbox.enforcement`, from the provider's wrap). A failure carrying the - * backend's RUNNER-FAILURE signature instead means the sandbox itself broke - * and the command never ran: the foreground path re-throws it as the - * structured fail-closed `SANDBOX_UNAVAILABLE` error (late twin of the - * provider's confine-time throw), a settled background task stamps - * `sandbox.runnerFailed` — either way a broken sandbox can never read as a - * failing command, and the command never slips through unconfined. - * - * Deny-only at the seam, escalation at the tool: a denial is a reported FACT - * here, and the one-shot user-approved escalated retry of a denied action - * (docs/rfc/implemented/feature/2026-07-06-sandbox.md) is driven by - * `dsh-tool-bash` through `ctx.approval` — this executor's contribution is the - * per-call `sandboxMode` override it honors in {@link resolve}: an escalated - * call runs (and classifies, and reports) under ITS granted mode while every - * neighboring call keeps its session's standing mode (or the configured - * default when that session has no override). - * + * Sandbox-consuming bash executor. It wraps the exact local bash argv through + * `ctx.sandbox`, inherits local process mechanics, and reports the selected + * mode, enforcement, and denial facts. Runner failure means the command never + * ran: foreground calls throw `SANDBOX_UNAVAILABLE`, while settled background + * tasks carry `runnerFailed`. The tool owns approval and passes per-call modes. * @module @deepseek-ai/dsh-bash-sandbox */ @@ -91,15 +57,9 @@ export class SandboxBashExecutor extends LocalBashExecutor { private readonly mode: SandboxMode private readonly workspaceRoot: string /** - * Per-task facts, keyed by task id from `start()` until the settle stamp - * consumes them: the mode the task runs under (per-call — an escalated task - * differs from its neighbors) plus its wrap facts. The seam returns facts - * PER WRAP — a provider may legally vary enforcement or dialect between - * calls — so overlapping background tasks must each classify against their - * OWN wrap; a single latest-wrap field would let a later `start()` clobber - * an earlier task's facts before it settles. A `danger-full-access` task - * has NO entry (nothing confined it), which is what the settle stamp keys - * off. + * Per-task mode and wrap facts retained until settlement. Overlapping tasks + * may use different modes or provider facts, so one latest-wrap field would + * misclassify earlier completions. */ private readonly taskFacts = new Map mode]` marker or the [`dsh-sandbox`](../sandbox/README.md) `SANDBOX_UNAVAILABLE` text while keeping runner selection and profiles outside context. +Indirectly, through [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md) and [`dsh-tool-bash`](../../bash/tool-bash/README.md), which render this provider's enforcement and denial facts while the [`dsh-sandbox`](../sandbox/README.md) seam owns the `SANDBOX_UNAVAILABLE` text and runner selection and profiles stay outside context. ## Known Limitations and Deferred Work diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index 007debb9f0..98dc86d23e 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -1,24 +1,8 @@ /** - * `LocalSandboxProvider`: the local implementation of the - * `@deepseek-ai/dsh-sandbox` seam. Wraps a caller's argv in a platform - * confinement runner selected BY PLATFORM: each platform names its runner - * chain ({@link PLATFORM_CHAINS}), a chain of one is selected directly (no - * probe — there is nothing to arbitrate), and a chain of several is probed - * FUNCTIONALLY in preference order (build and enforce a real profile once, - * not `--version`), the verdict cached for the provider's lifetime. Linux: - * `bwrap`, else the `landlock-run` Landlock launcher (kernel confinement - * that needs no userns/mount privileges; distributed as the npm package - * family `node-addon-landlock-run` — the decision recorded in - * docs/rfc/implemented/feature/2026-07-06-sandbox.md); darwin: macOS - * `sandbox-exec` speaking a Seatbelt (SBPL) profile, unprobed. - * When the platform has no chain or no candidate passes, - * {@link LocalSandboxProvider.confine} FAILS CLOSED with the seam's - * structured `SANDBOX_UNAVAILABLE` error instead of passing the argv - * through unconfined; an unusable runner selected WITHOUT a probe fails - * closed at execution time instead (it refuses to run the command), which - * the wrap's `runnerFailureSignatures` let consumers classify as a sandbox - * failure rather than a task failure. - * + * Local sandbox backend. It selects the platform runner chain (Linux bwrap then + * Landlock; macOS Seatbelt), functionally probes competing candidates once, and + * reports each wrap's enforcement and stderr dialects. Missing or unusable + * confinement fails closed rather than returning the original argv. * @module @deepseek-ai/dsh-sandbox-local */ @@ -34,20 +18,10 @@ import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from './pr /** Plugin config. All optional — `static Config` supplies the defaults. */ export interface Config { /** - * Override the sandbox runner argv (the bwrap-shaped profile arguments are - * appended). A NON-EMPTY argv is the operator's assertion that this runner - * exists and FULLY enforces the profile (confinement reports - * `enforcement: 'full'`, and — the runner's kernel mechanism being unknown - * — carries both Linux file-denial dialects as its denial signatures) — - * the runner chain and its probes are skipped, - * and a broken runner fails loudly at execution time. The operator also - * supplies {@link runnerFailureSignatures}, which distinguish the runner - * refusing its profile from the wrapped command failing normally. - * Absent (or empty — the schema normalizes an omitted array to `[]`): the - * built-in platform chains — Linux `bwrap` then the Landlock launcher - * (probed in that order), darwin `sandbox-exec` (the sole candidate, - * selected without a probe). Used for custom/alternative runners and - * for deterministic fake runners in keyless test tiers. + * Override the runner argv; bwrap-shaped profile arguments are appended. A + * non-empty override asserts full enforcement and skips built-in selection and + * probing; a broken runner then fails at execution and must be identifiable by + * {@link runnerFailureSignatures}. */ runnerCommand?: string[] /** @@ -59,16 +33,7 @@ export interface Config { * own failure dialect. */ runnerFailureSignatures?: string[] - /** - * Per-probe timeout in milliseconds for the chain's functional probes - * (default: 5000; must be a positive finite number — Node treats a 0 - * `spawnSync` timeout as UNBOUNDED, so 0 is rejected at construction). A - * probe that exceeds it reads as an unusable rung, so a - * host slow enough to trip the default — cold NFS mounts, heavily loaded - * CI — would otherwise be misclassified `SANDBOX_UNAVAILABLE` with no - * config escape. Bounds ONE probe, and the chain walk runs each at most once - * per provider lifetime. - */ + /** Positive timeout for each functional probe; zero would mean unbounded to Node. */ probeTimeoutMs?: number } @@ -131,13 +96,10 @@ type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt'; enforcement: const PLATFORM_CHAINS: Record = { linux: ['bwrap', 'landlock'], darwin: ['seatbelt'], - // Reserved slot, deliberately empty: Windows support fills it with a - // confinement runner (AppContainer / restricted-token family, shipped from - // its own repository on the landlock-run template) plus a - // SelectedRunner['runner'] union member — the switches' assertNever guards - // then walk the implementer to every site. An empty chain fails closed at - // confine(), identical to an unlisted platform: reserving the slot never - // weakens the fail-closed end. + // Reserved slot, deliberately empty: Windows support fills it with a confinement runner + // (AppContainer / restricted-token family, shipped from its own repository on the + // landlock-run template) plus a SelectedRunner['runner'] union member — the switches' + // assertNever guards then walk the implementer to every site. win32: [], } @@ -168,16 +130,9 @@ function assertPositiveFinite(name: string, value: number): void { } /** - * The denial dialect each runner's kernel speaks — the case-insensitive - * stderr substrings a denied file effect produces under it, carried on every - * wrap (the seam's `ConfinedArgv.denialSignatures`). Kernel facts, not - * tunables: bwrap denies through its read-only bind mounts (EROFS), Landlock - * refuses with EACCES, Seatbelt with EPERM — whose text is also what - * non-file EPERM boundaries print, the residual imprecision the consumer's - * conservative classifier documents. An operator-configured `runnerCommand` - * has an unknown kernel mechanism, so its wraps carry both Linux file-denial - * dialects; bare EPERM stays excluded there (it names non-file boundaries - * the mode vocabulary does not govern). + * The denial dialect each runner's kernel speaks — the case-insensitive stderr substrings a + * denied file effect produces under it, carried on every wrap (the seam's + * `ConfinedArgv.denialSignatures`). */ const DENIAL_SIGNATURES = { bwrap: ['read-only file system'], @@ -187,15 +142,9 @@ const DENIAL_SIGNATURES = { } as const satisfies Record /** - * How each runner's OWN failure identifies itself on stderr (the seam's - * `ConfinedArgv.runnerFailureSignatures`): every runner prefixes its error - * lines with its program name, and the shell's runner-not-found message - * carries the same `name: ` shape (`bash: bwrap: command not found`, - * `bash: …/bin/landlock-run: No such file or directory`) — so one substring - * per runner covers both "runner broke" and "runner missing". Consumers - * match these BEFORE the denial dialect: a runner's error text can contain - * denial words (an unopenable grant root reports `Permission denied`), and - * a runner failure means the command never ran at all. + * Runner-owned stderr prefixes cover both internal refusal and shell-level + * not-found errors. Consumers match these before denial text because the + * command never ran on this path. */ const RUNNER_FAILURE_SIGNATURES = { bwrap: ['bwrap: '], @@ -248,17 +197,15 @@ export class LocalSandboxProvider extends SandboxProvider { } /** - * Wrap `argv` in the selected runner's invocation for `policy` — the - * configured `runnerCommand` when present (the operator's assertion, no - * probe), else the platform chain's runner speaking its own profile - * dialect. Every wrap carries the runner's enforcement completeness, its - * denial dialect, and its runner-failure signatures. + * Wrap `argv` in the selected runner's invocation for `policy` — the configured + * `runnerCommand` when present (the operator's assertion, no probe), else the platform + * chain's runner speaking its own profile dialect. + * * @param argv - the exact argv the caller is about to spawn. * @param policy - the file-effect policy this execution runs under. - * @returns the wrapped argv plus the selected backend's enforcement - * completeness, denial signatures, and runner-failure signatures; - * throws the fail-closed `SANDBOX_UNAVAILABLE` error when the platform - * has no usable runner. + * @returns the wrapped argv plus the selected backend's enforcement completeness, denial + * signatures, and runner-failure signatures; throws the fail-closed + * `SANDBOX_UNAVAILABLE` error when the platform has no usable runner. */ confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv { if (this.runnerCommand !== undefined) { @@ -267,14 +214,9 @@ export class LocalSandboxProvider extends SandboxProvider { argv: [...this.runnerCommand, ...bwrapProfileArgs(policy), '--', ...argv], enforcement: 'full', denialSignatures: DENIAL_SIGNATURES.runnerCommand, - // The operator names the configured runner's OWN pre-exec refusal - // dialect; the consumer additionally re-joins the wrap through an - // outer `bash -c 'exec …'`, so we can add the missing/unexecutable - // outer-shell shapes ourselves. Scoping every automatic shape to - // argv0 keeps in-command errors out (a bare `exec:`/`Permission - // denied` prefix would claim tool output; `exec: : not found` - // cannot). The residual text-collision trade is documented by the - // seam's conservative classifier contract. + // The operator names the configured runner's own pre-exec refusal dialect; the consumer + // additionally re-joins the wrap through an outer `bash -c 'exec …'`, so we can add the + // missing/unexecutable outer-shell shapes ourselves. runnerFailureSignatures: [ ...this.configuredRunnerFailureSignatures, `exec: ${argv0}: not found`, @@ -320,11 +262,7 @@ export class LocalSandboxProvider extends SandboxProvider { const chain = this.internals.chain ?? PLATFORM_CHAINS[this.internals.platform ?? process.platform] ?? [] const [first, ...rest] = chain if (first === undefined) return 'unavailable' - // One candidate = nothing to arbitrate: select it without probing. Its - // runner fails closed at EXECUTION time if unusable (refuses to run the - // command), and the wrap's runnerFailureSignatures let the consumer - // classify that as a sandbox failure — never a silent unconfined run, - // never a plain task failure. + // A sole candidate needs no arbitration; its execution-time refusal still fails closed. if (rest.length === 0) return { runner: first, enforcement: STATIC_ENFORCEMENT[first] } for (const runner of chain) { const enforcement = this.probeRunner(runner)