feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox RFC's deferred cross-family phase. - dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the deployment default mode + workspaceRoot and the per-session override event, renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter. Decouples the bash seam from dsh-session. - dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences write/edit by the per-call mode (read-only denies, workspace-write contains to the workspace + temp roots via the shared writableRoots, danger passes through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent re-canonicalization. A policy fence in trusted code, not a kernel boundary. - dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider ladder, denial/hint markers, approveEscalation) both tool families use; approveEscalation takes a structural approver so dsh-sandbox gains no approval/agent dependency, and both tools stay duplication-free. - tool-fs: write/edit advertise sandbox_permissions/justification under a confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker, and resolve the same one-approved-wider retry. - examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating that disabled the fs stack under confined modes. RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
This commit is contained in:
100 files changed
+2238
-385
No files matched your search
@@ -19,7 +19,6 @@ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskId, BashTaskListener, BashTaskRead, OwnerToken } from './types.ts'
|
||||
|
||||
export { BashTaskId, OwnerToken } from './types.ts'
|
||||
export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts'
|
||||
export type {
|
||||
BashExecRequest,
|
||||
BashExecSpec,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Per-session sandbox-mode override: the session log as the store. A runtime
|
||||
* switch (an ACP `session/set_config_option`, a test scenario) is recorded as
|
||||
* one `bash/sandbox-mode` event on the session it applies to;
|
||||
* `effective = fold(events) ?? the executor's configured default`, so an
|
||||
* override survives restart by replay, two sessions can 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
|
||||
* prompt section and the boundary notices in `@deepseek-ai/dsh-tool-bash`,
|
||||
* never from the event itself. EXECUTION honors the fold in the tool layer —
|
||||
* it stamps the effective mode onto each call's `BashExecRequest.sandboxMode`
|
||||
* (weakest-precedence: an escalation grant for the call outranks it) — the
|
||||
* executor itself stays a config-fixed default plus per-call overrides.
|
||||
*
|
||||
* @module dsh-bash/session-mode
|
||||
*/
|
||||
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* The session's sandbox mode was switched — log-only (like `approval/*`;
|
||||
* NOT a surface event, carries no `surfaceOp`): durable and replayable,
|
||||
* never in the model transcript. The LAST such event is the session's
|
||||
* override ({@link effectiveSandboxMode}); who asked for it is derivable
|
||||
* from position (an event after the log's last `request/header*` was a
|
||||
* runtime switch by the user; see the tool layer's narrator).
|
||||
*/
|
||||
'bash/sandbox-mode': { mode: SandboxMode }
|
||||
}
|
||||
}
|
||||
|
||||
/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */
|
||||
export const SANDBOX_MODES: readonly SandboxMode[] = ['read-only', 'workspace-write', 'danger-full-access']
|
||||
|
||||
/**
|
||||
* The session's sandbox-mode override: the last `bash/sandbox-mode` event in
|
||||
* the log, or undefined when the session never switched (callers apply the
|
||||
* executor's configured default). The pure fold — resume needs no catch-up
|
||||
* machinery because replaying the log IS the state.
|
||||
* @param events - session events in log order (other event types are skipped).
|
||||
* @returns the mode of the last switch event, or undefined without one.
|
||||
*/
|
||||
export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined {
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const event = events[index] as SessionEvent
|
||||
if (event.type === 'bash/sandbox-mode') return event.data.mode
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* THE write path for a session's sandbox-mode override: appends exactly one
|
||||
* `bash/sandbox-mode` event — the switch IS its event; nothing mutates mode
|
||||
* state out of band. Takes effect on the session's next bash call and next
|
||||
* prompt assembly (the consumers fold on every read).
|
||||
* @param session - the session the override belongs to.
|
||||
* @param mode - the mode every subsequent bash call in this session runs
|
||||
* under (until the next switch).
|
||||
*/
|
||||
export function setSandboxMode(session: Session, mode: SandboxMode): void {
|
||||
session.append('bash/sandbox-mode', { mode })
|
||||
}
|
||||
@@ -130,7 +130,7 @@ export interface BashExecRequest {
|
||||
* consumer sets it only from an explicit policy source — an
|
||||
* `'allowed-once'` grant a human just issued through `ctx.approval` (the
|
||||
* escalation flow in the sandbox RFC § Escalation, which outranks), or the
|
||||
* session's standing override folded from its own `bash/sandbox-mode`
|
||||
* session's standing override folded from its own `sandbox/mode`
|
||||
* events (the sandbox RFC § Per-session mode switching — the user's recorded per-session
|
||||
* choice). A sandboxing executor confines THIS call under the given mode;
|
||||
* a non-sandboxing executor carries the field and confines nothing (the
|
||||
|
||||
Reference in New Issue
Block a user