feat(sandbox): the confinement seam and the per-platform native runner chains

ctx.sandbox (dsh-sandbox): confine(argv, policy) returns the argv to spawn
instead — wrapped so the process and its children run confined — plus the
enforcement completeness and the backend denial/runner-failure dialects;
no usable backend throws the fail-closed SANDBOX_UNAVAILABLE. Policy rides
per call. dsh-sandbox-local selects by platform and caches the verdict:
multi-candidate chains probe FUNCTIONALLY in preference order (Linux:
bwrap → the registry-installed node-addon-landlock-run launcher), a sole
candidate is selected unprobed (darwin: sandbox-exec/Seatbelt) and fails
closed at execution via runnerFailureSignatures; win32 is a reserved empty
chain. Profile parity is honest per backend (documented temp-area and ABI
differences; enforcement full|partial is a structured result fact).

CI: the sandbox-e2e matrix proves real-kernel confinement per rung (bwrap,
Landlock per architecture through the registry-installed launcher,
Seatbelt), failing on a silent all-skip; the packed-install rehearsal
installs the launcher family from the registry and asserts the binary
executable apart from kernel enforcement.
This commit is contained in:
kingwl
2026-07-10 15:43:02 +08:00
parent 80d8726601
commit 7b8c3a9b40
30 changed files with 2007 additions and 3 deletions
+124
View File
@@ -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\)'
+6
View File
@@ -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<br/>Process-sandbox seam"]
pkg_sandbox_local["sandbox-local"]
pkg_approval["approval"]
svc_approval["ctx.approval<br/>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. |
+37
View File
@@ -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))
+16
View File
@@ -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).
+9
View File
@@ -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) |
+5
View File
@@ -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"]
+2 -1
View File
@@ -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/<group>/<pkg>/`. The group directory is a pure container (no `package.json` of its own); the package name stays `@deepseek-ai/dsh-<pkg>` 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/<group>/<pkg>/`. The group directory is a pure container (no `package.json`); the package name stays `@deepseek-ai/dsh-<pkg>` 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/<group>/<pkg>/`. 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 |
+12
View File
@@ -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).
+18
View File
@@ -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: <argv0>: not found`, `<argv0>: No such file or directory`, `<argv0>: 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.
@@ -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"
}
}
+455
View File
@@ -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<string, readonly SelectedRunner['runner'][]> = {
linux: ['bwrap', 'landlock'],
darwin: ['seatbelt'],
// Reserved slot, deliberately empty: Windows support fills it with a
// confinement runner (AppContainer / restricted-token family, shipped from
// its own repository on the landlock-run template) plus a
// SelectedRunner['runner'] union member — the switches' assertNever guards
// then walk the implementer to every site. An empty chain fails closed at
// confine(), identical to an unlisted platform: reserving the slot never
// weakens the fail-closed end.
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<SelectedRunner['runner'], SandboxEnforcement> = {
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<SelectedRunner['runner'] | 'runnerCommand', readonly string[]>
/**
* How each runner's OWN failure identifies itself on stderr (the seam's
* `ConfinedArgv.runnerFailureSignatures`): every runner prefixes its error
* lines with its program name, and the shell's runner-not-found message
* carries the same `name: ` shape (`bash: bwrap: command not found`,
* `bash: …/bin/landlock-run: No such file or directory`) — so one substring
* per runner covers both "runner broke" and "runner missing". Consumers
* match these BEFORE the denial dialect: a runner's error text can contain
* denial words (an unopenable grant root reports `Permission denied`), and
* a runner failure means the command never ran at all.
*/
const RUNNER_FAILURE_SIGNATURES = {
bwrap: ['bwrap: '],
landlock: [`${LAUNCHER_BIN}: `],
seatbelt: ['sandbox-exec: '],
} as const satisfies Record<SelectedRunner['runner'], readonly string[]>
/**
* 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<Config> = 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: <argv0>: 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
@@ -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<string> {
const dir = await mkdtemp(join(base, 'dsh-bwrap-e2e-'))
tempDirs.push(dir)
return dir
}
async function provider(): Promise<LocalSandboxProvider> {
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)
})
})
@@ -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<string> {
const dir = await mkdtemp(join(base, 'dsh-landlock-e2e-'))
tempDirs.push(dir)
return dir
}
async function provider(): Promise<LocalSandboxProvider> {
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')
})
})
@@ -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 }))
})
})
@@ -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')
}
})
})
@@ -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<string> {
const dir = await mkdtemp(join(base, 'dsh-seatbelt-e2e-'))
tempDirs.push(dir)
return dir
}
async function provider(): Promise<LocalSandboxProvider> {
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')
})
})
@@ -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"
}
]
}
+11
View File
@@ -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]`).
+32
View File
@@ -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"
}
}
+199
View File
@@ -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<SandboxMode, 'danger-full-access'>
/**
* 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
@@ -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')
})
})
+21
View File
@@ -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"
}
]
}
+55
View File
@@ -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:
+9
View File
@@ -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
+1 -1
View File
@@ -7,5 +7,5 @@
"docs/testing.md": 800,
"examples/AGENTS.md": 653,
"packages/AGENTS.md": 450,
"packages/README.md": 660
"packages/README.md": 710
}
+10
View File
@@ -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',
+1
View File
@@ -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",
+2
View File
@@ -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" },
+2
View File
@@ -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" },
+2 -1
View File
@@ -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',