/** * 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 */ import { Context, Service } from 'cordis' import { HarnessError } from '@deepseek-ai/dsh-llm' export { ESCALATION_TARGETS, WIDER_MODES, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs, } from './escalation.ts' export type { EscalationApproval, EscalationApprover, EscalationOutcome, EscalationRequest } from './escalation.ts' export { canonicalPath, writableRoots } from './roots.ts' /** * 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' /** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */ export type ConfinedSandboxMode = Exclude /** * 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. */ export interface SandboxExecutionPolicy { /** The file-effect mode this execution runs under. */ mode: SandboxMode /** Absolute root directory `workspace-write` may write under. */ workspaceRoot: string } /** * 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' /** * 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. */ export interface SandboxPolicy extends SandboxExecutionPolicy { /** The file-effect mode this execution runs under. */ mode: ConfinedSandboxMode } /** * 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[] /** * 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 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 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) { 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. {@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 { /* v8 ignore next -- Windows has no sandbox backend to instantiate this 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