diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml
new file mode 100644
index 0000000000..561d74b587
--- /dev/null
+++ b/.github/workflows/sandbox.yml
@@ -0,0 +1,124 @@
+# Sandbox CI: the keyless real-kernel confinement proofs. A separate workflow
+# from ci.yml because the axis is different — these jobs fan out over
+# OS×runner (kernel capabilities), not node versions. The Landlock launcher
+# arrives from the registry with `pnpm install` (the npm package family
+# `node-addon-landlock-run`, built and released from its own repository), so
+# these legs exercise the true consumer path — nothing is compiled here.
+name: Sandbox
+
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ # Keyless real-kernel sandbox proofs (sandbox RFC § Testing): each ladder
+ # rung is only provable on a host where it enforces, so this job fans out
+ # an OS×runner matrix — bwrap and Landlock on Linux (separate legs: the
+ # Landlock files force the bwrap rung off, so each leg proves exactly one
+ # rung; Landlock twice, once per architecture, each confining through the
+ # registry-installed launcher), Seatbelt on macOS (sandbox-exec ships with
+ # the OS). One node
+ # version only: kernel confinement does not vary by node, and ci.yml's
+ # node matrix already covers the node axis.
+ #
+ # The e2e files self-skip where their runner is absent, so a leg that lost
+ # its runner (no bwrap, kernel without Landlock, macOS without
+ # sandbox-exec) would otherwise pass as a false green — the same trap
+ # e2e.yml's key preflight guards against. Each leg therefore asserts BOTH
+ # its platform files actually ran: `Test Files 2 passed (2)`, no skips.
+ sandbox-e2e:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ runner: bwrap
+ - os: ubuntu-24.04
+ runner: landlock
+ - os: ubuntu-24.04-arm
+ runner: landlock
+ - os: macos-latest
+ runner: seatbelt
+ name: sandbox e2e (${{ matrix.runner }}, ${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Enable corepack (pnpm)
+ run: corepack enable
+
+ - name: Install (immutable)
+ run: pnpm install --frozen-lockfile
+
+ # The bwrap rung needs bubblewrap on PATH and unprivileged user
+ # namespaces. Ubuntu 24.04 gates the latter behind an AppArmor knob;
+ # lift it best-effort — on images where the knob is absent the
+ # functional probe (and the run-guard below) is the arbiter anyway.
+ - name: Install bubblewrap (unrestrict userns)
+ if: matrix.runner == 'bwrap'
+ run: |
+ sudo apt-get update -q
+ sudo apt-get install -yq bubblewrap
+ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \
+ || echo "apparmor userns knob absent — the functional probe decides"
+
+ # The unit suite runs on ubuntu in `checks`; this is the one darwin leg
+ # in the workflow, so run it here too — the platform-dependent unit
+ # expectations (Seatbelt path canonicalization: /tmp IS /private/tmp)
+ # take their darwin branch only on this runner.
+ - name: Unit tests (darwin parity)
+ if: matrix.runner == 'seatbelt'
+ run: pnpm run test
+
+ - name: Sandbox e2e (real kernel confinement, world-verified)
+ # NO_COLOR: vitest force-enables ANSI color under GITHUB_ACTIONS even
+ # without a TTY, which would thread escape codes through the summary
+ # line the run-guard greps.
+ env:
+ NO_COLOR: 1
+ run: |
+ set -u +e -o pipefail
+ out=$(pnpm exec vitest run --config vitest.e2e.config.ts \
+ packages/sandbox/sandbox-local/tests/${{ matrix.runner }}.e2e.ts \
+ packages/bash/bash-sandbox/tests/${{ matrix.runner }}.e2e.ts 2>&1); status=$?
+ echo "$out"
+ [ "$status" -eq 0 ]
+ # Both platform files must have RUN — a self-skip (runner missing on
+ # the very platform that exists to prove it) is a failure, not a pass.
+ echo "$out" | grep -qE 'Test Files[[:space:]]+2 passed \(2\)'
+
+ # Publish-path rehearsal, Landlock legs only (the pack gates need built
+ # lib/). The e2e packs the workspace closure, installs the tarballs
+ # into a throwaway consumer — npm pulling `node-addon-landlock-run`
+ # and its platform package from the registry, the true consumer path —
+ # and confines through the INSTALLED launcher, asserting it executable
+ # apart (a mode-stripped binary must not masquerade as a non-enforcing
+ # kernel). Same no-silent-skip guard as above.
+ - name: Build packages (lib/ for the pack rehearsal)
+ if: matrix.runner == 'landlock'
+ run: pnpm run build
+
+ - name: Packed-distribution e2e (pack → install → confine)
+ if: matrix.runner == 'landlock'
+ env:
+ NO_COLOR: 1
+ run: |
+ set -u +e -o pipefail
+ out=$(pnpm exec vitest run --config vitest.e2e.config.ts \
+ packages/sandbox/sandbox-local/tests/packed-install.e2e.ts 2>&1); status=$?
+ echo "$out"
+ [ "$status" -eq 0 ]
+ echo "$out" | grep -qE 'Test Files[[:space:]]+1 passed \(1\)'
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index abddc73ed2..8dac9fbfd6 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -46,6 +46,9 @@ flowchart LR
pkg_bash_local["bash-local"]
pkg_hooks_claude["hooks-claude"]
pkg_hooks_codex["hooks-codex"]
+ pkg_sandbox["sandbox"]
+ svc_sandbox["ctx.sandbox
Process-sandbox seam"]
+ pkg_sandbox_local["sandbox-local"]
pkg_approval["approval"]
svc_approval["ctx.approval
Approval seam"]
pkg_code_runtime["code-runtime"]
@@ -90,6 +93,8 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_sandbox --> svc_sandbox
+ pkg_sandbox_local --> svc_sandbox
pkg_session --> svc_sessions
pkg_session_persistence --> svc_sessionPersistence
pkg_session_persistence_jsonl --> svc_sessionPersistence
@@ -165,6 +170,7 @@ flowchart LR
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-agent`](../packages/ui/stdio-agent), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-core`](../packages/core/agent-core) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local. |
+| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | - | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.approval` | `seam` | [`approval`](../packages/approval/approval) | [`acp`](../packages/ui/acp) | [`tools`](../packages/core/tools) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). |
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. |
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index ed175f250b..b092b20537 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -430,6 +430,42 @@ export interface Config {
Source: [`packages/guard/repeat-tool-guard/src/index.ts:55`](../packages/guard/repeat-tool-guard/src/index.ts)
+## `@deepseek-ai/dsh-sandbox-local`
+
+```ts config-catalog
+/** 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 spawn time like any missing command.
+ * 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.
+ */
+ runnerCommand?: 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.
+ */
+ probeTimeoutMs?: number
+}
+```
+
+Source: [`packages/sandbox/sandbox-local/src/index.ts:36`](../packages/sandbox/sandbox-local/src/index.ts)
+
## `@deepseek-ai/dsh-session-persistence-jsonl`
Requires: `sessions`
@@ -985,6 +1021,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
+- `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts))
- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts))
- `@deepseek-ai/dsh-workflow` — abstract `WorkflowService` ([`packages/workflow/workflow/src/index.ts`](../packages/workflow/workflow/src/index.ts))
diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md
index eb0fe8ffd1..e0fa68bab9 100644
--- a/docs/cordis-catalog/services.md
+++ b/docs/cordis-catalog/services.md
@@ -157,6 +157,22 @@ Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../co
Source: [`packages/llm/llm/src/index.ts:88`](../../packages/llm/llm/src/index.ts)
+## `ctx.sandbox` — `SandboxProvider` (abstract seam)
+
+Abstract process-sandbox service. Subclass, implement confine, and load the subclass as a plugin — it registers as `ctx.sandbox` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
+
+Semantics every implementation must honor:
+
+- confine either returns an argv whose runner ENFORCES the policy or fails closed — at `confine` time with SandboxUnavailableError (no backend for this host), or at EXECUTION time by the runner itself refusing to run the command (exiting without exec'ing it, identified by ConfinedArgv.runnerFailureSignatures). A silent unconfined passthrough is never a legal outcome on either path.
+- Probing exists to ARBITRATE between multiple candidate backends and may be skipped when a platform has exactly one: the sole candidate is selected directly and the runner's exec-time fail-closed refusal carries the safety property. When probing does run, it is functional (actually enforcing a profile, not a version check), at most once per provider lifetime; `confine` itself spawns nothing beyond that one-time probing.
+- The returned ConfinedArgv.enforcement states the backend's actual completeness for THIS host; `partial` is reported, never silently upgraded to `full`.
+
+```ts cordis-catalog
+abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
+```
+
+Source: [`packages/sandbox/sandbox/src/index.ts:180`](../../packages/sandbox/sandbox/src/index.ts)
+
## `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
Abstract durable session-persistence service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.sessionPersistence` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
diff --git a/docs/module-graph.md b/docs/module-graph.md
index 2bcd15c967..2c2e94a254 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -98,6 +98,10 @@ flowchart TD
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
end
+ subgraph group_sandbox["packages/sandbox"]
+ pkg_sandbox["sandbox"]
+ pkg_sandbox_local["sandbox-local"]
+ end
subgraph group_workflow["packages/workflow"]
pkg_tool_workflow["tool-workflow"]
pkg_workflow["workflow"]
@@ -116,6 +120,7 @@ flowchart TD
pkg_fs --> pkg_brand
pkg_fs --> pkg_llm
pkg_web --> pkg_llm
+ pkg_sandbox --> pkg_llm
pkg_agent --> pkg_brand
pkg_agent --> pkg_llm
pkg_agent --> pkg_session
@@ -134,6 +139,8 @@ flowchart TD
pkg_session_persistence --> pkg_session
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_sandbox_local --> pkg_llm
+ pkg_sandbox_local --> pkg_sandbox
pkg_compact_basic --> pkg_agent
pkg_compact_basic --> pkg_compact
pkg_compact_basic --> pkg_llm
@@ -288,6 +295,7 @@ flowchart TD
| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) |
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
+| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) |
@@ -299,6 +307,7 @@ flowchart TD
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
diff --git a/knip.json b/knip.json
index 67da80cd2a..ba6dfb6fb2 100644
--- a/knip.json
+++ b/knip.json
@@ -2,6 +2,7 @@
"$schema": "https://unpkg.com/knip@5/schema.json",
"exclude": ["duplicates"],
"ignoreWorkspaces": ["vendor/*"],
+ "ignoreBinaries": ["bwrap", "sandbox-exec"],
"workspaces": {
".": {
"entry": [
@@ -14,6 +15,10 @@
],
"project": ["scripts/**/*.ts", "examples/**/*.ts"]
},
+ "packages/sandbox/sandbox-local": {
+ "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
+ "project": ["src/**/*.ts", "tests/**/*.ts"]
+ },
"packages/*/*": {
"entry": ["tests/**/*.spec.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts"]
diff --git a/packages/README.md b/packages/README.md
index 8d1580b291..7a389b29fa 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -4,7 +4,7 @@ Harness packages, all under the `@deepseek-ai/dsh-*` scope. Each package is a Co
## Hierarchy
-Packages are grouped by modular role at `packages///`. The group directory is a pure container (no `package.json` of its own); the package name stays `@deepseek-ai/dsh-` regardless of group. **Each group README is the canonical per-package map** — package roles, ctx keys, and the product-vs-support split live there, next to the code.
+Packages are grouped by modular role at `packages///`. The group directory is a pure container (no `package.json`); the package name stays `@deepseek-ai/dsh-` regardless of group. **Each group README is the canonical per-package map** — package roles, ctx keys, and the product-vs-support split live there, next to the code.
| Group | Role | Release expectation |
|---|---|---|
@@ -12,6 +12,7 @@ Packages are grouped by modular role at `packages///`. The group dir
| [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface |
| [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface |
| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs + a worker-thread backend | Product — stable surface |
+| [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable surface |
| [`approval/`](approval/README.md) | One-shot permission decisions | Product — stable surface |
| [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, and the model-facing file tools | Product — stable surface |
| [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface |
diff --git a/packages/sandbox/README.md b/packages/sandbox/README.md
new file mode 100644
index 0000000000..bc535bbc8a
--- /dev/null
+++ b/packages/sandbox/README.md
@@ -0,0 +1,12 @@
+# sandbox/ — process-sandbox capability family
+
+The confinement half of the [capability-seam split](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md): an abstract provider interface and platform backends. Consumers hand `ctx.sandbox` the exact argv they are about to spawn and spawn the returned (wrapped) argv instead; policy (`SandboxPolicy`: mode + workspace root) rides each call, so different consumers confine under different policies at the same instant. All **product** packages.
+
+| Package | Role | ctx key |
+|---|---|---|
+| `sandbox/` | Abstract process-sandbox seam (the `SandboxProvider` contract + the mode/enforcement/policy vocabulary) | `ctx.sandbox` |
+| `sandbox-local/` | Local backends by platform chain: Linux `bwrap` else the `landlock-run` launcher (the npm-distributed [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) family, built and released from its own repository), darwin `sandbox-exec`/Seatbelt — multi-candidate chains functionally probed, sole candidates selected directly, verdict cached, fail-closed | (registers `ctx.sandbox`) |
+
+The seam confines SAME-WORLD subprocesses only (shared filesystem and kernel). Containers, microVMs, and remote executors are NOT backends here — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups; the boundary is recorded in [the sandbox RFC](../../docs/rfc/proposed/feature/2026-07-06-sandbox.md).
+
+The staged first consumer is the sandboxed bash executor (it hands over the exact `['bash', '-c', command]` argv it is about to spawn). In-process tools (fs/web) cannot be confined by an OS wrapper — their sandbox semantics are policy at their own seams (the sandbox RFC's cross-family phase).
diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md
new file mode 100644
index 0000000000..c4ca758aee
--- /dev/null
+++ b/packages/sandbox/sandbox-local/README.md
@@ -0,0 +1,18 @@
+# @deepseek-ai/dsh-sandbox-local
+
+Local implementation of the [`@deepseek-ai/dsh-sandbox`](../sandbox/) seam: wraps a caller's argv in a platform confinement runner. Selection is BY PLATFORM, resolved once and cached: each platform names its runner chain, a chain of one is selected directly (probing arbitrates between candidates — a sole candidate leaves nothing to arbitrate), and a chain of several is probed functionally in preference order. Linux: [`bwrap`](https://github.com/containers/bubblewrap) when its probe passes, else the [`landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) Landlock launcher (kernel confinement that needs no userns/mount privileges — see the [sandbox RFC](../../../docs/rfc/proposed/feature/2026-07-06-sandbox.md) for the prebuilt-binary decision and profile-parity notes); darwin: `sandbox-exec` speaking a Seatbelt (SBPL) profile, unprobed. A platform with no chain means `confine()` FAILS CLOSED with the seam's structured `SANDBOX_UNAVAILABLE` error (win32 today: a reserved, deliberately empty chain awaiting an AppContainer-family runner); an unprobed runner that turns out unusable fails closed at EXECUTION instead — it refuses to run the command, and every wrap's `runnerFailureSignatures` let the consumer classify that as a sandbox failure rather than a task failure. Never a silent unconfined passthrough on any path.
+
+Policy is per call (`SandboxPolicy`: mode + workspace root); the provider holds only the mechanism and the cached ladder verdict. Every wrap reports the selected runner's `enforcement` (`full`, or `partial` on an older Landlock ABI that governs only a subset of accesses — read from the launcher's `--probe` report line) and its `denialSignatures` — the stderr dialect that rung's kernel speaks on a denied file effect (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt), which stderr-inferring consumers match instead of a cross-runner union. A non-empty `runnerCommand` config is the operator's assertion of a runner that fully enforces the bwrap-shaped profile: the ladder and probes are skipped (the wrap carries both Linux denial dialects, the mechanism being unknown) — also the deterministic fake-runner seam for keyless test tiers. Its runner-failure dialect is the OUTER shell's argv0-scoped failure shapes (`exec: : not found`, `: No such file or directory`, `: Permission denied`) — the consumer re-joins the wrap through `bash -c 'exec …'`, so a missing or unexecutable configured runner classifies as a sandbox failure (fail closed at execution), never as a failing command or a denial. `probeTimeoutMs` (default 5000) bounds each functional probe, the escape hatch for hosts slow enough that a timed-out probe would otherwise misread as `SANDBOX_UNAVAILABLE`.
+
+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`.
+
+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.
+
+```yaml
+- id: sandbox
+ name: '@deepseek-ai/dsh-sandbox-local'
+```
+
+The staged first consumer is the sandboxed bash executor.
diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json
new file mode 100644
index 0000000000..d2f8b34161
--- /dev/null
+++ b/packages/sandbox/sandbox-local/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@deepseek-ai/dsh-sandbox-local",
+ "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, or macOS Seatbelt — functionally probed, fail-closed",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/types/**/*.d.ts",
+ "lib/types/**/*.d.ts.map",
+ "src"
+ ],
+ "license": "BSD-3-Clause",
+ "peerDependencies": {
+ "@deepseek-ai/dsh-llm": "^0.0.1",
+ "@deepseek-ai/dsh-sandbox": "^0.0.1",
+ "cordis": "^4.0.0-rc.6"
+ },
+ "dependencies": {
+ "node-addon-landlock-run": "0.0.0-test.0",
+ "schemastery": "^3.18.0"
+ },
+ "devDependencies": {
+ "@deepseek-ai/dsh-llm": "workspace:^",
+ "@deepseek-ai/dsh-sandbox": "workspace:^",
+ "cordis": "^4.0.0-rc.6"
+ }
+}
diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts
new file mode 100644
index 0000000000..01189bc78b
--- /dev/null
+++ b/packages/sandbox/sandbox-local/src/index.ts
@@ -0,0 +1,455 @@
+/**
+ * `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/proposed/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.
+ *
+ * @module @deepseek-ai/dsh-sandbox-local
+ */
+
+import { spawnSync } from 'node:child_process'
+import { realpathSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { grantArgs as landlockGrantArgs, LAUNCHER_BIN, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock } from 'node-addon-landlock-run'
+import { Context } from 'cordis'
+import z from 'schemastery'
+import { assertNever } from '@deepseek-ai/dsh-llm'
+import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
+import type { ConfinedArgv, ConfinedSandboxMode, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
+
+/** 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 spawn time like any missing command.
+ * 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.
+ */
+ runnerCommand?: 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.
+ */
+ probeTimeoutMs?: number
+}
+
+/**
+ * The `bwrap` profile arguments for one policy. The whole host tree is bound
+ * read-only; a fresh `/dev` keeps `>/dev/null` redirects working and a fresh
+ * `/proc` keeps process-inspecting tools working. `workspace-write`
+ * additionally mounts an ephemeral writable `/tmp` and rebinds the workspace
+ * root read-write (bind order matters: later binds overlay earlier ones).
+ * Deliberately NO `--unshare-pid` (it would break the process-group kill
+ * semantics shell consumers rely on) and NO network unsharing (the seam's
+ * mode vocabulary promises file effects only).
+ * @param policy - the file-effect policy to express as bwrap arguments.
+ * @returns the bwrap profile arguments (before the trailing `--` + argv).
+ */
+export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
+ const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent']
+ if (policy.mode === 'workspace-write') {
+ args.push('--tmpfs', '/tmp')
+ args.push('--bind', policy.workspaceRoot, policy.workspaceRoot)
+ }
+ return args
+}
+
+/**
+ * The `landlock-run` grant arguments for one policy — the bwrap
+ * profile's file-effect semantics expressed as a Landlock allow-list
+ * (Landlock cannot mount, so there are no fresh/ephemeral filesystems). The
+ * whole tree is readable and executable; of `/dev`, ONLY `/dev/null` is
+ * writable — a whole-`/dev` grant would expose real host paths beneath it
+ * (`/dev/shm`, a shared tmpfs) to persistent writes, which `read-only`
+ * promises never happen. bwrap can hand out a fresh ephemeral `/dev`; on the
+ * host's own `/dev` the write grant must be node-by-node, and `>/dev/null`
+ * is the one redirects need. `workspace-write` adds the HOST `/tmp` (shared
+ * and persistent, where bwrap's is ephemeral — the honest difference,
+ * recorded in the sandbox RFC's runner notes) plus the workspace
+ * root read-write. The flag spelling belongs to `node-addon-landlock-run`'s
+ * `grantArgs`; this function owns only the policy → grants mapping.
+ * @param policy - the file-effect policy to express as launcher grants.
+ * @returns the launcher grant arguments (before `--` + argv).
+ */
+export function landlockProfileArgs(policy: SandboxPolicy): string[] {
+ const readWrite = ['/dev/null']
+ if (policy.mode === 'workspace-write') {
+ readWrite.push('/tmp', policy.workspaceRoot)
+ }
+ return landlockGrantArgs({ readOnly: ['/'], readWrite })
+}
+
+/**
+ * Resolve a granted root to the path the kernel actually sees. Seatbelt path
+ * filters match the CANONICAL path (symlinks resolved), and the roots this
+ * profile grants are symlinked on every macOS: `/tmp` is `/private/tmp` and
+ * the user temp dir lives under `/var` → `/private/var` — an as-spelled
+ * grant would match nothing.
+ */
+function canonicalPath(path: string): string {
+ try {
+ return realpathSync(path)
+ } catch {
+ // realpathSync failed: the path (or a prefix) is missing or unreadable.
+ // Grant the spelling as-is — an unresolvable root matches nothing until
+ // it exists, which is the conservative outcome, and inventing a fallback
+ // resolution here would grant a path the caller never named.
+ return path
+ }
+}
+
+/** Quote one path as an SBPL string literal (backslashes and double quotes escaped). */
+function sbplString(path: string): string {
+ return `"${path.replaceAll('\\', String.raw`\\`).replaceAll('"', String.raw`\"`)}"`
+}
+
+/**
+ * The `sandbox-exec` arguments for one policy: `-p` plus a Seatbelt (SBPL)
+ * profile with the same file-effect semantics as the other dialects, built
+ * as allow-default → `(deny file-write*)` → write allow-list (later rules
+ * win), so exactly the mode's promised file effects are governed — network
+ * and process visibility stay unrestricted, which is all the seam's mode
+ * vocabulary claims. Of `/dev`, ONLY the `/dev/null` literal is writable
+ * (the same node-not-directory reasoning as the Landlock grant).
+ * `workspace-write` adds the workspace root, the host `/tmp`, and the
+ * per-user darwin temp dir (`os.tmpdir()`, launchd's `TMPDIR`, inherited by
+ * the confined child) — on darwin that directory IS the platform's `/tmp`
+ * for every mkstemp-family tool, so omitting it would deny the mode's
+ * promised temp area. All granted roots are canonicalized because Seatbelt
+ * matches resolved paths ({@link canonicalPath}); duplicates after
+ * resolution collapse.
+ * @param policy - the file-effect policy to express as an SBPL profile.
+ * @returns the `sandbox-exec` arguments (`-p` + profile, before `--` + argv).
+ */
+export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
+ const forms = ['(version 1)', '(allow default)', '(deny file-write*)', `(allow file-write* (literal ${sbplString('/dev/null')}))`]
+ if (policy.mode === 'workspace-write') {
+ const roots = [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
+ forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbplString(root)})`).join(' ')})`)
+ }
+ return ['-p', forms.join(' ')]
+}
+
+/**
+ * Functional `bwrap` probe: can it actually build the read-only profile on
+ * this host? (`--version` alone would miss a disabled unprivileged user
+ * namespace.) Synchronous by design — it runs once, lazily, before the first
+ * confined wrap, and the chain's verdict is cached for the provider's
+ * lifetime. `timeoutMs` bounds the probe (the `probeTimeoutMs` config).
+ * The Landlock rung needs no such helper: resolution (`launcherPath`) and
+ * the functional probe (`probe`) come from `node-addon-landlock-run`, the
+ * package family that ships the launcher binary itself, so the probe-report
+ * parsing can never drift against the binary.
+ */
+function defaultProbeBwrap(timeoutMs: number): boolean {
+ const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {
+ timeout: timeoutMs,
+ stdio: 'ignore',
+ })
+ return probe.status === 0
+}
+
+/**
+ * Functional Seatbelt probe: apply the real `read-only` profile through
+ * `sandbox-exec -p` and run `true` under it — exit 0 means the kernel
+ * accepted and enforced the profile (`sandbox-exec` exits non-zero when
+ * `sandbox_init` refuses it). A missing `sandbox-exec` (every non-macOS
+ * host) fails the spawn and probes `unusable`, exactly like the other
+ * rungs' absent binaries. Apple marks the CLI deprecated but ships it on
+ * every macOS; if it ever disappears, this probe is what fails closed.
+ */
+function defaultProbeSeatbelt(seatbeltExec: string, timeoutMs: number): boolean {
+ const probe = spawnSync(seatbeltExec, [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], {
+ timeout: timeoutMs,
+ stdio: 'ignore',
+ })
+ return probe.status === 0
+}
+
+/** Test seam: inject probe verdicts / a fake launcher / a platform without real runners. */
+export interface SandboxInternals {
+ /** Replaces `process.platform` for chain selection (exercise any platform's chain from any host). */
+ platform?: string
+ /** Replaces the platform's chain wholesale (walk mechanics — e.g. probing a rung the product chains only reach unprobed). */
+ chain?: readonly SelectedRunner['runner'][]
+ /** Replaces the functional `bwrap` probe (the Linux chain's first rung). */
+ probeBwrap?: () => boolean
+ /** Replaces the functional Landlock launcher probe (the Linux chain's second rung). */
+ probeLandlock?: (launcher: string) => SandboxEnforcement | 'unusable'
+ /** Replaces the functional Seatbelt probe (the darwin chain's sole rung — only consulted if that chain ever grows). */
+ probeSeatbelt?: (seatbeltExec: string) => boolean
+ /** Replaces the resolved `landlock-run` launcher path (a fake launcher script). */
+ landlockLauncher?: string
+ /** Replaces the `sandbox-exec` executable the probe and wraps invoke (a fake script). */
+ seatbeltExec?: string
+}
+
+/** The chain's verdict: which runner confines, and how completely it enforces. */
+type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt'; enforcement: SandboxEnforcement }
+
+/**
+ * The runner chain per platform — selection is BY PLATFORM first, probes
+ * second: a platform's chain is probed in preference order only when it has
+ * MORE than one candidate (probing arbitrates; it does not re-validate a
+ * choice that has no alternative). A platform with no chain fails closed at
+ * `confine()`. Linux prefers `bwrap` (its mount profile is closest to the
+ * mode vocabulary) over the Landlock launcher; darwin has exactly one
+ * candidate, selected without any probe.
+ */
+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.
+ win32: [],
+}
+
+/**
+ * Enforcement completeness a rung claims when selected WITHOUT a probe (a
+ * chain of one). `bwrap` and Seatbelt govern every promised file effect by
+ * construction, so the claim is a profile fact; `landlock` is listed for the
+ * table's totality but is unreachable unprobed today (the Linux chain has
+ * two rungs, so it is only ever selected through its probe, whose report is
+ * what distinguishes full from per-ABI-partial — and the launcher additionally
+ * self-reports partial enforcement on stderr at every confined run).
+ */
+const STATIC_ENFORCEMENT: Record = {
+ bwrap: 'full',
+ landlock: 'full',
+ seatbelt: 'full',
+}
+
+/**
+ * A probe bound must be a positive finite number: Node treats
+ * `spawnSync({ timeout: 0 })` as NO timeout, so an unvalidated 0 would
+ * silently mean "unbounded" — the opposite of what the field promises.
+ */
+function assertPositiveFinite(name: string, value: number): void {
+ if (!Number.isFinite(value) || value <= 0) {
+ throw new Error(`sandbox-local: ${name} must be a positive finite number`)
+ }
+}
+
+/**
+ * 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).
+ */
+const DENIAL_SIGNATURES = {
+ bwrap: ['read-only file system'],
+ landlock: ['permission denied'],
+ seatbelt: ['operation not permitted'],
+ runnerCommand: ['read-only file system', 'permission denied'],
+} 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.
+ */
+const RUNNER_FAILURE_SIGNATURES = {
+ bwrap: ['bwrap: '],
+ landlock: [`${LAUNCHER_BIN}: `],
+ seatbelt: ['sandbox-exec: '],
+} as const satisfies Record
+
+/**
+ * Local process-sandbox provider. Registers as `ctx.sandbox`. Stateless
+ * apart from the cached chain verdict — it spawns nothing but the one-time
+ * probes, so there is no disposal work beyond cordis' own.
+ */
+export class LocalSandboxProvider extends SandboxProvider {
+ // Inline schema call: the config catalog walks `static Config` statically.
+ static Config: z = z.object({
+ runnerCommand: z.array(z.string()).default([]),
+ probeTimeoutMs: z.natural().default(5_000),
+ })
+
+ /** Test seam (mirrors the bash executors' `internals`). */
+ internals: SandboxInternals = {}
+
+ private readonly runnerCommand: string[] | undefined
+ private readonly probeTimeoutMs: number
+ /** Cached chain verdict; undefined until the first confined wrap needs it. */
+ private selectedRunner: SelectedRunner | 'unavailable' | undefined
+
+ constructor(ctx: Context, config: Config) {
+ super(ctx)
+ // The schema (static Config) defaults both fields — the casts record
+ // those runtime facts. An empty runnerCommand means "not configured":
+ // use the platform chain.
+ const runner = config.runnerCommand as string[]
+ this.runnerCommand = runner.length > 0 ? runner : undefined
+ this.probeTimeoutMs = config.probeTimeoutMs as number
+ assertPositiveFinite('probeTimeoutMs', this.probeTimeoutMs)
+ }
+
+ /**
+ * 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.
+ * @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.
+ */
+ confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv {
+ if (this.runnerCommand !== undefined) {
+ const argv0 = this.runnerCommand[0] as string
+ return {
+ argv: [...this.runnerCommand, ...bwrapProfileArgs(policy), '--', ...argv],
+ enforcement: 'full',
+ denialSignatures: DENIAL_SIGNATURES.runnerCommand,
+ // The configured runner's own failure dialect is unknown (as is its
+ // kernel mechanism), but the consumer never spawns the wrap directly
+ // — it re-joins it through an outer `bash -c 'exec …'` — so a
+ // missing or unexecutable runner fails with the OUTER shell's
+ // argv0-scoped shapes, and those we do know. Scoping every shape to
+ // argv0 keeps in-command errors out (a bare `exec:`/`Permission
+ // denied` prefix would claim tool output; `exec: : not
+ // found` cannot). The residual collision — a command invoking a
+ // file named exactly like the runner and hitting the same errno —
+ // is the classifier's documented conservative-inference trade.
+ runnerFailureSignatures: [
+ `exec: ${argv0}: not found`,
+ `${argv0}: No such file or directory`,
+ `${argv0}: Permission denied`,
+ ],
+ }
+ }
+ const selected = this.selectRunner(policy.mode)
+ return {
+ argv: [...this.runnerArgv(selected.runner, policy), '--', ...argv],
+ enforcement: selected.enforcement,
+ denialSignatures: DENIAL_SIGNATURES[selected.runner],
+ runnerFailureSignatures: RUNNER_FAILURE_SIGNATURES[selected.runner],
+ }
+ }
+
+ /** The selected rung's runner invocation (program + profile arguments) for one policy. */
+ private runnerArgv(runner: SelectedRunner['runner'], policy: SandboxPolicy): string[] {
+ switch (runner) {
+ case 'bwrap': return ['bwrap', ...bwrapProfileArgs(policy)]
+ case 'landlock': return [this.landlockLauncher(), ...landlockProfileArgs(policy)]
+ case 'seatbelt': return [this.seatbeltExec(), ...seatbeltProfileArgs(policy)]
+ default: return assertNever(runner)
+ }
+ }
+
+ /**
+ * Resolve which runner confines commands, once, for the provider's
+ * lifetime: this platform's chain ({@link PLATFORM_CHAINS}), its sole
+ * candidate selected directly, multiple candidates arbitrated by
+ * functional probes in chain order. Fail closed when the platform has no
+ * chain or no candidate passes — the command never runs.
+ */
+ private selectRunner(mode: ConfinedSandboxMode): SelectedRunner {
+ this.selectedRunner ??= this.chainVerdict()
+ if (this.selectedRunner === 'unavailable') throw new SandboxUnavailableError(mode)
+ return this.selectedRunner
+ }
+
+ /** Walk this platform's chain: sole candidate unprobed, several probed in order, none usable → unavailable. */
+ private chainVerdict(): SelectedRunner | 'unavailable' {
+ 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.
+ if (rest.length === 0) return { runner: first, enforcement: STATIC_ENFORCEMENT[first] }
+ for (const runner of chain) {
+ const enforcement = this.probeRunner(runner)
+ if (enforcement !== 'unusable') return { runner, enforcement }
+ }
+ return 'unavailable'
+ }
+
+ /** One rung's functional probe (each at most once, via the chain walk). */
+ private probeRunner(runner: SelectedRunner['runner']): SandboxEnforcement | 'unusable' {
+ // bwrap's mount profile and Seatbelt's deny-file-write* profile govern
+ // every promised file effect by construction, so their passing probes
+ // are always full enforcement; only the Landlock launcher's probe report
+ // distinguishes full from per-ABI-partial.
+ switch (runner) {
+ case 'bwrap': {
+ const probe = this.internals.probeBwrap ?? (() => defaultProbeBwrap(this.probeTimeoutMs))
+ return probe() ? 'full' : 'unusable'
+ }
+ case 'landlock': {
+ const probe = this.internals.probeLandlock ?? (launcher => defaultProbeLandlock(launcher, { timeoutMs: this.probeTimeoutMs }))
+ return probe(this.landlockLauncher())
+ }
+ case 'seatbelt': {
+ const probe = this.internals.probeSeatbelt ?? (exec => defaultProbeSeatbelt(exec, this.probeTimeoutMs))
+ return probe(this.seatbeltExec()) ? 'full' : 'unusable'
+ }
+ default: return assertNever(runner)
+ }
+ }
+
+ /** The Landlock launcher to probe and exec (test seam over the resolved one). */
+ private landlockLauncher(): string {
+ return this.internals.landlockLauncher ?? landlockLauncherPath()
+ }
+
+ /** The `sandbox-exec` executable to probe and exec (test seam over the system one). */
+ private seatbeltExec(): string {
+ return this.internals.seatbeltExec ?? 'sandbox-exec'
+ }
+}
+
+export default LocalSandboxProvider
diff --git a/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts
new file mode 100644
index 0000000000..da6e683da1
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts
@@ -0,0 +1,118 @@
+import { spawnSync } from 'node:child_process'
+import { existsSync, readFileSync, rmSync } from 'node:fs'
+import { mkdtemp, rm } from 'node:fs/promises'
+import { homedir, tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { Context } from 'cordis'
+import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
+import { bwrapProfileArgs, LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
+
+/**
+ * KEYLESS bwrap integration proof for the BACKEND: the REAL `bwrap` confining
+ * REAL processes through `confine()` + a direct spawn of the returned argv.
+ * Nothing is forced off: bwrap is the ladder's FIRST rung, so a passing probe
+ * selects it naturally — the wrap shape assertion pins that. Verifies the
+ * WORLD (files exist or don't) and that the kernel's denial text matches the
+ * dialect the wrap advertises; the through-`ctx.bash` consumer proof lives
+ * with `@deepseek-ai/dsh-bash-sandbox`.
+ *
+ * Self-skips wherever the functional probe fails — no `bwrap` on PATH, or a
+ * host that denies unprivileged user namespaces (the probe is the same
+ * profile the provider enforces, so skip conditions match runtime exactly).
+ *
+ * Workspaces for the workspace-write tests live under the HOME directory on
+ * purpose: bwrap's `/tmp` is an EPHEMERAL mount (the documented
+ * bwrap-profile difference — pinned by its own test below), so only a
+ * workspace OUTSIDE `/tmp` proves the workspace-root rebind itself.
+ */
+
+const probe = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
+const bwrapUsable = probe.status === 0
+
+let ctx: Context | undefined
+const tempDirs: string[] = []
+const tempFiles: string[] = []
+
+afterEach(async () => {
+ await ctx?.fiber.dispose()
+ ctx = undefined
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+ for (const file of tempFiles.splice(0)) rmSync(file, { force: true })
+})
+
+async function tempDir(base: string): Promise {
+ const dir = await mkdtemp(join(base, 'dsh-bwrap-e2e-'))
+ tempDirs.push(dir)
+ return dir
+}
+
+async function provider(): Promise {
+ ctx = new Context()
+ await ctx.plugin(LocalSandboxProvider, {})
+ return ctx.sandbox as LocalSandboxProvider
+}
+
+/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's facts. */
+function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
+ const confined = sandbox.confine(['bash', '-c', command], policy)
+ const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
+ return { result, confined }
+}
+
+describe.skipIf(!bwrapUsable)('sandbox-local: real bwrap confinement', () => {
+ it('the passing probe selects the bwrap rung naturally — first in the ladder, full enforcement, EROFS dialect', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const confined = sandbox.confine(['true'], { mode: 'read-only', workspaceRoot: workdir })
+ expect(confined.argv[0]).toBe('bwrap')
+ expect(confined.enforcement).toBe('full')
+ expect(confined.denialSignatures).toEqual(['read-only file system'])
+ })
+
+ it('read-only denies a write — the file must NOT exist, and the kernel speaks the advertised dialect', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).not.toBe(0)
+ // The wrap's denialSignatures must be what the kernel actually prints.
+ expect(result.stderr.toLowerCase()).toContain('read-only file system')
+ expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
+ })
+
+ it('read-only keeps the tree readable/executable and the fresh /dev/null writable', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).toBe(0)
+ expect(result.stdout).toBe('dev-ok\n')
+ })
+
+ it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
+ const workdir = await tempDir(homedir())
+ const outside = await tempDir(homedir())
+ const sandbox = await provider()
+
+ const inside = runConfined(sandbox, `printf bwrap-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(inside.result.status).toBe(0)
+ expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('bwrap-ok')
+
+ const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(denied.result.status).not.toBe(0)
+ expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
+ })
+
+ it('workspace-write mounts an EPHEMERAL /tmp: the write succeeds inside, the host /tmp stays untouched', async () => {
+ // The documented bwrap-profile difference: Landlock and Seatbelt grant
+ // the HOST temp areas, bwrap swaps in a fresh tmpfs that dies with the
+ // process — the strongest of the three temp semantics.
+ const workdir = await tempDir(homedir())
+ const target = `/tmp/dsh-bwrap-e2e-ephemeral-${process.pid}.txt`
+ tempFiles.push(target)
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, `printf tmp-ok > ${target} && cat ${target}`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(result.status).toBe(0)
+ expect(result.stdout).toBe('tmp-ok')
+ expect(existsSync(target)).toBe(false)
+ })
+})
diff --git a/packages/sandbox/sandbox-local/tests/landlock.e2e.ts b/packages/sandbox/sandbox-local/tests/landlock.e2e.ts
new file mode 100644
index 0000000000..104d3b318a
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tests/landlock.e2e.ts
@@ -0,0 +1,118 @@
+import { spawnSync } from 'node:child_process'
+import { existsSync, readFileSync } from 'node:fs'
+import { mkdtemp, rm } from 'node:fs/promises'
+import { homedir, tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { Context } from 'cordis'
+import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
+import { launcherPath } from 'node-addon-landlock-run'
+import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
+
+/**
+ * KEYLESS Landlock integration proof for the BACKEND: the REAL npm-distributed
+ * `landlock-run` launcher (`node-addon-landlock-run`) confining REAL processes through `confine()` + a direct
+ * spawn of the returned argv, with the bwrap rung forced off so the ladder
+ * lands on the launcher. Verifies the WORLD (files exist or don't), not the
+ * wrapper argv alone; the through-`ctx.bash` consumer proof lives with
+ * `@deepseek-ai/dsh-bash-sandbox`.
+ *
+ * Self-skips when the running kernel does not enforce Landlock (or this
+ * platform has no launcher package — the probe cannot pass then). The
+ * binary itself arrives with `pnpm install`, so absence is not a checkout
+ * state.
+ *
+ * Workspaces live under the HOME directory on purpose: `workspace-write`
+ * grants the host `/tmp` wholesale (the documented Landlock-profile
+ * difference), so only a workspace OUTSIDE `/tmp` proves the workspace-root
+ * grant itself.
+ */
+
+const probe = spawnSync(launcherPath(), ['--probe'], { timeout: 5_000, encoding: 'utf8' })
+const landlockUsable = probe.status === 0
+/** The running kernel's enforcement level, from the launcher's probe report — every wrap below must carry exactly this. */
+const enforcement = /partially enforced/.test(probe.stdout ?? '') ? 'partial' : 'full'
+
+let ctx: Context | undefined
+const tempDirs: string[] = []
+
+afterEach(async () => {
+ await ctx?.fiber.dispose()
+ ctx = undefined
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+})
+
+async function tempDir(base: string): Promise {
+ const dir = await mkdtemp(join(base, 'dsh-landlock-e2e-'))
+ tempDirs.push(dir)
+ return dir
+}
+
+async function provider(): Promise {
+ ctx = new Context()
+ await ctx.plugin(LocalSandboxProvider, {})
+ const sandbox = ctx.sandbox as LocalSandboxProvider
+ sandbox.internals = { probeBwrap: () => false }
+ return sandbox
+}
+
+/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's enforcement. */
+function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
+ const confined = sandbox.confine(['bash', '-c', command], policy)
+ const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
+ return { result, enforcement: confined.enforcement }
+}
+
+describe.skipIf(!landlockUsable)('sandbox-local: real Landlock confinement through the bundled launcher', () => {
+ it('read-only denies a write — the file must NOT exist, the wrap reports the probed enforcement', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result, enforcement: wrapped } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).not.toBe(0)
+ expect(wrapped).toBe(enforcement)
+ expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
+ })
+
+ it('read-only keeps the tree readable/executable and /dev/null writable', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).toBe(0)
+ expect(result.stdout).toBe('dev-ok\n')
+ })
+
+ it('read-only denies a write beneath the host /dev (the /dev/shm tmpfs must stay untouched)', async () => {
+ // The grant is /dev/null the FILE, not /dev the directory: /dev/shm is a
+ // world-writable host tmpfs, and a write landing there would be exactly
+ // the persistent host effect read-only promises never happen.
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const target = `/dev/shm/dsh-landlock-e2e-${process.pid}`
+ const { result } = runConfined(sandbox, `echo hi > ${target}`, { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).not.toBe(0)
+ expect(existsSync(target)).toBe(false)
+ })
+
+ it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
+ const workdir = await tempDir(homedir())
+ const outside = await tempDir(homedir())
+ const sandbox = await provider()
+
+ const inside = runConfined(sandbox, `printf landlock-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(inside.result.status).toBe(0)
+ expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('landlock-ok')
+
+ const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(denied.result.status).not.toBe(0)
+ expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
+ })
+
+ it('workspace-write grants the host /tmp (the documented Landlock-profile difference)', async () => {
+ const workdir = await tempDir(homedir())
+ const scratch = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, `printf tmp-ok > ${scratch}/scratch.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(result.status).toBe(0)
+ expect(readFileSync(join(scratch, 'scratch.txt'), 'utf8')).toBe('tmp-ok')
+ })
+})
diff --git a/packages/sandbox/sandbox-local/tests/local.spec.ts b/packages/sandbox/sandbox-local/tests/local.spec.ts
new file mode 100644
index 0000000000..d12f4970cb
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tests/local.spec.ts
@@ -0,0 +1,349 @@
+/**
+ * LocalSandboxProvider tests. No real runner is assumed to exist on the test
+ * host: `runnerCommand` injects deterministic runner argvs, and `internals`
+ * injects probe verdicts plus fake Landlock launcher / `sandbox-exec`
+ * scripts, so profile dialects, ladder selection, verdict caching,
+ * probe-report parsing, per-rung denial signatures, and fail-closed behavior
+ * are all exercised through the real `confine()` path.
+ */
+
+import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { describe, expect, it, vi } from 'vitest'
+import { Context } from 'cordis'
+import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
+import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
+import {
+ bwrapProfileArgs,
+ landlockProfileArgs,
+ LocalSandboxProvider,
+ seatbeltProfileArgs,
+} from '@deepseek-ai/dsh-sandbox-local'
+import type { Config } from '@deepseek-ai/dsh-sandbox-local'
+
+const RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
+const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
+
+async function setup(config: Config = {}, internals: LocalSandboxProvider['internals'] = {}) {
+ const ctx = new Context()
+ await ctx.plugin(LocalSandboxProvider, config)
+ const sandbox = ctx.sandbox as LocalSandboxProvider
+ sandbox.internals = internals
+ return { ctx, sandbox }
+}
+
+/** Write an executable fake `landlock-run` that answers `--probe` with `report`. */
+function fakeLauncher(report = 'landlock: fully enforced'): string {
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
+ const launcher = join(dir, 'landlock-run')
+ writeFileSync(launcher, `#!/bin/sh\nif [ "$1" = "--probe" ]; then echo "${report}"; exit 0; fi\nexit 125\n`, { mode: 0o755 })
+ return launcher
+}
+
+/** Write an executable fake `sandbox-exec` that exits `status` for any invocation. */
+function fakeSeatbeltExec(status: number): string {
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-seatbelt-'))
+ const exec = join(dir, 'sandbox-exec')
+ writeFileSync(exec, `#!/bin/sh\nexit ${status}\n`, { mode: 0o755 })
+ return exec
+}
+
+/** The seatbelt read-only profile — every seatbelt profile starts with these forms. */
+const SEATBELT_RO_PROFILE = '(version 1) (allow default) (deny file-write*) (allow file-write* (literal "/dev/null"))'
+
+describe('profile dialects', () => {
+ it('bwrap read-only: whole tree read-only with fresh /dev and /proc, no writable mounts', () => {
+ expect(bwrapProfileArgs(RO)).toEqual(['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent'])
+ })
+
+ it('bwrap workspace-write: adds an ephemeral /tmp and rebinds the workspace root', () => {
+ expect(bwrapProfileArgs(WW)).toEqual([
+ '--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent',
+ '--tmpfs', '/tmp', '--bind', '/ws', '/ws',
+ ])
+ })
+
+ it('landlock read-only: readable tree plus a writable /dev/null, nothing else', () => {
+ // /dev/null specifically, NOT /dev: a whole-/dev grant would let confined
+ // commands write real host paths beneath it (/dev/shm) under read-only.
+ expect(landlockProfileArgs(RO)).toEqual(['--ro', '/', '--rw', '/dev/null'])
+ })
+
+ it('landlock workspace-write: adds the host /tmp and the workspace root', () => {
+ expect(landlockProfileArgs(WW)).toEqual(['--ro', '/', '--rw', '/dev/null', '--rw', '/tmp', '--rw', '/ws'])
+ })
+
+ it('seatbelt read-only: allow-default with every file write denied except the /dev/null literal', () => {
+ expect(seatbeltProfileArgs(RO)).toEqual(['-p', SEATBELT_RO_PROFILE])
+ })
+
+ it('seatbelt workspace-write: one more allow for the canonicalized workspace root, /tmp, and the user temp dir', () => {
+ // `/ws` does not exist, so it is granted as spelled (the canonicalization
+ // fallback); `/tmp` and `os.tmpdir()` exist everywhere and are granted
+ // CANONICALIZED — Seatbelt matches resolved paths (`/tmp` IS
+ // `/private/tmp` on macOS), and both collapse to one grant on hosts
+ // where they resolve to the same directory.
+ const roots = [...new Set(['/ws', realpathSync('/tmp'), realpathSync(tmpdir())])]
+ const allow = `(allow file-write* ${roots.map(root => `(subpath "${root}")`).join(' ')})`
+ expect(seatbeltProfileArgs(WW)).toEqual(['-p', `${SEATBELT_RO_PROFILE} ${allow}`])
+ })
+
+ it('seatbelt workspace-write dedups a workspace root that already IS the temp dir', () => {
+ const profile = seatbeltProfileArgs({ mode: 'workspace-write', workspaceRoot: tmpdir() })[1] as string
+ const grant = `(subpath "${realpathSync(tmpdir())}")`
+ expect(profile).toContain(grant)
+ expect(profile.split(grant)).toHaveLength(2)
+ })
+})
+
+describe('runnerCommand config', () => {
+ it('a non-empty runnerCommand skips the chain: runner argv + bwrap-shaped profile + -- + caller argv, asserted full', async () => {
+ const probeBwrap = vi.fn(() => false)
+ const probeLandlock = vi.fn(() => 'unusable' as const)
+ const probeSeatbelt = vi.fn(() => false)
+ const { sandbox } = await setup({ runnerCommand: ['fake-runner', '--flag'] }, { probeBwrap, probeLandlock, probeSeatbelt })
+ const confined = sandbox.confine(['bash', '-c', 'echo hi'], WW)
+ expect(confined).toEqual({
+ argv: ['fake-runner', '--flag', ...bwrapProfileArgs(WW), '--', 'bash', '-c', 'echo hi'],
+ enforcement: 'full',
+ // An operator runner's kernel mechanism is unknown: both Linux
+ // file-denial dialects, never bare EPERM.
+ denialSignatures: ['read-only file system', 'permission denied'],
+ // The runner's own dialect is unknown, but the consumer re-joins the
+ // wrap through an outer `bash -c 'exec …'` — a missing or
+ // unexecutable runner fails with the OUTER shell's argv0-scoped
+ // shapes, and those classify as sandbox failures like any rung.
+ runnerFailureSignatures: [
+ 'exec: fake-runner: not found',
+ 'fake-runner: No such file or directory',
+ 'fake-runner: Permission denied',
+ ],
+ })
+ expect(probeBwrap).not.toHaveBeenCalled()
+ expect(probeLandlock).not.toHaveBeenCalled()
+ expect(probeSeatbelt).not.toHaveBeenCalled()
+ })
+
+ it('an EMPTY runnerCommand means unconfigured: the platform chain still gates the wrap', async () => {
+ const probeBwrap = vi.fn(() => false)
+ const { sandbox } = await setup({ runnerCommand: [] }, { platform: 'linux', probeBwrap, probeLandlock: () => 'unusable' })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(SandboxUnavailableError)
+ expect(probeBwrap).toHaveBeenCalledTimes(1)
+ })
+})
+
+describe('the platform chains', () => {
+ it('linux probes bwrap first: a passing probe wraps with the bwrap dialect at full enforcement', async () => {
+ const probeBwrap = vi.fn(() => true)
+ const probeLandlock = vi.fn(() => 'full' as const)
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock })
+ const confined = sandbox.confine(['true'], RO)
+ expect(confined).toEqual({
+ argv: ['bwrap', ...bwrapProfileArgs(RO), '--', 'true'],
+ enforcement: 'full',
+ denialSignatures: ['read-only file system'],
+ runnerFailureSignatures: ['bwrap: '],
+ })
+ expect(probeLandlock).not.toHaveBeenCalled()
+ })
+
+ it('linux falls back to the launcher when the bwrap probe fails, speaking the landlock dialect', async () => {
+ const probeBwrap = vi.fn(() => false)
+ const probeLandlock = vi.fn(() => 'full' as const)
+ const launcher = fakeLauncher()
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock, landlockLauncher: launcher })
+ const confined = sandbox.confine(['bash', '-c', 'echo hi'], WW)
+ expect(confined).toEqual({
+ argv: [launcher, ...landlockProfileArgs(WW), '--', 'bash', '-c', 'echo hi'],
+ enforcement: 'full',
+ denialSignatures: ['permission denied'],
+ runnerFailureSignatures: ['landlock-run: '],
+ })
+ expect(probeLandlock).toHaveBeenCalledWith(launcher)
+ })
+
+ it('darwin selects its sole candidate WITHOUT probing: nothing to arbitrate', async () => {
+ // The safety property moves to execution time: an unusable sandbox-exec
+ // refuses to run the command, and the wrap's runnerFailureSignatures let
+ // the consumer classify that as a sandbox failure, not a task failure.
+ const probeSeatbelt = vi.fn(() => true)
+ const { sandbox } = await setup({}, { platform: 'darwin', probeSeatbelt })
+ const confined = sandbox.confine(['bash', '-c', 'echo hi'], RO)
+ expect(confined).toEqual({
+ argv: ['sandbox-exec', ...seatbeltProfileArgs(RO), '--', 'bash', '-c', 'echo hi'],
+ enforcement: 'full',
+ denialSignatures: ['operation not permitted'],
+ runnerFailureSignatures: ['sandbox-exec: '],
+ })
+ expect(probeSeatbelt).not.toHaveBeenCalled()
+ })
+
+ it('a platform with no chain fails closed without a single probe: the command never runs', async () => {
+ const probeBwrap = vi.fn(() => true)
+ const probeLandlock = vi.fn(() => 'full' as const)
+ const probeSeatbelt = vi.fn(() => true)
+ const { sandbox } = await setup({}, { platform: 'freebsd', probeBwrap, probeLandlock, probeSeatbelt })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE }))
+ expect(probeBwrap).not.toHaveBeenCalled()
+ expect(probeLandlock).not.toHaveBeenCalled()
+ expect(probeSeatbelt).not.toHaveBeenCalled()
+ })
+
+ it('win32 is a reserved EMPTY chain: fails closed identically until a Windows runner fills it', async () => {
+ // The slot exists so Windows support is an additive fill-in (chain entry
+ // + runner union member), never a redesign — and reserving it must not
+ // weaken the fail-closed end in the meantime.
+ const { sandbox } = await setup({}, { platform: 'win32' })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
+ })
+
+ it('caches the verdict for the provider lifetime: one chain walk across wraps', async () => {
+ const probeBwrap = vi.fn(() => true)
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap })
+ sandbox.confine(['true'], RO)
+ sandbox.confine(['true'], WW)
+ expect(probeBwrap).toHaveBeenCalledTimes(1)
+ })
+
+ it('the unavailable verdict is cached too, and the error is structured', async () => {
+ const probeBwrap = vi.fn(() => false)
+ const probeLandlock = vi.fn(() => 'unusable' as const)
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE }))
+ expect(() => sandbox.confine(['true'], RO)).toThrow(SandboxUnavailableError)
+ expect(probeBwrap).toHaveBeenCalledTimes(1)
+ expect(probeLandlock).toHaveBeenCalledTimes(1)
+ })
+
+ it('a multi-rung chain probes a seatbelt rung like any other (the walk, not the platform table, decides)', async () => {
+ // The product chains reach seatbelt only as darwin's sole (unprobed)
+ // candidate; the chain seam exercises the probing path it would take in
+ // a grown chain, keeping the default seatbelt probe honest.
+ const exec = fakeSeatbeltExec(0)
+ const probeBwrap = vi.fn(() => false)
+ const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap, seatbeltExec: exec })
+ const confined = sandbox.confine(['true'], RO)
+ expect(confined.argv[0]).toBe(exec)
+ expect(confined.enforcement).toBe('full')
+ expect(probeBwrap).toHaveBeenCalledTimes(1)
+ })
+
+ it('a rogue chain entry throws via the probe walk\'s exhaustiveness guard (closed union)', async () => {
+ // Same convention as the wrap switch below: the union is closed, so a
+ // runner added later fails to compile at the probe switch instead of
+ // silently selecting without a probe. Only a cast can reach the guard.
+ const { sandbox } = await setup({}, { chain: ['chroot', 'bwrap'] as unknown as readonly ['bwrap'] })
+ expect(() => sandbox.confine(['true'], RO)).toThrow('unreachable variant')
+ })
+
+ it('a rogue cached runner tag throws via the exhaustiveness guard (closed union)', async () => {
+ // The wrap switches on the chain verdict's runner tag and ends with
+ // assertNever: a rogue tag (only reachable by a cast — the union is
+ // closed and chainVerdict writes only its own literals) must throw, so a
+ // runner added later fails to compile at the switch instead of silently
+ // wrapping with another runner's dialect.
+ const { sandbox } = await setup()
+ ;(sandbox as unknown as { selectedRunner: unknown }).selectedRunner = { runner: 'chroot', enforcement: 'full' }
+ expect(() => sandbox.confine(['true'], RO)).toThrow('unreachable variant')
+ })
+
+ it('runs the real default probes on the linux chain when none are injected (usable here or fail closed there)', async () => {
+ // Pinning the platform (not the probes) makes the REAL defaultProbeBwrap
+ // spawn run on every host: bwrap answers on a Linux box, ENOENT reads as
+ // an unusable rung anywhere else — either way the walk is genuine.
+ const { sandbox } = await setup({}, { platform: 'linux' })
+ const verdict = (() => {
+ try {
+ sandbox.confine(['true'], RO)
+ return 'usable'
+ } catch (error: unknown) {
+ if (error instanceof SandboxUnavailableError) return 'unavailable'
+ throw error
+ }
+ })()
+ expect(['usable', 'unavailable']).toContain(verdict)
+ })
+
+ it('walks the real platform chain when nothing is injected (usable here or fail closed there)', async () => {
+ const { sandbox } = await setup({}, {})
+ const verdict = (() => {
+ try {
+ sandbox.confine(['true'], RO)
+ return 'usable'
+ } catch (error: unknown) {
+ if (error instanceof SandboxUnavailableError) return 'unavailable'
+ throw error
+ }
+ })()
+ expect(['usable', 'unavailable']).toContain(verdict)
+ })
+})
+
+describe('the default landlock probe (launcher CLI contract)', () => {
+ it('parses a fully-enforced probe report as full enforcement', async () => {
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: fakeLauncher() })
+ expect(sandbox.confine(['true'], RO).enforcement).toBe('full')
+ })
+
+ it('parses a partially-enforced (older-ABI) probe report as partial enforcement', async () => {
+ const launcher = fakeLauncher('landlock: partially enforced (older ABI)')
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
+ expect(sandbox.confine(['true'], RO).enforcement).toBe('partial')
+ })
+
+ it('reads a failing launcher as unusable: the chain ends and fails closed', async () => {
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
+ const launcher = join(dir, 'landlock-run')
+ writeFileSync(launcher, '#!/bin/sh\nexit 125\n', { mode: 0o755 })
+ const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
+ })
+})
+
+describe('probeTimeoutMs config', () => {
+ it('rejects 0 at construction: Node treats a 0 spawnSync timeout as UNBOUNDED, the opposite of the field', async () => {
+ const ctx = new Context()
+ await expect(ctx.plugin(LocalSandboxProvider, { probeTimeoutMs: 0 }))
+ .rejects.toThrow(/probeTimeoutMs must be a positive finite number/)
+ })
+
+ it('bounds the default probes: a launcher slower than the configured timeout reads as unusable', async () => {
+ // The same sleeping launcher passes under the default 5000ms budget and
+ // fails under a 250ms one — the config demonstrably reaches spawnSync.
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-slow-landlock-'))
+ const launcher = join(dir, 'landlock-run')
+ writeFileSync(launcher, '#!/bin/sh\nsleep 1\necho "landlock: fully enforced"\nexit 0\n', { mode: 0o755 })
+
+ const patient = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
+ expect(patient.sandbox.confine(['true'], RO).enforcement).toBe('full')
+
+ const impatient = await setup(
+ { probeTimeoutMs: 250 },
+ { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher },
+ )
+ expect(() => impatient.sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
+ })
+})
+
+describe('the default seatbelt probe (sandbox-exec contract)', () => {
+ // The product chains reach seatbelt only unprobed (darwin's sole
+ // candidate), so the default probe's contract is pinned through the chain
+ // seam: a grown chain must probe it like any other rung.
+ it('selects the rung when the executable applies the read-only profile and exits 0', async () => {
+ const exec = fakeSeatbeltExec(0)
+ const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap: () => false, seatbeltExec: exec })
+ const confined = sandbox.confine(['true'], RO)
+ expect(confined).toEqual({
+ argv: [exec, ...seatbeltProfileArgs(RO), '--', 'true'],
+ enforcement: 'full',
+ denialSignatures: ['operation not permitted'],
+ runnerFailureSignatures: ['sandbox-exec: '],
+ })
+ })
+
+ it('reads a failing executable as unusable: the chain ends and fails closed', async () => {
+ const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap: () => false, seatbeltExec: fakeSeatbeltExec(1) })
+ expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
+ })
+})
diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts
new file mode 100644
index 0000000000..9d0e8ade4f
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts
@@ -0,0 +1,173 @@
+import { spawnSync } from 'node:child_process'
+import { accessSync, constants, existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
+import { rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+
+/**
+ * KEYLESS publish-path rehearsal for this package's own distribution: the
+ * provider must work from its PACKED tarball plus its REGISTRY launcher
+ * dependency, not the git checkout. `pnpm pack` produces the EXACT bytes
+ * `pnpm publish` would upload; this suite packs the workspace closure
+ * (`dsh-sandbox-local` + its `@deepseek-ai` peers), installs the tarballs
+ * into a throwaway consumer OUTSIDE the repo — npm resolving the
+ * `node-addon-landlock-run` dependency (and its os/cpu-selected platform
+ * package) from the public registry, the real consumer path — and drives
+ * the INSTALLED packages under plain `node`: no tsx, no tsconfig paths, no
+ * workspace resolution, so a `files`-list omission, a broken launcher
+ * dependency, or a mode-stripped binary fails here instead of at the first
+ * real install.
+ *
+ * World-proofs: the registry-installed launcher carries this host's ELF
+ * architecture and IS executable (a tarball that loses the mode bit would
+ * otherwise masquerade as a non-enforcing kernel — the fail-closed branch
+ * below must never absorb that), and the installed provider confines a real
+ * process THROUGH it (bwrap forced off) — or fails closed when the running
+ * kernel does not enforce Landlock, which is itself the installed
+ * fail-closed contract. Byte provenance of the launcher is the
+ * `node-addon-landlock-run` repository's own release-pipeline concern.
+ *
+ * Self-skips off Linux or when the built `lib/` is absent (run
+ * `pnpm run build` first — CI's landlock legs do).
+ */
+
+const packageDir = fileURLToPath(new URL('..', import.meta.url))
+const repoRoot = fileURLToPath(new URL('../../../..', import.meta.url))
+
+/** The closure the consumer needs: the package and its transitive `@deepseek-ai` peers; the launcher family arrives from the registry. */
+const WORKSPACE_CLOSURE = [
+ 'packages/sandbox/sandbox-local',
+ 'packages/sandbox/sandbox',
+ 'packages/llm/llm',
+ 'packages/util/brand',
+]
+
+/** ELF `e_machine` (offset 18, LE) for this host: x86-64 = 62, AArch64 = 183. */
+const E_MACHINE = { x64: 62, arm64: 183 }[process.arch as 'x64' | 'arm64']
+
+const packable = process.platform === 'linux'
+ && E_MACHINE !== undefined
+ && existsSync(join(packageDir, 'lib', 'index.js'))
+
+let consumerDir = ''
+let workDir = ''
+/** The consumer script's JSON verdict (see its source below). */
+let verdict: {
+ launcher: string
+ launcherExists: boolean
+ enforcing: boolean
+ wrapArgv0?: string
+ enforcement?: string
+ exitCode?: number | null
+ stderrHasDialect?: boolean
+ confineOutcome?: string
+} = { launcher: '', launcherExists: false, enforcing: false }
+
+describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-path rehearsal)', () => {
+ beforeAll(async () => {
+ const packDest = mkdtempSync(join(tmpdir(), 'dsh-pack-'))
+ consumerDir = mkdtempSync(join(tmpdir(), 'dsh-packed-consumer-'))
+ workDir = mkdtempSync(join(tmpdir(), 'dsh-packed-work-'))
+
+ // Pack each closure member with the exact bytes publish would upload.
+ const tarballs: string[] = []
+ for (const pkg of WORKSPACE_CLOSURE) {
+ const pack = spawnSync('pnpm', ['pack', '--pack-destination', packDest], {
+ cwd: join(repoRoot, pkg),
+ encoding: 'utf8',
+ timeout: 120_000,
+ })
+ expect(pack.status, `pnpm pack failed for ${pkg}:\n${pack.stdout}\n${pack.stderr}`).toBe(0)
+ const lines = pack.stdout.trim().split('\n')
+ tarballs.push(lines[lines.length - 1] as string)
+ }
+
+ // A real consumer: plain ESM project, tarballs installed by npm — the
+ // peer ranges (^0.0.1) resolve to the tarball versions, cordis pins to
+ // the peer range's rc, and `node-addon-landlock-run` (with its
+ // os/cpu-selected platform package, an OPTIONAL dependency of the entry
+ // — so no `--omit=optional` here) comes from the public registry.
+ writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
+ const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.6'], {
+ cwd: consumerDir,
+ encoding: 'utf8',
+ timeout: 300_000,
+ })
+ expect(install.status, `npm install failed:\n${install.stdout}\n${install.stderr}`).toBe(0)
+
+ // The consumer script runs under PLAIN node against the installed
+ // packages and reports a JSON verdict; every assertion happens back in
+ // the test. bwrap is forced off so the wrap must select the INSTALLED
+ // launcher; a non-enforcing kernel must surface the fail-closed error.
+ writeFileSync(join(consumerDir, 'consumer.mjs'), `
+ import { spawnSync } from 'node:child_process'
+ import { existsSync } from 'node:fs'
+ import { Context } from 'cordis'
+ import { launcherPath } from 'node-addon-landlock-run'
+ import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
+ const ctx = new Context()
+ await ctx.plugin(LocalSandboxProvider, {})
+ const sandbox = ctx.sandbox
+ sandbox.internals = { probeBwrap: () => false }
+ const launcher = launcherPath()
+ const probe = spawnSync(launcher, ['--probe'], { encoding: 'utf8', timeout: 5000 })
+ const out = { launcher, launcherExists: existsSync(launcher), enforcing: probe.status === 0 }
+ const workdir = process.argv[2]
+ if (out.enforcing) {
+ const confined = sandbox.confine(['bash', '-c', \`echo hi > \${workdir}/denied.txt\`], { mode: 'read-only', workspaceRoot: workdir })
+ out.wrapArgv0 = confined.argv[0]
+ out.enforcement = confined.enforcement
+ const run = spawnSync(confined.argv[0], confined.argv.slice(1), { encoding: 'utf8', timeout: 30000 })
+ out.exitCode = run.status
+ out.stderrHasDialect = /permission denied/i.test(run.stderr)
+ } else {
+ try {
+ sandbox.confine(['true'], { mode: 'read-only', workspaceRoot: workdir })
+ out.confineOutcome = 'wrapped'
+ } catch (error) {
+ out.confineOutcome = error?.code === 'SANDBOX_UNAVAILABLE' ? 'fail-closed' : String(error)
+ }
+ }
+ console.log(JSON.stringify(out))
+ `)
+ const consumer = spawnSync('node', ['consumer.mjs', workDir], { cwd: consumerDir, encoding: 'utf8', timeout: 60_000 })
+ expect(consumer.status, `consumer script failed:\n${consumer.stdout}\n${consumer.stderr}`).toBe(0)
+ verdict = JSON.parse(consumer.stdout.trim().split('\n').pop() as string) as typeof verdict
+ }, 480_000)
+
+ afterAll(async () => {
+ await Promise.all([consumerDir, workDir].filter(Boolean).map(dir => rm(dir, { recursive: true, force: true })))
+ })
+
+ it('installs the registry launcher for this host: present, EXECUTABLE, right ELF arch', () => {
+ const installed = join(consumerDir, 'node_modules', `node-addon-landlock-run-linux-${process.arch}`, 'bin', 'landlock-run')
+ expect(existsSync(installed), 'platform package missing from the installed tree').toBe(true)
+ // A tarball or extraction step that strips the mode bit would leave the
+ // probe failing exactly like a non-enforcing kernel — assert it apart.
+ expect(() => { accessSync(installed, constants.X_OK) }, 'installed launcher is not executable').not.toThrow()
+ expect(readFileSync(installed).readUInt16LE(18), 'ELF e_machine').toBe(E_MACHINE)
+ })
+
+ it('the installed provider resolves the launcher INSIDE the consumer node_modules platform package', () => {
+ expect(verdict.launcher)
+ .toBe(join(consumerDir, 'node_modules', `node-addon-landlock-run-linux-${process.arch}`, 'bin', 'landlock-run'))
+ })
+
+ it('confines through the installed launcher (enforcing kernel) or fails closed (non-enforcing) — never unconfined', async () => {
+ // Fail-closed is only the acceptable outcome when the installed binary
+ // IS present and executable and the kernel merely does not enforce —
+ // the first test pins that apart, so nothing hides behind this branch.
+ expect(verdict.launcherExists, 'installed launcher missing').toBe(true)
+ if (verdict.enforcing) {
+ expect(verdict.wrapArgv0).toBe(verdict.launcher)
+ expect(['full', 'partial']).toContain(verdict.enforcement)
+ expect(verdict.exitCode).not.toBe(0)
+ expect(verdict.stderrHasDialect, 'kernel denial text must match the advertised dialect').toBe(true)
+ expect(existsSync(join(workDir, 'denied.txt'))).toBe(false)
+ } else {
+ expect(verdict.confineOutcome).toBe('fail-closed')
+ }
+ })
+})
diff --git a/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts b/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts
new file mode 100644
index 0000000000..7136e4e524
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts
@@ -0,0 +1,120 @@
+import { spawnSync } from 'node:child_process'
+import { existsSync, readFileSync } from 'node:fs'
+import { mkdtemp, rm } from 'node:fs/promises'
+import { homedir, tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { Context } from 'cordis'
+import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
+import { LocalSandboxProvider, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local'
+
+/**
+ * KEYLESS Seatbelt integration proof for the BACKEND: the REAL macOS
+ * `sandbox-exec` confining REAL processes through `confine()` + a direct
+ * spawn of the returned argv, with the Linux rungs forced off so the ladder
+ * lands on Seatbelt. Verifies the WORLD (files exist or don't) and that the
+ * kernel's denial text matches the dialect the wrap advertises; the
+ * through-`ctx.bash` consumer proof lives with `@deepseek-ai/dsh-bash-sandbox`.
+ *
+ * Self-skips wherever the functional probe fails — every non-macOS host, or
+ * a macOS whose `sandbox-exec` refuses the profile.
+ *
+ * Workspaces for the workspace-write tests live under the HOME directory on
+ * purpose: `workspace-write` grants `/tmp` and the per-user temp dir
+ * wholesale (the documented Seatbelt-profile temp areas), so only a
+ * workspace OUTSIDE both proves the workspace-root grant itself.
+ */
+
+const probe = spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
+const seatbeltUsable = probe.status === 0
+
+let ctx: Context | undefined
+const tempDirs: string[] = []
+
+afterEach(async () => {
+ await ctx?.fiber.dispose()
+ ctx = undefined
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+})
+
+async function tempDir(base: string): Promise {
+ const dir = await mkdtemp(join(base, 'dsh-seatbelt-e2e-'))
+ tempDirs.push(dir)
+ return dir
+}
+
+async function provider(): Promise {
+ ctx = new Context()
+ await ctx.plugin(LocalSandboxProvider, {})
+ const sandbox = ctx.sandbox as LocalSandboxProvider
+ sandbox.internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
+ return sandbox
+}
+
+/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's facts. */
+function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
+ const confined = sandbox.confine(['bash', '-c', command], policy)
+ const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
+ return { result, confined }
+}
+
+describe.skipIf(!seatbeltUsable)('sandbox-local: real Seatbelt confinement through sandbox-exec', () => {
+ it('read-only denies a write — the file must NOT exist, and the kernel speaks the advertised dialect', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result, confined } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).not.toBe(0)
+ expect(confined.enforcement).toBe('full')
+ // The wrap's denialSignatures must be what the kernel actually prints.
+ expect(result.stderr.toLowerCase()).toContain('operation not permitted')
+ expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
+ })
+
+ it('read-only keeps the tree readable/executable and /dev/null writable', async () => {
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
+ expect(result.status).toBe(0)
+ expect(result.stdout).toBe('dev-ok\n')
+ })
+
+ it('read-only grants no temp area: a write under the user temp dir is denied too', async () => {
+ // The per-user darwin temp dir is a workspace-write grant, not a
+ // read-only one — under read-only the only write-shaped path is /dev/null.
+ const workdir = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const target = join(workdir, 'tmp-denied.txt')
+ const { result } = runConfined(sandbox, `echo hi > ${target}`, { mode: 'read-only', workspaceRoot: await tempDir(homedir()) })
+ expect(result.status).not.toBe(0)
+ expect(existsSync(target)).toBe(false)
+ })
+
+ it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
+ const workdir = await tempDir(homedir())
+ const outside = await tempDir(homedir())
+ const sandbox = await provider()
+
+ const inside = runConfined(sandbox, `printf seatbelt-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(inside.result.status).toBe(0)
+ expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('seatbelt-ok')
+
+ const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
+ expect(denied.result.status).not.toBe(0)
+ expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
+ })
+
+ it('workspace-write grants /tmp and the user temp dir (the documented Seatbelt-profile temp areas)', async () => {
+ const workdir = await tempDir(homedir())
+ const hostTmp = await tempDir('/tmp')
+ const userTmp = await tempDir(tmpdir())
+ const sandbox = await provider()
+ const { result } = runConfined(
+ sandbox,
+ `printf tmp-ok > ${hostTmp}/scratch.txt && printf user-tmp-ok > ${userTmp}/scratch.txt`,
+ { mode: 'workspace-write', workspaceRoot: workdir },
+ )
+ expect(result.status).toBe(0)
+ expect(readFileSync(join(hostTmp, 'scratch.txt'), 'utf8')).toBe('tmp-ok')
+ expect(readFileSync(join(userTmp, 'scratch.txt'), 'utf8')).toBe('user-tmp-ok')
+ })
+})
diff --git a/packages/sandbox/sandbox-local/tsconfig.json b/packages/sandbox/sandbox-local/tsconfig.json
new file mode 100644
index 0000000000..c756b6af69
--- /dev/null
+++ b/packages/sandbox/sandbox-local/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/types"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../../vendor/cosmokit"
+ },
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../../../vendor/schemastery"
+ },
+ {
+ "path": "../../llm/llm"
+ },
+ {
+ "path": "../sandbox"
+ }
+ ]
+}
diff --git a/packages/sandbox/sandbox/README.md b/packages/sandbox/sandbox/README.md
new file mode 100644
index 0000000000..c63b9da5de
--- /dev/null
+++ b/packages/sandbox/sandbox/README.md
@@ -0,0 +1,11 @@
+# @deepseek-ai/dsh-sandbox
+
+Abstract process-sandbox seam. Owns the `ctx.sandbox` service contract ([`SandboxProvider`](src/index.ts)) and the confinement vocabulary the harness shares: `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`, file effects only), `SandboxEnforcement` (`full` / `partial`, per kernel ABI), `SandboxPolicy` (per-CALL policy — mode + workspace root), and the fail-closed `SANDBOX_UNAVAILABLE` error. Interface package of the [capability-seam split](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md): depends only on cordis (+ the harness error base), never on a backend.
+
+The contract in one line: `ctx.sandbox.confine(argv, policy)` returns the argv to spawn INSTEAD of your own — wrapped so the process (and everything it spawns) runs confined — plus two facts about the selected backend: the enforcement completeness it achieves and its denial dialect (`denialSignatures`, the stderr substrings its kernel prints on a denied file effect — what stderr-inferring consumers match instead of a cross-backend union); when no backend is usable it throws rather than passing the argv through unconfined.
+
+Policy rides the call, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is just a new call with a wider policy.
+
+**Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox RFC](../../../docs/rfc/proposed/feature/2026-07-06-sandbox.md).
+
+Implementations: [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). The staged first consumer is the sandboxed bash executor (wrapping `['bash', '-c', command]`).
diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json
new file mode 100644
index 0000000000..b37ef714ea
--- /dev/null
+++ b/packages/sandbox/sandbox/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@deepseek-ai/dsh-sandbox",
+ "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/types/**/*.d.ts",
+ "lib/types/**/*.d.ts.map",
+ "src"
+ ],
+ "license": "BSD-3-Clause",
+ "peerDependencies": {
+ "@deepseek-ai/dsh-llm": "^0.0.1",
+ "cordis": "^4.0.0-rc.6"
+ },
+ "devDependencies": {
+ "@deepseek-ai/dsh-llm": "workspace:^",
+ "cordis": "^4.0.0-rc.6"
+ }
+}
diff --git a/packages/sandbox/sandbox/src/index.ts b/packages/sandbox/sandbox/src/index.ts
new file mode 100644
index 0000000000..43ee8f3d16
--- /dev/null
+++ b/packages/sandbox/sandbox/src/index.ts
@@ -0,0 +1,199 @@
+/**
+ * The process-sandbox seam (`ctx.sandbox`): an abstract service defining WHAT
+ * platform confinement does — wrap a subprocess argv so it executes under a
+ * file-effect policy — without saying HOW. Implementations subclass
+ * {@link SandboxProvider} and register as the `sandbox` service;
+ * `@deepseek-ai/dsh-sandbox-local` (per-platform chains: Linux `bwrap` then the
+ * npm-distributed `landlock-run` launcher, macOS `sandbox-exec`/Seatbelt) is
+ * the first.
+ * Consumers hand over the exact argv they are about to spawn
+ * (the staged bash executor wraps `['bash', '-c', command]`; a
+ * subagent backend wraps its child-agent argv) and spawn the returned argv
+ * instead.
+ *
+ * The seam confines SAME-WORLD subprocesses only: a backend shares the
+ * host's filesystem and kernel, and the policy's `workspaceRoot` names a
+ * real host path. Containers, microVMs, and remote executors are NOT
+ * backends of this seam — they are sibling implementations of whole
+ * capability seams (`ctx.bash`, `ctx.fs`), deployed as environment-coherent
+ * groups; the boundary is recorded in
+ * docs/rfc/proposed/feature/2026-07-06-sandbox.md.
+ *
+ * @module @deepseek-ai/dsh-sandbox
+ */
+
+import { Context, Service } from 'cordis'
+import { HarnessError } from '@deepseek-ai/dsh-llm'
+
+/**
+ * File-effect policy a sandbox backend enforces on confined processes.
+ *
+ * - `read-only` — the process cannot write the filesystem anywhere; a
+ * write-shaped `/dev/null` sink stays available so `>/dev/null` redirects
+ * keep working (HOW is the backend's choice: bwrap mounts a fresh `/dev`,
+ * the Landlock launcher and Seatbelt grant the single `/dev/null` node).
+ * - `workspace-write` — writes are allowed only under the policy's
+ * workspace root and `/tmp`; everything else stays read-only. Which `/tmp`
+ * is backend-specific — an ephemeral mount under bwrap, the HOST `/tmp`
+ * under the Landlock launcher, the host `/private/tmp` plus the per-user
+ * darwin temp dir under Seatbelt: the seam promises the write boundary,
+ * not the mount's nature.
+ * - `danger-full-access` — no confinement; a consumer configured with it
+ * spawns its argv unwrapped and never calls the provider.
+ *
+ * The mode governs FILE effects only: network and process visibility are not
+ * restricted (a backend that cannot honestly enforce them must not pretend
+ * to). How completely the file effects themselves are enforced is likewise a
+ * reported fact, not an assumption — see {@link SandboxEnforcement}.
+ */
+export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
+
+/** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */
+export type ConfinedSandboxMode = Exclude
+
+/**
+ * How completely the selected backend enforces a confined mode's file
+ * effects.
+ *
+ * - `full` — every file effect the mode promises to block is governed: the
+ * `bwrap` mount profile, a Landlock kernel enforcing the launcher's whole
+ * ruleset, or an operator-configured runner (configuring one asserts full
+ * enforcement along with existence).
+ * - `partial` — the backend is active but the kernel governs only the subset
+ * of accesses its ABI knows (an older Landlock ABI: path-based truncate is
+ * ungoverned before ABI v3), so a file effect the mode promises to block
+ * may still land. A caller that needs the mode's promise to be absolute
+ * must treat `partial` as outside that promise.
+ */
+export type SandboxEnforcement = 'full' | 'partial'
+
+/**
+ * What one confined execution is allowed to touch — carried PER CALL, not
+ * fixed on the provider: two consumers may confine under different policies
+ * at the same instant (bash under `read-only` while a confined child agent
+ * needs its state directory writable), and an approved escalated retry is a
+ * new call with a wider policy. Defaulting/resolution is the consumer's
+ * explicit step (its config owns the fallback chain); the provider treats
+ * the policy as fully specified.
+ */
+export interface SandboxPolicy {
+ /** The file-effect mode this execution runs under. */
+ mode: ConfinedSandboxMode
+ /** Absolute root directory `workspace-write` may write under. */
+ workspaceRoot: string
+}
+
+/**
+ * A {@link SandboxProvider.confine} result: the argv to spawn in place of
+ * the caller's own, plus the enforcement completeness the selected backend
+ * achieves for it.
+ */
+export interface ConfinedArgv {
+ /** The wrapped argv (runner, profile, separator, then the caller's argv). */
+ argv: string[]
+ /** How completely the selected backend enforces the policy's file effects. */
+ enforcement: SandboxEnforcement
+ /**
+ * The selected backend's denial DIALECT: the case-insensitive stderr
+ * substrings a file effect denied by THIS backend produces (EROFS text
+ * under bwrap's read-only binds, EACCES under Landlock, EPERM under
+ * Seatbelt). A consumer that infers denials from a failed run's stderr
+ * matches against exactly these rather than a cross-backend union — the
+ * union claims denials a given backend never produces.
+ */
+ denialSignatures: readonly string[]
+ /**
+ * How the RUNNER ITSELF failing identifies itself: case-insensitive stderr
+ * substrings produced when the sandbox binary is missing, refuses its
+ * profile, or fails closed before exec'ing the command (`bwrap: `,
+ * `landlock-run: `, `sandbox-exec: ` — each covers both the runner's own
+ * error prefix and the shell's runner-not-found message). ORTHOGONAL to
+ * {@link denialSignatures}: a denial is the confined COMMAND being blocked
+ * (the sandbox working as designed); a runner failure means the command
+ * NEVER RAN and must surface as a sandbox failure, not a task failure —
+ * consumers check these signatures FIRST (a runner's own error text may
+ * contain denial words, e.g. an unopenable grant root reporting
+ * `Permission denied`).
+ */
+ runnerFailureSignatures: readonly string[]
+}
+
+/**
+ * Error `code` carried by the infrastructure error a provider throws when a
+ * confined policy is requested but no backend is available or usable on this
+ * host: confinement FAILS CLOSED (refuses to run) rather than silently
+ * executing unconfined. Thrown as a `HarnessError`, it reaches the model
+ * through the structured `{ name, code }` error channel on `tool/result`, so
+ * callers can distinguish "the sandbox is missing" from a failing command.
+ */
+export const SANDBOX_UNAVAILABLE = 'SANDBOX_UNAVAILABLE'
+
+/**
+ * Thrown by {@link SandboxProvider.confine} when a confined policy is
+ * requested but no backend is usable on this host: confinement fails closed.
+ * Carries the {@link SANDBOX_UNAVAILABLE} code through the structured
+ * `{ name, code }` error channel.
+ */
+export class SandboxUnavailableError extends HarnessError {
+ constructor(mode: ConfinedSandboxMode, detail?: string) {
+ super(
+ `sandbox mode "${mode}" is requested but no sandbox backend is usable on this host; `
+ + 'refusing to run the command unconfined. Install bubblewrap or run a Landlock-enforcing '
+ + 'kernel (Linux), ensure sandbox-exec is usable (macOS) — Windows has no confinement '
+ + 'backend yet — or switch the consumer to danger-full-access.'
+ + (detail === undefined ? '' : ` Runner failure: ${detail}`),
+ SANDBOX_UNAVAILABLE,
+ )
+ this.name = 'SandboxUnavailableError'
+ }
+}
+
+declare module 'cordis' {
+ interface Context {
+ sandbox: SandboxProvider
+ }
+}
+
+/**
+ * Abstract process-sandbox service. Subclass, implement {@link confine}, and
+ * load the subclass as a plugin — it registers as `ctx.sandbox` (one
+ * implementation per context; loading a second throws, cordis' standard
+ * duplicate-service behavior).
+ *
+ * Semantics every implementation must honor:
+ * - {@link confine} either returns an argv whose runner ENFORCES the policy
+ * or fails closed — at `confine` time with {@link SandboxUnavailableError}
+ * (no backend for this host), or at EXECUTION time by the runner itself
+ * refusing to run the command (exiting without exec'ing it, identified by
+ * {@link ConfinedArgv.runnerFailureSignatures}). A silent unconfined
+ * passthrough is never a legal outcome on either path.
+ * - Probing exists to ARBITRATE between multiple candidate backends and may
+ * be skipped when a platform has exactly one: the sole candidate is
+ * selected directly and the runner's exec-time fail-closed refusal carries
+ * the safety property. When probing does run, it is functional (actually
+ * enforcing a profile, not a version check), at most once per provider
+ * lifetime; `confine` itself spawns nothing beyond that one-time probing.
+ * - The returned {@link ConfinedArgv.enforcement} states the backend's
+ * actual completeness for THIS host; `partial` is reported, never silently
+ * upgraded to `full`.
+ */
+export abstract class SandboxProvider extends Service {
+ constructor(ctx: Context) {
+ super(ctx, 'sandbox')
+ }
+
+ /**
+ * Wrap `argv` so it executes confined under `policy` on this host; the
+ * caller spawns the returned argv in place of its own.
+ * @param argv - the exact argv the caller is about to spawn (program plus
+ * arguments), NOT a shell string — a shell-shaped consumer passes
+ * `['bash', '-c', command]`.
+ * @param policy - the file-effect policy this execution runs under,
+ * carried per call (see {@link SandboxPolicy}).
+ * @returns the argv to spawn instead, plus the enforcement completeness
+ * the selected backend achieves for it.
+ */
+ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
+}
+
+export default SandboxProvider
diff --git a/packages/sandbox/sandbox/tests/vocabulary.spec.ts b/packages/sandbox/sandbox/tests/vocabulary.spec.ts
new file mode 100644
index 0000000000..caba42b2a7
--- /dev/null
+++ b/packages/sandbox/sandbox/tests/vocabulary.spec.ts
@@ -0,0 +1,35 @@
+/**
+ * Vocabulary-contract tests for the sandbox seam: the fail-closed error's
+ * structured identity is what tool results and consumers key on, so its
+ * shape is pinned here, next to the vocabulary that owns it. Provider
+ * behavior is each implementation's suite (`dsh-sandbox-local`); consumer
+ * behavior is each consumer's (`dsh-bash-sandbox`).
+ */
+
+import { describe, expect, it } from 'vitest'
+import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
+
+describe('SandboxUnavailableError', () => {
+ it('carries the structured { name, code } identity consumers key on', () => {
+ const error = new SandboxUnavailableError('read-only')
+ expect(error.name).toBe('SandboxUnavailableError')
+ expect(error.code).toBe(SANDBOX_UNAVAILABLE)
+ expect(error).toBeInstanceOf(Error)
+ })
+
+ it('names the refused mode and the operator escape hatches in its message', () => {
+ const error = new SandboxUnavailableError('workspace-write')
+ expect(error.message).toContain('"workspace-write"')
+ expect(error.message).toContain('danger-full-access')
+ expect(error.message).not.toContain('Runner failure')
+ })
+
+ it('carries the runner detail when the failure is discovered at execution time', () => {
+ // The late twin of the confine-time throw: an unprobed sole candidate
+ // that fails closed at exec surfaces the SAME error, with the runner's
+ // own first stderr line as the cause.
+ const error = new SandboxUnavailableError('read-only', 'landlock-run: landlock is not enforced by this kernel')
+ expect(error.code).toBe(SANDBOX_UNAVAILABLE)
+ expect(error.message).toContain('Runner failure: landlock-run: landlock is not enforced by this kernel')
+ })
+})
diff --git a/packages/sandbox/sandbox/tsconfig.json b/packages/sandbox/sandbox/tsconfig.json
new file mode 100644
index 0000000000..9f687793d7
--- /dev/null
+++ b/packages/sandbox/sandbox/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/types"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../../vendor/cosmokit"
+ },
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../../llm/llm"
+ }
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bef41d9bdf..27302831d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -626,6 +626,34 @@ importers:
specifier: ^4.0.0-rc.6
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
+ packages/sandbox/sandbox:
+ devDependencies:
+ '@deepseek-ai/dsh-llm':
+ specifier: workspace:^
+ version: link:../../llm/llm
+ cordis:
+ specifier: ^4.0.0-rc.6
+ version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
+
+ packages/sandbox/sandbox-local:
+ dependencies:
+ node-addon-landlock-run:
+ specifier: 0.0.0-test.0
+ version: 0.0.0-test.0
+ schemastery:
+ specifier: ^3.18.0
+ version: 3.18.0
+ devDependencies:
+ '@deepseek-ai/dsh-llm':
+ specifier: workspace:^
+ version: link:../../llm/llm
+ '@deepseek-ai/dsh-sandbox':
+ specifier: workspace:^
+ version: link:../sandbox
+ cordis:
+ specifier: ^4.0.0-rc.6
+ version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
+
packages/session-persistence/session-persistence:
devDependencies:
'@deepseek-ai/dsh-session':
@@ -3780,6 +3808,22 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ node-addon-landlock-run-linux-arm64@0.0.0-test.0:
+ resolution: {integrity: sha512-oJsXcC33qKl9mWYx0n9YPJ2pUAoY39PoIX0Gx4lDrSCTEvENFrEaODAsQYNY+eEGpn9YMN7E+FOftvea3/1FqQ==}
+ engines: {node: '>=20'}
+ cpu: [arm64]
+ os: [linux]
+
+ node-addon-landlock-run-linux-x64@0.0.0-test.0:
+ resolution: {integrity: sha512-eXvdfnH/UV55MTZzroKvM3CD68SP5OlCsuth908YOcJOnn0LPD5KJjmBz6ToDlBYjF52NNK62+g7TvmUWjbKWQ==}
+ engines: {node: '>=20'}
+ cpu: [x64]
+ os: [linux]
+
+ node-addon-landlock-run@0.0.0-test.0:
+ resolution: {integrity: sha512-c5qopltRonjW6+VinXYMp4FVi9Sxf8eQEb/9G82EksQQ+JC4CDprv+ko5URWmtyZ3wD4GFdeRTyw1AG5yCwJhQ==}
+ engines: {node: '>=20'}
+
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@@ -6786,6 +6830,17 @@ snapshots:
natural-compare@1.4.0: {}
+ node-addon-landlock-run-linux-arm64@0.0.0-test.0:
+ optional: true
+
+ node-addon-landlock-run-linux-x64@0.0.0-test.0:
+ optional: true
+
+ node-addon-landlock-run@0.0.0-test.0:
+ optionalDependencies:
+ node-addon-landlock-run-linux-arm64: 0.0.0-test.0
+ node-addon-landlock-run-linux-x64: 0.0.0-test.0
+
node-domexception@1.0.0: {}
node-fetch@3.3.2:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index b2b731fc58..6407a99f52 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -19,3 +19,12 @@ allowBuilds:
# need, so we deny them — install still succeeds.
'@google/genai': false
protobufjs: false
+
+# The Landlock launcher family is our own sibling-repo release, consumed
+# fresh (hours old at each coordinated bump) — the release-age quarantine
+# would block every such bump, so the family is exempted BY NAME, not by
+# pinned version.
+minimumReleaseAgeExclude:
+ - node-addon-landlock-run
+ - node-addon-landlock-run-linux-arm64
+ - node-addon-landlock-run-linux-x64
diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json
index a5261698a2..52f563b86d 100644
--- a/scripts/doc-budgets.manifest.json
+++ b/scripts/doc-budgets.manifest.json
@@ -7,5 +7,5 @@
"docs/testing.md": 800,
"examples/AGENTS.md": 653,
"packages/AGENTS.md": 450,
- "packages/README.md": 660
+ "packages/README.md": 710
}
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index be6d8b7121..dd6c6fb67d 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -70,6 +70,7 @@ const GROUP_ORDER = [
'llm',
'core',
'bash',
+ 'sandbox',
'fs',
'compact',
'subagent',
@@ -159,6 +160,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local.',
},
+ {
+ key: 'sandbox',
+ pkg: 'sandbox',
+ title: 'Process-sandbox seam',
+ mode: 'seam',
+ implementations: ['sandbox-local'],
+ consumers: [],
+ note: 'Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement.',
+ },
{
key: 'approval',
pkg: 'approval',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index b6d0281f81..80becb532c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -42,6 +42,7 @@
"@deepseek-ai/dsh-*": [
"./packages/approval/*/src",
"./packages/core/*/src",
+ "./packages/sandbox/*/src",
"./packages/llm/*/src",
"./packages/bash/*/src",
"./packages/code-runtime/*/src",
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 2033218d01..c6c814c221 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -19,6 +19,8 @@
{ "path": "./packages/session-persistence/session-persistence-sqlite" },
{ "path": "./packages/core/system-prompt" },
{ "path": "./packages/approval/approval" },
+ { "path": "./packages/sandbox/sandbox" },
+ { "path": "./packages/sandbox/sandbox-local" },
{ "path": "./packages/core/agent" },
{ "path": "./packages/ui/user-interaction" },
{ "path": "./packages/core/tools" },
diff --git a/tsconfig.json b/tsconfig.json
index af8badd19f..cd458490f7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,6 +30,8 @@
{ "path": "./packages/session-persistence/session-persistence-sqlite" },
{ "path": "./packages/core/system-prompt" },
{ "path": "./packages/approval/approval" },
+ { "path": "./packages/sandbox/sandbox" },
+ { "path": "./packages/sandbox/sandbox-local" },
{ "path": "./packages/core/agent" },
{ "path": "./packages/ui/user-interaction" },
{ "path": "./packages/core/tools" },
diff --git a/tsdown.config.ts b/tsdown.config.ts
index 7b172180d1..9479c5c91d 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -12,7 +12,8 @@ import { defineConfig } from 'tsdown'
export default defineConfig({
// Explicit globs: `workspace: true` would also discover examples (any
// package.json), but only vendor and the packages hierarchy are pnpm
- // workspaces.
+ // workspaces. The Landlock launcher platform packages ship a prebuilt
+ // native binary and no JavaScript — nothing to bundle.
workspace: ['vendor/*', 'packages/*/*'],
entry: ['lib/types/index.js'],
outDir: 'lib',