deepseek-harness/packages/fs/fs-sandbox
kingwl 2dc62497ce 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.
2026-07-14 20:05:57 +08:00
..
src feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity 2026-07-14 20:05:57 +08:00
tests feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity 2026-07-14 20:05:57 +08:00
package.json feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity 2026-07-14 20:05:57 +08:00
README.md feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity 2026-07-14 20:05:57 +08:00
tsconfig.json feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity 2026-07-14 20:05:57 +08:00

dsh-fs-sandbox — the sandbox-enforcing filesystem backend

SandboxedFileSystem extends LocalFileSystem and registers as ctx.fs. It inherits every text-storage mechanic verbatim (resolve, stat, read/stream, list, the atomic write, the read-match-write edit critical section) and adds only a per-call MODE fence on writeText/editText. Reads always pass through — every mode permits reading.

Loading it INSTEAD OF dsh-fs-local, together with a ctx.sandboxPolicy, is the whole swap; the model-facing tools (dsh-tool-fs) are untouched. Injects sandboxPolicy for the default mode and the workspace-write boundary root — the SAME policy home bash reads, so the two families never confine to different roots.

The fence

The per-call mode is the tool-stamped effective mode (session override or escalation grant), falling back to the deployment default:

  • read-only — denies every mutation with the structured FS_SANDBOX_DENIED.
  • workspace-write — allows a mutation only when the target canonicalizes under a writable root: the workspace root plus the platform temp areas (/tmp, os.tmpdir()), the SAME set the Seatbelt profile grants, derived from the one writableRoots function so the fs fence and the bash runner cannot drift. The target is re-canonicalized immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught.
  • danger-full-access — delegates unfenced.

Threat model: a policy fence, not a kernel boundary

The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface. This mirrors the code-runtime stance: containment, not a security boundary. Kernel-grade isolation of untrusted CODE stays ctx.bash's job (dsh-bash-sandbox). The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs openat2-class primitives not worth their portability cost here.

A denial is a structured FsError (FS_SANDBOX_DENIED, carrying the effective mode) — no stderr text inference (unlike bash's kernel denials), because an in-process fence knows exactly what it refused. The model-facing [sandbox: file access denied under <mode> mode] marker and the one-approved-wider retry live in the tool layer (dsh-tool-fs), exactly as bash's do. See the cross-family fs sandbox RFC.