docs: align sandbox extraction with prose standard

This commit is contained in:
Tianyi Cui
2026-07-14 23:42:22 +08:00
parent d6d8c6ae0e
commit bbead98c1d
4 changed files with 56 additions and 194 deletions
+3 -3
View File
@@ -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
View File
@@ -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)