Merge remote-tracking branch 'origin/master' into cross-family-fs-sandbox
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # examples/acp-agent/cordis.yml # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/permission-switching/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # packages/bash/bash-sandbox/src/index.ts # packages/bash/bash/src/types.ts # packages/bash/tool-bash/src/index.ts # packages/fs/fs/src/index.ts # packages/sandbox/sandbox-policy/src/session-mode.ts # packages/ui/permission/src/index.ts # packages/ui/permission/tests/permission.spec.ts # scripts/doc-budgets.manifest.json
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
# @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/implemented/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.
|
||||
Local implementation of the [`dsh-sandbox`](../sandbox/) seam. It selects and caches one platform runner: Linux prefers a working `bwrap` then Landlock; macOS uses Seatbelt. Multiple candidates are probed in order, while a sole candidate is selected directly.
|
||||
|
||||
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`.
|
||||
Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries runner-failure signatures so consumers can distinguish a broken sandbox from a command failure. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection rationale and profile differences.
|
||||
|
||||
Policy is per call; the provider stores only the mechanism and cached runner verdict. Each wrap reports enforcement completeness plus backend-specific denial and runner-failure signatures. `runnerCommand` is an operator assertion of a bwrap-shaped runner and skips probes, but missing or unexecutable commands still fail closed at execution. Because its mechanism is unknown, it carries both Linux denial dialects. `probeTimeoutMs` bounds functional probes. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection and failure semantics.
|
||||
|
||||
The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes.
|
||||
|
||||
The Landlock launcher comes from the npm package family [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) — an entry package (this package's one runtime dependency) plus per-platform binary packages selected by npm's `os`/`cpu` fields, built and released from [its own repository](https://github.com/deepseek-harness/node-addon-landlock-run). The entry package owns the launcher's CLI contract: `launcherPath()` resolution (a host with no platform package yields a never-existing path whose probe fails exactly like an unenforcing kernel), the functional `probe()`, and `grantArgs()` flag spelling — versioned together with the binary, so probe-report parsing can never drift against it. This provider keeps only the policy side: the mode → grants mapping (`landlockProfileArgs`) and the ladder. The consumer path is rehearsed by `tests/packed-install.e2e.ts`: pack THIS package's closure, install into a throwaway consumer with the launcher family coming from the registry, assert the installed binary executable (a stripped mode bit must not masquerade as a non-enforcing kernel), and confine through it under plain `node`.
|
||||
[`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) supplies the platform launcher, functional probe, and CLI argument vocabulary. This provider owns only mode-to-grant mapping and runner selection. Keeping path resolution and probe parsing with the versioned binary prevents contract drift.
|
||||
|
||||
Every rung has its keyless world-proof (`tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, `tests/seatbelt.e2e.ts`), each self-skipping where its runner is absent; CI's `sandbox-e2e` matrix runs all of them against real kernels (bwrap plus one Landlock leg per architecture on Linux, Seatbelt on macOS) and fails on a silent all-skip.
|
||||
Each rung has a self-skipping keyless world-effect test; CI runs platform legs against real kernels and rejects a silent all-skip. The packed-install test exercises the registry launcher and executable mode through a plain-Node consumer.
|
||||
|
||||
```yaml
|
||||
- id: sandbox
|
||||
@@ -16,3 +18,15 @@ Every rung has its keyless world-proof (`tests/bwrap.e2e.ts`, `tests/landlock.e2
|
||||
```
|
||||
|
||||
Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/); see [the acp-agent example](../../../examples/acp-agent/) for the runnable default composition.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md) and [`dsh-tool-bash`](../../bash/tool-bash/README.md), which render this provider's enforcement and denial facts while the [`dsh-sandbox`](../sandbox/README.md) seam owns the `SANDBOX_UNAVAILABLE` text and runner selection and profiles stay outside context.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Windows has no runner** — `win32` fails closed with `SANDBOX_UNAVAILABLE`; an AppContainer-family backend is deferred.
|
||||
- **Landlock may be partial** — older supported kernel ABIs confine only the access classes they expose, reported as `enforcement: 'partial'` rather than overstated as full.
|
||||
- **Seatbelt depends on deprecated `sandbox-exec`** — macOS still ships it, but this provider cannot replace or probe that private policy engine if Apple removes it.
|
||||
- **Runner selection is cached for the provider lifetime** — installing, removing, or repairing a runner requires reloading the plugin before selection changes.
|
||||
- **`runnerCommand` is an operator assertion** — a configured custom runner skips functional probes and is assumed to implement the bwrap-shaped profile honestly.
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
/**
|
||||
* `LocalSandboxProvider`: the local implementation of the
|
||||
* `@deepseek-ai/dsh-sandbox` seam. Wraps a caller's argv in a platform
|
||||
* confinement runner selected BY PLATFORM: each platform names its runner
|
||||
* chain ({@link PLATFORM_CHAINS}), a chain of one is selected directly (no
|
||||
* probe — there is nothing to arbitrate), and a chain of several is probed
|
||||
* FUNCTIONALLY in preference order (build and enforce a real profile once,
|
||||
* not `--version`), the verdict cached for the provider's lifetime. Linux:
|
||||
* `bwrap`, else the `landlock-run` Landlock launcher (kernel confinement
|
||||
* that needs no userns/mount privileges; distributed as the npm package
|
||||
* family `node-addon-landlock-run` — the decision recorded in
|
||||
* docs/rfc/implemented/feature/2026-07-06-sandbox.md); darwin: macOS
|
||||
* `sandbox-exec` speaking a Seatbelt (SBPL) profile, unprobed.
|
||||
* When the platform has no chain or no candidate passes,
|
||||
* {@link LocalSandboxProvider.confine} FAILS CLOSED with the seam's
|
||||
* structured `SANDBOX_UNAVAILABLE` error instead of passing the argv
|
||||
* through unconfined; an unusable runner selected WITHOUT a probe fails
|
||||
* closed at execution time instead (it refuses to run the command), which
|
||||
* the wrap's `runnerFailureSignatures` let consumers classify as a sandbox
|
||||
* failure rather than a task failure.
|
||||
*
|
||||
* Local sandbox backend. It selects the platform runner chain (Linux bwrap then
|
||||
* Landlock; macOS Seatbelt), functionally probes competing candidates once, and
|
||||
* reports each wrap's enforcement and stderr dialects. Missing or unusable
|
||||
* confinement fails closed rather than returning the original argv.
|
||||
* @module @deepseek-ai/dsh-sandbox-local
|
||||
*/
|
||||
|
||||
@@ -35,20 +19,10 @@ import type { ConfinedArgv, ConfinedSandboxMode, SandboxEnforcement, SandboxPoli
|
||||
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Override the sandbox runner argv (the bwrap-shaped profile arguments are
|
||||
* appended). A NON-EMPTY argv is the operator's assertion that this runner
|
||||
* exists and FULLY enforces the profile (confinement reports
|
||||
* `enforcement: 'full'`, and — the runner's kernel mechanism being unknown
|
||||
* — carries both Linux file-denial dialects as its denial signatures) —
|
||||
* the runner chain and its probes are skipped,
|
||||
* and a broken runner fails loudly at execution time. The operator also
|
||||
* supplies {@link runnerFailureSignatures}, which distinguish the runner
|
||||
* refusing its profile from the wrapped command failing normally.
|
||||
* Absent (or empty — the schema normalizes an omitted array to `[]`): the
|
||||
* built-in platform chains — Linux `bwrap` then the Landlock launcher
|
||||
* (probed in that order), darwin `sandbox-exec` (the sole candidate,
|
||||
* selected without a probe). Used for custom/alternative runners and
|
||||
* for deterministic fake runners in keyless test tiers.
|
||||
* Override the runner argv; bwrap-shaped profile arguments are appended. A
|
||||
* non-empty override asserts full enforcement and skips built-in selection and
|
||||
* probing; a broken runner then fails at execution and must be identifiable by
|
||||
* {@link runnerFailureSignatures}.
|
||||
*/
|
||||
runnerCommand?: string[]
|
||||
/**
|
||||
@@ -60,28 +34,15 @@ export interface Config {
|
||||
* own failure dialect.
|
||||
*/
|
||||
runnerFailureSignatures?: string[]
|
||||
/**
|
||||
* Per-probe timeout in milliseconds for the chain's functional probes
|
||||
* (default: 5000; must be a positive finite number — Node treats a 0
|
||||
* `spawnSync` timeout as UNBOUNDED, so 0 is rejected at construction). A
|
||||
* probe that exceeds it reads as an unusable rung, so a
|
||||
* host slow enough to trip the default — cold NFS mounts, heavily loaded
|
||||
* CI — would otherwise be misclassified `SANDBOX_UNAVAILABLE` with no
|
||||
* config escape. Bounds ONE probe, and the chain walk runs each at most once
|
||||
* per provider lifetime.
|
||||
*/
|
||||
/** Positive timeout for each functional probe; zero would mean unbounded to Node. */
|
||||
probeTimeoutMs?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Build a bwrap profile: the host is read-only with fresh `/dev` and `/proc`;
|
||||
* workspace-write overlays writable temp and workspace mounts. PID and network
|
||||
* isolation are intentionally outside the file-effect policy.
|
||||
*
|
||||
* @param policy - the file-effect policy to express as bwrap arguments.
|
||||
* @returns the bwrap profile arguments (before the trailing `--` + argv).
|
||||
*/
|
||||
@@ -95,19 +56,10 @@ export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Build Landlock grants for the same file policy without synthetic mounts.
|
||||
* Read-only grants only `/dev/null` for writes; workspace-write also grants the
|
||||
* host temp root and workspace.
|
||||
*
|
||||
* @param policy - the file-effect policy to express as launcher grants.
|
||||
* @returns the launcher grant arguments (before `--` + argv).
|
||||
*/
|
||||
@@ -130,10 +82,7 @@ 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.
|
||||
// An unresolved grant matches nothing until the named path exists; keep its spelling.
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -144,20 +93,10 @@ function sbplString(path: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Build a Seatbelt profile that denies file writes then allows `/dev/null` and,
|
||||
* for workspace-write, the canonical workspace, host temp, and per-user macOS
|
||||
* temp roots. Network and process visibility remain unrestricted.
|
||||
*
|
||||
* @param policy - the file-effect policy to express as an SBPL profile.
|
||||
* @returns the `sandbox-exec` arguments (`-p` + profile, before `--` + argv).
|
||||
*/
|
||||
@@ -170,17 +109,7 @@ export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
|
||||
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.
|
||||
*/
|
||||
/** Probe whether `bwrap` can create the profile; the provider caches the bounded result. */
|
||||
function defaultProbeBwrap(timeoutMs: number): boolean {
|
||||
const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {
|
||||
timeout: timeoutMs,
|
||||
@@ -239,13 +168,10 @@ type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt'; enforcement:
|
||||
const PLATFORM_CHAINS: Record<string, readonly SelectedRunner['runner'][]> = {
|
||||
linux: ['bwrap', 'landlock'],
|
||||
darwin: ['seatbelt'],
|
||||
// Reserved slot, deliberately empty: Windows support fills it with a
|
||||
// confinement runner (AppContainer / restricted-token family, shipped from
|
||||
// its own repository on the landlock-run template) plus a
|
||||
// SelectedRunner['runner'] union member — the switches' assertNever guards
|
||||
// then walk the implementer to every site. An empty chain fails closed at
|
||||
// confine(), identical to an unlisted platform: reserving the slot never
|
||||
// weakens the fail-closed end.
|
||||
// Reserved slot, deliberately empty: Windows support fills it with a confinement runner
|
||||
// (AppContainer / restricted-token family, shipped from its own repository on the
|
||||
// landlock-run template) plus a SelectedRunner['runner'] union member — the switches'
|
||||
// assertNever guards then walk the implementer to every site.
|
||||
win32: [],
|
||||
}
|
||||
|
||||
@@ -276,16 +202,9 @@ function assertPositiveFinite(name: string, value: number): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The denial dialect each runner's kernel speaks — the case-insensitive
|
||||
* stderr substrings a denied file effect produces under it, carried on every
|
||||
* wrap (the seam's `ConfinedArgv.denialSignatures`). Kernel facts, not
|
||||
* tunables: bwrap denies through its read-only bind mounts (EROFS), Landlock
|
||||
* refuses with EACCES, Seatbelt with EPERM — whose text is also what
|
||||
* non-file EPERM boundaries print, the residual imprecision the consumer's
|
||||
* conservative classifier documents. An operator-configured `runnerCommand`
|
||||
* has an unknown kernel mechanism, so its wraps carry both Linux file-denial
|
||||
* dialects; bare EPERM stays excluded there (it names non-file boundaries
|
||||
* the mode vocabulary does not govern).
|
||||
* The denial dialect each runner's kernel speaks — the case-insensitive stderr substrings a
|
||||
* denied file effect produces under it, carried on every wrap (the seam's
|
||||
* `ConfinedArgv.denialSignatures`).
|
||||
*/
|
||||
const DENIAL_SIGNATURES = {
|
||||
bwrap: ['read-only file system'],
|
||||
@@ -295,15 +214,9 @@ const DENIAL_SIGNATURES = {
|
||||
} as const satisfies Record<SelectedRunner['runner'] | 'runnerCommand', readonly string[]>
|
||||
|
||||
/**
|
||||
* How each runner's OWN failure identifies itself on stderr (the seam's
|
||||
* `ConfinedArgv.runnerFailureSignatures`): every runner prefixes its error
|
||||
* lines with its program name, and the shell's runner-not-found message
|
||||
* carries the same `name: ` shape (`bash: bwrap: command not found`,
|
||||
* `bash: …/bin/landlock-run: No such file or directory`) — so one substring
|
||||
* per runner covers both "runner broke" and "runner missing". Consumers
|
||||
* match these BEFORE the denial dialect: a runner's error text can contain
|
||||
* denial words (an unopenable grant root reports `Permission denied`), and
|
||||
* a runner failure means the command never ran at all.
|
||||
* Runner-owned stderr prefixes cover both internal refusal and shell-level
|
||||
* not-found errors. Consumers match these before denial text because the
|
||||
* command never ran on this path.
|
||||
*/
|
||||
const RUNNER_FAILURE_SIGNATURES = {
|
||||
bwrap: ['bwrap: '],
|
||||
@@ -356,17 +269,15 @@ export class LocalSandboxProvider extends SandboxProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap `argv` in the selected runner's invocation for `policy` — the
|
||||
* configured `runnerCommand` when present (the operator's assertion, no
|
||||
* probe), else the platform chain's runner speaking its own profile
|
||||
* dialect. Every wrap carries the runner's enforcement completeness, its
|
||||
* denial dialect, and its runner-failure signatures.
|
||||
* Wrap `argv` in the selected runner's invocation for `policy` — the configured
|
||||
* `runnerCommand` when present (the operator's assertion, no probe), else the platform
|
||||
* chain's runner speaking its own profile dialect.
|
||||
*
|
||||
* @param argv - the exact argv the caller is about to spawn.
|
||||
* @param policy - the file-effect policy this execution runs under.
|
||||
* @returns the wrapped argv plus the selected backend's enforcement
|
||||
* completeness, denial signatures, and runner-failure signatures;
|
||||
* throws the fail-closed `SANDBOX_UNAVAILABLE` error when the platform
|
||||
* has no usable runner.
|
||||
* @returns the wrapped argv plus the selected backend's enforcement completeness, denial
|
||||
* signatures, and runner-failure signatures; throws the fail-closed
|
||||
* `SANDBOX_UNAVAILABLE` error when the platform has no usable runner.
|
||||
*/
|
||||
confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv {
|
||||
if (this.runnerCommand !== undefined) {
|
||||
@@ -375,14 +286,9 @@ export class LocalSandboxProvider extends SandboxProvider {
|
||||
argv: [...this.runnerCommand, ...bwrapProfileArgs(policy), '--', ...argv],
|
||||
enforcement: 'full',
|
||||
denialSignatures: DENIAL_SIGNATURES.runnerCommand,
|
||||
// The operator names the configured runner's OWN pre-exec refusal
|
||||
// dialect; the consumer additionally re-joins the wrap through an
|
||||
// outer `bash -c 'exec …'`, so we can add the missing/unexecutable
|
||||
// outer-shell shapes ourselves. Scoping every automatic shape to
|
||||
// argv0 keeps in-command errors out (a bare `exec:`/`Permission
|
||||
// denied` prefix would claim tool output; `exec: <argv0>: not found`
|
||||
// cannot). The residual text-collision trade is documented by the
|
||||
// seam's conservative classifier contract.
|
||||
// The operator names the configured runner's own pre-exec refusal dialect; the consumer
|
||||
// additionally re-joins the wrap through an outer `bash -c 'exec …'`, so we can add the
|
||||
// missing/unexecutable outer-shell shapes ourselves.
|
||||
runnerFailureSignatures: [
|
||||
...this.configuredRunnerFailureSignatures,
|
||||
`exec: ${argv0}: not found`,
|
||||
@@ -428,11 +334,7 @@ export class LocalSandboxProvider extends SandboxProvider {
|
||||
const chain = this.internals.chain ?? PLATFORM_CHAINS[this.internals.platform ?? process.platform] ?? []
|
||||
const [first, ...rest] = chain
|
||||
if (first === undefined) return 'unavailable'
|
||||
// One candidate = nothing to arbitrate: select it without probing. Its
|
||||
// runner fails closed at EXECUTION time if unusable (refuses to run the
|
||||
// command), and the wrap's runnerFailureSignatures let the consumer
|
||||
// classify that as a sandbox failure — never a silent unconfined run,
|
||||
// never a plain task failure.
|
||||
// A sole candidate needs no arbitration; its execution-time refusal still fails closed.
|
||||
if (rest.length === 0) return { runner: first, enforcement: STATIC_ENFORCEMENT[first] }
|
||||
for (const runner of chain) {
|
||||
const enforcement = this.probeRunner(runner)
|
||||
|
||||
@@ -9,22 +9,11 @@ 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.
|
||||
* Keyless backend integration through `confine()` and a real bwrap process. With no rung forced,
|
||||
* a passing probe must select the first rung. Tests assert world effects, wrap shape, and that the
|
||||
* kernel denial matches the advertised dialect; consumer coverage lives in dsh-bash-sandbox.
|
||||
* Skips when bwrap or user namespaces are unavailable. HOME-based workspaces avoid bwrap's
|
||||
* ephemeral `/tmp`, so workspace-write actually proves the workspace-root rebind.
|
||||
*/
|
||||
|
||||
const probe = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
|
||||
|
||||
@@ -10,22 +10,10 @@ 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.
|
||||
* Keyless backend integration through `confine()` and the registry `landlock-run` launcher, with
|
||||
* bwrap forced off. Tests assert real world effects; consumer coverage lives in dsh-bash-sandbox.
|
||||
* Skips when the platform package or enforcing kernel is unavailable. HOME-based workspaces avoid
|
||||
* Landlock's wholesale `/tmp` grant, so workspace-write proves the workspace-root grant itself.
|
||||
*/
|
||||
|
||||
const probe = spawnSync(launcherPath(), ['--probe'], { timeout: 5_000, encoding: 'utf8' })
|
||||
|
||||
@@ -252,19 +252,15 @@ describe('the platform chains', () => {
|
||||
})
|
||||
|
||||
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.
|
||||
// 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.
|
||||
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.
|
||||
// Only a cast can create this rogue closed-union tag. It must hit `assertNever`, ensuring a new
|
||||
// runner cannot silently use another runner's wrap or denial dialect.
|
||||
const { sandbox } = await setup()
|
||||
;(sandbox as unknown as { selectedRunner: unknown }).selectedRunner = { runner: 'chroot', enforcement: 'full' }
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow('unreachable variant')
|
||||
|
||||
@@ -7,30 +7,14 @@ 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.
|
||||
* Keyless publish-path rehearsal. It packs the package and workspace peers, installs those exact
|
||||
* tarballs in an external plain-Node consumer, and lets npm resolve the registry Landlock launcher
|
||||
* plus its platform package. No tsx, path mapping, or workspace resolution can hide missing files,
|
||||
* dependency errors, or lost executable modes.
|
||||
*
|
||||
* 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).
|
||||
* The installed launcher must match the host architecture, remain executable, and either confine a
|
||||
* real process with bwrap disabled or fail closed on a non-enforcing kernel. Skips off Linux or
|
||||
* before `pnpm run build`; launcher byte provenance belongs to its upstream release pipeline.
|
||||
*/
|
||||
|
||||
const packageDir = fileURLToPath(new URL('..', import.meta.url))
|
||||
@@ -84,11 +68,8 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-
|
||||
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.
|
||||
// Peer ranges resolve to the tarballs; Cordis is pinned to their peer range. Do not omit optional
|
||||
// dependencies because the launcher selects its OS/CPU package through one.
|
||||
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,
|
||||
|
||||
@@ -9,20 +9,11 @@ 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.
|
||||
* Keyless backend integration through `confine()` and a real macOS Seatbelt process, with Linux
|
||||
* rungs forced off. Tests assert world effects and that the kernel denial matches the advertised
|
||||
* dialect; consumer coverage lives in dsh-bash-sandbox. Skips off macOS or when the profile probe
|
||||
* fails. HOME-based workspaces avoid Seatbelt's wholesale temp-directory grants, so
|
||||
* workspace-write proves the workspace-root grant itself.
|
||||
*/
|
||||
|
||||
const probe = spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
|
||||
|
||||
@@ -21,3 +21,12 @@ Two families enforce the same mode vocabulary: the sandboxed bash executor (`@de
|
||||
## The per-session store
|
||||
|
||||
A runtime switch (an ACP `session/set_config_option`, a test scenario) is one log-only `sandbox/mode` event on the session it applies to. `effective = fold(events) ?? the deployment default`, so an override survives restart by replay, two sessions never see each other's state, and there is no external config store. The event is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event. Execution honors the fold in each tool layer, weakest-precedence beneath an escalation grant.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-bash` and `dsh-tool-fs`, which render the effective mode this service holds in their `[sandbox: …]` denial markers and escalation prompts; the `sandbox/mode` event itself never reaches the model.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **`workspaceRoot` is process-wide and fixed for the service's lifetime** — a per-session workspace root is a deferred phase of the sandbox RFC; this package centralizing the root is its groundwork, not its design.
|
||||
- **File-effect modes only** — `SandboxMode` governs file effects; network and process policy are outside its vocabulary, so no knob here restricts them.
|
||||
|
||||
@@ -9,3 +9,24 @@ Policy rides the call, not the provider: two consumers may confine under differe
|
||||
**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/implemented/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). Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/) (wraps `['bash', '-c', command]`).
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Confinement error, indirectly
|
||||
|
||||
**What the model sees**: Through [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md) and [`dsh-tool-bash`](../../bash/tool-bash/README.md), failure to enforce a requested mode produces code `SANDBOX_UNAVAILABLE` and the exact error below. An execution-time runner failure adds ` Runner failure: <detail>`.
|
||||
|
||||
**Token effect**: Conditional error text is visible for that call and retained in history until compaction.
|
||||
|
||||
#### Exact error
|
||||
|
||||
```markdown
|
||||
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.
|
||||
```
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **File effects are the whole policy vocabulary** — the seam expresses no network, process, syscall, device, or credential restrictions.
|
||||
- **Same-world confinement only** — containers, microVMs, and remote execution require replacing capability implementations rather than adding a provider here.
|
||||
- **Denial reporting is a stderr dialect** — the seam returns backend signatures instead of a typed runtime denial channel, so consumers that need classification must infer it from the child process's output.
|
||||
- **One provider per context** — composing different sandbox mechanisms simultaneously requires a provider-level ladder or separate Cordis contexts; callers choose policy per call, not backend identity.
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
/**
|
||||
* 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
|
||||
* (`@deepseek-ai/dsh-bash-sandbox` 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/implemented/feature/2026-07-06-sandbox.md.
|
||||
*
|
||||
* Same-world process-confinement seam: wrap exact subprocess argv under a
|
||||
* host-path file policy. Containers, microVMs, and remote execution replace the
|
||||
* surrounding capability seam instead; this service shares the host kernel and filesystem.
|
||||
* @module @deepseek-ai/dsh-sandbox
|
||||
*/
|
||||
|
||||
@@ -37,25 +20,10 @@ export type { EscalationApproval, EscalationApprover, EscalationOutcome, Escalat
|
||||
export { canonicalPath, writableRoots } from './roots.ts'
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* File-effect policy for confined processes. `read-only` permits only required
|
||||
* sinks such as `/dev/null`; `workspace-write` also permits the workspace and a
|
||||
* backend-defined temp area; `danger-full-access` bypasses confinement. Network
|
||||
* and process visibility are outside this vocabulary.
|
||||
*/
|
||||
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
|
||||
@@ -63,18 +31,9 @@ export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
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.
|
||||
* Enforcement completeness for this host. `partial` means an active backend or
|
||||
* older kernel ABI cannot govern every promised file effect; callers requiring
|
||||
* an absolute boundary must not treat it as `full`.
|
||||
*/
|
||||
export type SandboxEnforcement = 'full' | 'partial'
|
||||
|
||||
@@ -114,36 +73,25 @@ export interface ConfinedArgv {
|
||||
*/
|
||||
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`).
|
||||
* Case-insensitive signatures for runner failure before command execution.
|
||||
* Consumers check these before denial signatures: runner failure means the
|
||||
* command never ran, while denial means confinement worked and blocked it.
|
||||
*/
|
||||
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.
|
||||
* Error code for a requested confined mode when no backend is usable. The
|
||||
* provider fails closed, and `HarnessError` carries the code through
|
||||
* `tool/result` so callers can distinguish missing confinement from command
|
||||
* failure.
|
||||
*/
|
||||
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.
|
||||
* Thrown when {@link SandboxProvider.confine} cannot enforce the requested
|
||||
* mode. Carries {@link SANDBOX_UNAVAILABLE} through the structured error
|
||||
* channel.
|
||||
*/
|
||||
export class SandboxUnavailableError extends HarnessError {
|
||||
constructor(mode: ConfinedSandboxMode, detail?: string) {
|
||||
@@ -166,27 +114,10 @@ declare module 'cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* Abstract process-sandbox service. {@link confine} must return enforcing argv
|
||||
* or fail closed at wrap or runner-execution time; silent unconfined passthrough
|
||||
* is forbidden. Functional probes arbitrate multi-runner chains and may be
|
||||
* skipped for a sole candidate, whose own refusal remains the fail-closed end.
|
||||
*/
|
||||
export abstract class SandboxProvider extends Service {
|
||||
constructor(ctx: Context) {
|
||||
|
||||
Reference in New Issue
Block a user