Merge branch 'master' into worktree-windows-runtime

This commit is contained in:
Tianyi Cui
2026-07-20 20:39:13 +08:00
1064 changed files with 22568 additions and 10875 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
# @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.
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](../../../.agents/notes/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/implemented/feature/2026-07-06-sandbox.md).
**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 Agent Note](../../../.agents/notes/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]`).
+189
View File
@@ -0,0 +1,189 @@
/**
* The escalation vocabulary and choreography shared by every sandbox-enforcing
* tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the
* strictly-wider ladder, the argument-pairing validation, the model-facing
* denial/hint markers, and {@link approveEscalation} — the ordered fail-closed
* sequence that resolves a `sandbox_permissions` request through a
* user-approval channel BEFORE anything executes. One home keeps the two
* families' approval ordering and verbatim error texts from drifting apart.
*
* The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}),
* not the approval service type: the tool layer — which owns the agent, the
* call id, and the tool name — closes over `ctx.approval.request(...)` and
* hands the closure down, so this package never depends on the approval or
* agent packages.
*
* @module dsh-sandbox/escalation
*/
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { SandboxMode } from './index.ts'
/**
* The strictly-wider table: what a call whose effective mode is the key may
* escalate TO. Checked at EXECUTION, never baked into a tool schema — the
* schema's enum is {@link ESCALATION_TARGETS}, because schemas are
* registry-global while the effective mode is per-call truth.
*/
export const WIDER_MODES: Record<string, readonly SandboxMode[]> = {
'read-only': ['workspace-write', 'danger-full-access'],
'workspace-write': ['danger-full-access'],
}
/**
* The closed escalation-target vocabulary — every mode a call could ever
* escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
* whenever the mounted capability confines: cutting the enum down to the modes
* wider than the composition's DEFAULT would strand a session whose effective
* mode sits below it (a `danger-full-access` default would advertise nothing
* while a narrower-switched session stays confined with no lever).
*/
export const ESCALATION_TARGETS: readonly SandboxMode[] = ['workspace-write', 'danger-full-access']
/**
* Validate the escalation argument pairing a tool schema cannot express:
* `sandbox_permissions` and `justification` travel together — an approval
* prompt without a reason, or a reason driving nothing, is a malformed ask —
* and the justification must be a non-empty sentence.
* @param sandboxPermissions - the raw `sandbox_permissions` argument, if given.
* @param justification - the raw `justification` argument, if given.
*/
export function validateEscalationArgs(sandboxPermissions: string | undefined, justification: string | undefined): void {
if (sandboxPermissions !== undefined && justification === undefined) {
throw new Error('invalid escalation: sandbox_permissions requires a justification')
}
if (justification !== undefined && sandboxPermissions === undefined) {
throw new Error('invalid escalation: justification is only valid together with sandbox_permissions')
}
if (justification !== undefined && justification.trim().length === 0) {
throw new Error('invalid justification: expected a non-empty sentence')
}
}
/**
* The model-facing denial marker — the one vocabulary both enforcing families
* teach and report, so the model recognizes a policy denial identically
* whether the kernel refused a bash file effect or the filesystem provider's
* fence refused a mutation.
* @param mode - the mode the denied call ran under.
* @returns the marker line, exactly as the model sees it.
*/
export function sandboxDenialMarker(mode: SandboxMode): string {
return `[sandbox: file access denied under ${mode} mode]`
}
/**
* The same-turn escalation hint that rides a denial when the composition
* advertises the escalation fields — the nudge lives at the decision point so
* the sanctioned retry does not depend on the model recalling the tool
* description.
* @param subject - the family's noun for the denied action (`command` for
* bash, `operation` for a filesystem mutation).
* @returns the hint line, exactly as the model sees it.
*/
export function escalationHintMarker(subject: string): string {
return `[sandbox: escalation available — retry this exact ${subject} once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`
}
/**
* The closed outcome vocabulary of one escalation ask — structurally identical
* to the approval seam's `ApprovalOutcome` so an `ApprovalService.request`
* return is assignable without this package importing it.
*/
export type EscalationOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
/**
* The minimal approval-request shape {@link approveEscalation} needs —
* structurally the approval seam's `ApprovalService`, generic over the agent
* type `A` and call-id type `C` so this package resolves escalations through
* `ctx.approval` without importing the approval or agent packages (the tool
* layer infers `A`/`C` as its own `Agent`/`CallId`).
*/
export interface EscalationApprover<A = object, C = string> {
/**
* Ask the human to approve one action, resolving to a closed outcome.
* @param req - the audit-self-contained request (agent, tool, call id, reason, optional signal).
* @returns the human's decision as a closed {@link EscalationOutcome}.
*/
request(req: { agent: A; toolName: string; callId: C; reason: string; signal?: AbortSignal }): Promise<EscalationOutcome>
}
/**
* The approval ingredients an escalating tool hands {@link approveEscalation}:
* the approval requester (`ctx.approval`, or `undefined` when none is
* composed), the calling agent (or `undefined` for an agent-less execution),
* and the call's identity. The tool layer holds all of these; this package
* only judges them.
*/
export interface EscalationApproval<A = object, C = string> {
/** The approval requester (`ctx.approval`), or `undefined` when none is composed. */
approver: EscalationApprover<A, C> | undefined
/** The calling agent, or `undefined` for an agent-less execution (fails closed). */
agent: A | undefined
/** The tool-call id the approval prompt attaches to. */
callId: C
/** The tool name recorded on the approval request. */
toolName: string
/** The tool-execution abort signal the approval request rides, when present. */
signal?: AbortSignal
}
/** One escalation request, as {@link approveEscalation} judges it. */
export interface EscalationRequest {
/** The requested target mode (schema-pinned to {@link ESCALATION_TARGETS} when advertised). */
requestedMode: string
/** The model's one-sentence reason, shown verbatim to the user inside the audit reason. */
justification: string
/** The call's effective mode (session override ?? composition default) the request must strictly widen. */
effectiveMode: SandboxMode
/** The family's noun for the escalated action in user-facing texts (`command` for bash, `operation` for fs). */
subject: string
}
/**
* Resolve a sandbox-escalation request BEFORE anything executes: check strict
* widening against the call's effective mode, then resolve the approval
* channel, then map every outcome — the ordered fail-closed sequence both
* enforcing families share. Returns the granted mode to stamp onto exactly
* this call; throws the distinct verbatim text for every other path (a
* non-widening request, a missing approval service, an agent-less execution,
* a rejection, a cancellation, an unanswerable ask) — the tool registry turns
* the throw into the call's isError result, and nothing has run. A
* non-widening request never prompts a human.
* @param request - the escalation to judge (see {@link EscalationRequest}).
* @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
* @returns the granted mode, consumed by the one call that asked.
*/
export async function approveEscalation<A, C>(request: EscalationRequest, approval: EscalationApproval<A, C>): Promise<SandboxMode> {
const { requestedMode: mode, effectiveMode, justification, subject } = request
// Strict widening is an EXECUTION check against the call's effective mode —
// deliberately not a schema constraint (the enum is the closed target
// vocabulary; the effective mode is per-call truth).
if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode as SandboxMode)) {
throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`)
}
if (approval.approver === undefined) {
throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`)
}
if (approval.agent === undefined) {
throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`)
}
// Self-contained for the audit trail: approval/asked stores this reason,
// and the target mode is part of the grant's identity.
const outcome = await approval.approver.request({
agent: approval.agent,
toolName: approval.toolName,
callId: approval.callId,
reason: `escalate sandbox to ${mode}: ${justification}`,
...approval.signal ? { signal: approval.signal } : {},
})
switch (outcome) {
// The schema enum already pinned `mode` to the closed target vocabulary;
// the check above proved it is strictly wider.
case 'allowed-once': return mode as SandboxMode
case 'rejected': throw new Error(`the user rejected escalating this ${subject} to "${mode}"`)
case 'cancelled': throw new Error(`approval for escalating to "${mode}" was cancelled`)
case 'unavailable': throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`)
default: return assertNever(outcome, 'EscalationOutcome')
}
}
+11
View File
@@ -8,6 +8,17 @@
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
+51
View File
@@ -0,0 +1,51 @@
/**
* The writable-root derivation shared by every enforcement dialect that
* expresses a mode as a canonical allow-list: `workspace-write` means "the
* workspace root plus the platform temp areas", and this module is that
* meaning's one home. The Seatbelt profile
* (`@deepseek-ai/dsh-sandbox-local`) and the in-process filesystem fence
* (`@deepseek-ai/dsh-fs-sandbox`) both derive their allow-list here, so "the
* write tool cannot write /tmp but bash can" asymmetries cannot arise between
* them. The bwrap and Landlock dialects keep their own grant spellings (an
* ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner
* differences recorded in the sandbox RFC — with parity pinned by test.
*
* @module dsh-sandbox/roots
*/
import { realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import type { SandboxPolicy } from './index.ts'
/**
* Resolve a granted root to the path the enforcement layer actually compares:
* canonical (symlinks resolved), because both Seatbelt filters and the fs
* fence's containment check match resolved paths — `/tmp` IS `/private/tmp`
* on darwin, and an as-spelled grant would match nothing.
* @param path - the root as configured or platform-reported.
* @returns the canonical path, or the spelling as-is when resolution fails
* (a missing root matches nothing until it exists — the conservative
* outcome; inventing a fallback would grant a path the caller never named).
*/
export function canonicalPath(path: string): string {
try {
return realpathSync(path)
} catch {
// realpathSync failed: the path (or a prefix) is missing or unreadable.
return path
}
}
/**
* The roots one confined execution may WRITE under — the mode's meaning as a
* canonical, deduplicated allow-list. `read-only` allows nothing;
* `workspace-write` allows the policy's workspace root, the host `/tmp`, and
* the per-user platform temp dir (`os.tmpdir()` — the real temp area for
* mkstemp-family tools; omitting it would deny what the mode promises).
* @param policy - the file-effect policy to derive the allow-list from.
* @returns the canonical writable roots; empty exactly under `read-only`.
*/
export function writableRoots(policy: SandboxPolicy): string[] {
if (policy.mode !== 'workspace-write') return []
return [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
}
@@ -0,0 +1,111 @@
/**
* Tests for the shared escalation vocabulary and choreography: the strictly-
* wider ladder, the argument-pairing validation, the model-facing markers, and
* {@link approveEscalation}'s ordered fail-closed sequence. Both enforcing tool
* families (`dsh-tool-bash`, `dsh-tool-fs`) delegate here, so the ordering and
* verbatim texts are pinned once, next to the vocabulary that owns them.
*/
import { describe, expect, it } from 'vitest'
import {
ESCALATION_TARGETS,
WIDER_MODES,
approveEscalation,
escalationHintMarker,
sandboxDenialMarker,
validateEscalationArgs,
} from '@deepseek-ai/dsh-sandbox'
import type { EscalationApprover, EscalationOutcome } from '@deepseek-ai/dsh-sandbox'
describe('the strictly-wider ladder', () => {
it('read-only escalates to either wider mode; workspace-write only to full access', () => {
expect(WIDER_MODES['read-only']).toEqual(['workspace-write', 'danger-full-access'])
expect(WIDER_MODES['workspace-write']).toEqual(['danger-full-access'])
expect(WIDER_MODES['danger-full-access']).toBeUndefined()
})
it('the target enum is the closed set every session could escalate TO (read-only is the floor)', () => {
expect(ESCALATION_TARGETS).toEqual(['workspace-write', 'danger-full-access'])
})
})
describe('validateEscalationArgs', () => {
it('accepts neither field, or both with a non-empty justification', () => {
expect(() => { validateEscalationArgs(undefined, undefined) }).not.toThrow()
expect(() => { validateEscalationArgs('workspace-write', 'because the workspace needs it') }).not.toThrow()
})
it('rejects one field without the other, and a blank justification', () => {
expect(() => { validateEscalationArgs('workspace-write', undefined) }).toThrow(/requires a justification/)
expect(() => { validateEscalationArgs(undefined, 'orphan reason') }).toThrow(/only valid together with sandbox_permissions/)
expect(() => { validateEscalationArgs('workspace-write', ' ') }).toThrow(/non-empty sentence/)
})
})
describe('the model-facing markers', () => {
it('the denial marker names the mode', () => {
expect(sandboxDenialMarker('read-only')).toBe('[sandbox: file access denied under read-only mode]')
expect(sandboxDenialMarker('workspace-write')).toBe('[sandbox: file access denied under workspace-write mode]')
})
it('the hint marker names the family subject', () => {
expect(escalationHintMarker('command')).toContain('retry this exact command once with sandbox_permissions')
expect(escalationHintMarker('operation')).toContain('retry this exact operation once with sandbox_permissions')
})
})
describe('approveEscalation', () => {
const req = (over: Partial<Parameters<typeof approveEscalation>[0]> = {}) => ({
requestedMode: 'workspace-write',
justification: 'the user asked to write in the workspace',
effectiveMode: 'read-only' as const,
subject: 'command',
...over,
})
/** An approver that records the request and returns a fixed outcome. */
const approver = (outcome: EscalationOutcome, sink?: (req: unknown) => void): EscalationApprover => ({
request: async (request) => { sink?.(request); return outcome },
})
const ingredients = (over: Partial<Parameters<typeof approveEscalation>[1]> = {}) => ({
approver: approver('allowed-once'),
agent: {},
callId: 'call-1',
toolName: 'bash',
...over,
})
it('grants: returns the requested mode, asking through the approver with the audit reason', async () => {
const seen: { reason?: string }[] = []
const granted = await approveEscalation(req(), ingredients({ approver: approver('allowed-once', r => seen.push(r as { reason?: string })) }))
expect(granted).toBe('workspace-write')
expect(seen[0]?.reason).toBe('escalate sandbox to workspace-write: the user asked to write in the workspace')
})
it('a non-widening request fails closed with its own text and never asks', async () => {
const seen: unknown[] = []
const spy = ingredients({ approver: approver('allowed-once', r => seen.push(r)) })
await expect(approveEscalation(req({ requestedMode: 'read-only' }), spy))
.rejects.toThrow(/not strictly wider than this call's current "read-only" mode/)
await expect(approveEscalation(req({ requestedMode: 'workspace-write', effectiveMode: 'danger-full-access' as never }), spy))
.rejects.toThrow(/not strictly wider/)
expect(seen).toEqual([])
})
it('a missing approval service and an agent-less call each fail closed with distinct text', async () => {
await expect(approveEscalation(req(), ingredients({ approver: undefined }))).rejects.toThrow(/no approval service is composed/)
await expect(approveEscalation(req(), ingredients({ agent: undefined }))).rejects.toThrow(/no agent to route it through/)
})
it('maps each non-grant outcome to its distinct verbatim text (subject in the rejection)', async () => {
await expect(approveEscalation(req({ subject: 'operation' }), ingredients({ approver: approver('rejected') })))
.rejects.toThrow('the user rejected escalating this operation to "workspace-write"')
await expect(approveEscalation(req(), ingredients({ approver: approver('cancelled') })))
.rejects.toThrow('approval for escalating to "workspace-write" was cancelled')
await expect(approveEscalation(req(), ingredients({ approver: approver('unavailable') })))
.rejects.toThrow('no approval channel is available')
})
it('an outcome outside the closed union trips the exhaustiveness guard (defensive)', async () => {
await expect(approveEscalation(req(), ingredients({ approver: approver('bogus' as never) }))).rejects.toThrow()
})
})
@@ -0,0 +1,39 @@
/**
* Tests for the writable-root derivation: the mode's meaning as a canonical
* allow-list. Pinned here so the fs fence and the Seatbelt profile — both
* deriving from `writableRoots` — cannot drift.
*/
import { realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { mkdtempSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
describe('canonicalPath', () => {
it('resolves symlinks (an existing path realpaths)', () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-roots-'))
expect(canonicalPath(dir)).toBe(realpathSync(dir))
})
it('returns the spelling as-is when the path cannot be resolved (conservative — matches nothing until it exists)', () => {
expect(canonicalPath('/does/not/exist/anywhere-xyz')).toBe('/does/not/exist/anywhere-xyz')
})
})
describe('writableRoots', () => {
it('read-only grants nothing', () => {
expect(writableRoots({ mode: 'read-only', workspaceRoot: process.cwd() })).toEqual([])
})
it('workspace-write grants the workspace root plus the platform temp areas, canonical and deduplicated', () => {
const ws = mkdtempSync(join(tmpdir(), 'dsh-ws-'))
const roots = writableRoots({ mode: 'workspace-write', workspaceRoot: ws })
expect(roots).toContain(realpathSync(ws))
expect(roots).toContain(canonicalPath('/tmp'))
expect(roots).toContain(realpathSync(tmpdir()))
// Deduplicated after canonicalization (/tmp and os.tmpdir() may coincide).
expect(new Set(roots).size).toBe(roots.length)
})
})