The `readDenyPaths` policy field shipped in the previous commit broke Linux
confinement outright. bwrap has to create the `/dev/null` bind's mount point
inside a tree its own profile has already made read-only, so it refused the
entire confinement whenever the parent directory was absent — every host that
has not stored a credential yet, including a fresh install:
bwrap: Can't mkdir parents for /home/runner/.dsh/.env: Read-only file system
which the executor correctly classifies as SANDBOX_UNAVAILABLE, so every
confined bash call failed closed. Landlock cannot subtract from its own `/`
read grant, so it reported `partial` enforcement on every confined call for a
file it never hid, with no way to switch the denial off (schemastery fills an
omitted array with `[]`, so empty and omitted were indistinguishable).
A protection that breaks confinement where it works and misreports it where it
does not is worse than a documented absence. Revert the field, both expressible
backends, the enforcement downgrade, and the policy default; state the residue
plainly in the credentials-local READMEs — file mode stops other OS users, not
the model — and keep the OS-keychain provider recorded as the real answer.
The narrower discipline stands: no surface hoists the credential document into
`process.env`, and the model is never handed a resolved path to it.
The credential store is 0600 under a 0700 directory, which stops other OS
users but not the model: tool processes run as the same user, so under
the shipped danger-full-access default they read it like any other file.
SandboxExecutionPolicy grows readDenyPaths, and sandbox-policy defaults
it to $DSH_HOME/.env — the exact file rather than the harness home, so
the model keeps its documented access to its own session log. Seatbelt
appends a trailing deny (last matching rule wins) and bwrap maps
/dev/null over each path after any workspace bind; Landlock grants are a
pure allow-list that cannot subtract from its own / read grant, so
confine() reports partial enforcement there instead of claiming a
boundary the process does not have.
A real-kernel Seatbelt e2e proves the shape: the same read succeeds
unconfined and fails under the denial, while a sibling file in the same
directory stays readable. Both READMEs state the residual boundary
plainly — no confining mode means no boundary — and record the OS
keychain provider as the real answer.
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.