# Conflicts: # .agents/notes/README.i18n.yaml # .agents/notes/README.zh.md # docs/core-data-structures/bash.md # docs/core-data-structures/code-runtime.md # docs/core-data-structures/compaction.md # docs/core-data-structures/scope.md # docs/core-data-structures/session-query.md # docs/core-data-structures/user-interaction.md # docs/core-data-structures/web.md # docs/rfc/README.md # scripts/translation-pairing.manifest.json # scripts/type-equiv.manifest.json
7.3 KiB
Process Sandbox
English | 中文
The process-sandbox seam of dsh-sandbox wraps a same-world subprocess argv in a file-effect policy without coupling consumers to a platform runner. dsh-sandbox-local supplies the Linux bwrap/Landlock and macOS Seatbelt backends; dsh-bash-sandbox is the first consumer. Containers, microVMs, and remote execution are sibling implementations of whole capability seams, not providers of ctx.sandbox.
Source: packages/sandbox/sandbox/src/index.ts
Modes and enforcement
SandboxMode governs filesystem effects only. read-only denies writes except the required /dev/null sink; workspace-write permits writes under the workspace root and the backend's promised temp area; danger-full-access bypasses confinement. Network and process visibility are outside this vocabulary.
/**
* 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.
*/
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
Only the first two modes can be sent to a provider. A danger-full-access consumer spawns its original argv and does not call ctx.sandbox.
/** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */
type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
Enforcement is a reported fact. full means the backend governs every file effect promised by the mode; partial means an active backend or older kernel ABI governs only a subset, so consumers that require the absolute promise must reject or surface that distinction.
/**
* 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`.
*/
type SandboxEnforcement = 'full' | 'partial'
Per-call policy
The complete execution policy is resolved and carried per capability call. It includes danger-full-access so a consumer can resolve policy once before deciding whether to bypass confinement. Normal tool calls derive workspaceRoot from the calling session's immutable cwd; deployment configuration is the agentless fallback. The root is canonicalized with filesystem semantics before lexical normalization, so a cwd containing symlink/.. identifies the directory where a spawned process actually runs.
/**
* The complete file-effect policy resolved for one capability call. The root
* is carried even under modes that do not consume it so callers can resolve
* policy once before choosing the enforcement path.
*/
interface SandboxExecutionPolicy {
/** The file-effect mode this execution runs under. */
mode: SandboxMode
/** Absolute root directory `workspace-write` may write under. */
workspaceRoot: string
}
ctx.sandboxPolicy.resolve() accepts the active session and, for an approved retry, an explicit mode. The service owns precedence and root fallback so bash and fs do not repeat it.
/** Inputs that select the sandbox policy for one capability call. */
interface SandboxPolicyRequest {
/** Calling session; its immutable cwd becomes the workspace boundary. */
session?: Session
/** Explicit approved mode override, which outranks session policy. */
mode?: SandboxMode
}
Only a confined execution reaches ctx.sandbox; its provider policy narrows the mode while retaining the same root. This permits concurrent sessions, consumers, and one-shot escalated retries to ask the same provider for different boundaries without mutating provider state.
/**
* 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 an explicit step at
* the consumer boundary; the provider treats the policy as fully specified.
*/
interface SandboxPolicy extends SandboxExecutionPolicy {
/** The file-effect mode this execution runs under. */
mode: ConfinedSandboxMode
}
Wrapped argv and classification dialects
ConfinedArgv is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr dialects. denialSignatures identify the confined command being blocked while the sandbox works correctly. runnerFailureSignatures identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
/**
* 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.
*/
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[]
/**
* 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[]
}
An operator-configured local runner must supply at least one runnerFailureSignatures entry for its own pre-exec refusal dialect; the provider adds outer-shell missing and unexecutable forms automatically. This makes an executable custom runner rejecting its profile distinguishable from the wrapped command exiting with the same status.
Provider and fail-closed errors
ctx.sandbox.confine(argv, policy) returns a ConfinedArgv or throws SandboxUnavailableError with code SANDBOX_UNAVAILABLE when no usable backend exists. A selected runner can also fail closed at execution time, in which case its failure signature carries the same infrastructure meaning. Silent unconfined passthrough is never legal for a confined policy.
Provider probing arbitrates between multiple candidates and is cached for the provider lifetime. A platform with one candidate may select it directly; execution-time refusal retains the safety property. The local provider reports bwrap and Seatbelt as full and preserves the Landlock launcher's full/partial kernel verdict.