Bläddra i källkod

docs: align sandbox extraction with prose standard

Tianyi Cui 3 månader sedan
förälder
incheckning
bbead98c1d

+ 7 - 26
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`
 

+ 16 - 73
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<BashTaskId, {
     mode: ConfinedSandboxMode
@@ -143,11 +103,8 @@ export class SandboxBashExecutor extends LocalBashExecutor {
     }
     const confined = this.confine(spec.command, mode)
     const result = await super.run({ ...spec, command: confined.command })
-    // Runner failure outranks denial: the sandbox itself failed and the
-    // command NEVER RAN — surface the same structured fail-closed error a
-    // confine-time discovery throws (late detection, same outcome), with
-    // the runner's own first stderr line as the cause. Returning it as a
-    // task result would let a broken sandbox read as a failing command.
+    // Runner failure outranks denial because the command did not run. Throw the
+    // same fail-closed error as confine-time discovery with the first stderr line.
     if (classifyRunnerFailure(result, confined.runnerFailureSignatures)) {
       throw new SandboxUnavailableError(mode, result.stderr.text.trim().split('\n')[0])
     }
@@ -158,11 +115,8 @@ export class SandboxBashExecutor extends LocalBashExecutor {
     // Same stamped-by-resolve invariant as run().
     const mode = spec.sandboxMode as SandboxMode
     if (mode === 'danger-full-access') return super.start(spec)
-    // Sandbox facts are stamped at settle time by {@link notifyTaskDone}
-    // (denial classification runs against the settled task's collected
-    // stderr). The map entry lands synchronously after spawn, strictly
-    // before the earliest possible settle (a process exit reaches us no
-    // sooner than the next tick).
+    // Classification needs settled stderr. Store facts synchronously after
+    // spawn, before the earliest process completion can be observed.
     const confined = this.confine(spec.command, mode)
     const task = super.start({ ...spec, command: confined.command })
     const { enforcement, denialSignatures, runnerFailureSignatures } = confined
@@ -171,27 +125,16 @@ export class SandboxBashExecutor extends LocalBashExecutor {
   }
 
   /**
-   * Stamp the sandbox facts BEFORE completion listeners run: the base
-   * executor notifies from inside the task's settle path, so overriding the
-   * notification point is what makes `task.sandbox` visible to `onTaskDone`
-   * consumers and `done` awaiters alike. Each task classifies against the
-   * facts of ITS OWN wrap and reports ITS OWN mode (consumed from the
-   * per-task map here — settle is the entry's end of life): with per-call
-   * escalation, tasks under different modes settle side by side, so keying
-   * anything off the configured default would misreport them. A
-   * `danger-full-access` task has no map entry and carries no facts (nothing
-   * confined it); a signal-killed task (null exit code) is never a denial,
-   * mirroring the foreground classifier.
+   * Stamp per-task sandbox facts before completion listeners and `done` settle.
+   * Full-access tasks have no facts; signal deaths are not denials.
    */
   protected override notifyTaskDone(task: BashTask): void {
     const facts = this.taskFacts.get(task.id)
     if (facts !== undefined) {
       this.taskFacts.delete(task.id)
       const stderr = this.collectedStderr(task.id)
-      // Runner failure outranks denial (the command never ran; the runner's
-      // own error text can contain denial words). A settled task has no
-      // error channel left, so the fact IS the surface here — the foreground
-      // path throws instead.
+      // Runner failure outranks denial. Background settlement has no throw
+      // channel, so this fact is its counterpart to the foreground exception.
       const runnerFailed = matchesSignature(task.exitCode, stderr, facts.runnerFailureSignatures)
       task.sandbox = {
         mode: facts.mode,

+ 3 - 3
packages/sandbox/sandbox-local/README.md

@@ -10,9 +10,9 @@ Policy is per call; the provider stores only the mechanism and cached runner ver
 
 The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes.
 
-The Landlock launcher comes from the npm package family [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) — an entry package (this package's one runtime dependency) plus per-platform binary packages selected by npm's `os`/`cpu` fields, built and released from [its own repository](https://github.com/deepseek-harness/node-addon-landlock-run). The entry package owns the launcher's CLI contract: `launcherPath()` resolution (a host with no platform package yields a never-existing path whose probe fails exactly like an unenforcing kernel), the functional `probe()`, and `grantArgs()` flag spelling — versioned together with the binary, so probe-report parsing can never drift against it. This provider keeps only the policy side: the mode → grants mapping (`landlockProfileArgs`) and the ladder. The consumer path is rehearsed by `tests/packed-install.e2e.ts`: pack THIS package's closure, install into a throwaway consumer with the launcher family coming from the registry, assert the installed binary executable (a stripped mode bit must not masquerade as a non-enforcing kernel), and confine through it under plain `node`.
+[`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) supplies the platform launcher, functional probe, and CLI argument vocabulary. This provider owns only mode-to-grant mapping and runner selection. Keeping path resolution and probe parsing with the versioned binary prevents contract drift.
 
-Every rung has its keyless world-proof (`tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, `tests/seatbelt.e2e.ts`), each self-skipping where its runner is absent; CI's `sandbox-e2e` matrix runs all of them against real kernels (bwrap plus one Landlock leg per architecture on Linux, Seatbelt on macOS) and fails on a silent all-skip.
+Each rung has a self-skipping keyless world-effect test; CI runs platform legs against real kernels and rejects a silent all-skip. The packed-install test exercises the registry launcher and executable mode through a plain-Node consumer.
 
 ```yaml
 - id: sandbox
@@ -23,7 +23,7 @@ Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/); see [the
 
 ## Model Experience
 
-Indirectly, through `dsh-bash-sandbox` and `dsh-tool-bash`, which render this provider's enforcement dialect as the exact `[sandbox: file access denied under <mode> 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
 

+ 30 - 92
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<string, readonly SelectedRunner['runner'][]> = {
   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<SelectedRunner['runner'] | 'runnerCommand', readonly string[]>
 
 /**
- * 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: <argv0>: 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)