sandbox-acp-agent
The coding agent with its bash executor swapped for the sandbox stack (@deepseek-ai/dsh-sandbox-local + @deepseek-ai/dsh-bash-sandbox — the one-entry executor swap the ctx.bash capability seam exists for), served over the Agent Client Protocol, plus @deepseek-ai/dsh-user-approval — which makes this the first composition where the approval loop is LIVE end to end: bash runs under read-only, a denial comes back as the structured marker, the model retries once with sandbox_permissions + justification, the ACP bridge's answerer turns that ask into a session/request_permission prompt in your editor, and "Allow once" runs exactly that command under the wider mode (sandbox RFC § Escalation).
pnpm run demo:sandbox-acp # needs DEEPSEEK_API_KEY; drive it from Zed or any ACP client
Zed setup is the same as acp-agent with this example's command; only the leaf cordis.yml differs (the sandbox stack + the approval entry in place of the local bash executor and the extra tool stacks).
- Every approval is one-shot (
Allow once/Reject— noallow_always: the harness has no grant storage yet), and a dismissed prompt or a rejected ask fails closed with its own error text; so does every ask when no editor is attached to answer. - Two session config options are live (sandbox RFC § Per-session mode switching): a capable client shows
Sandbox(read-only/workspace-write/danger-full-access) andApprovals(ask/never) selectors per session — a switch is one log-only event on that session's log and execution follows it; the sandbox mode is deliberately NOT stated in the prompt or narrated (the model learns the boundary from the denial marker — behavior, not belief), while an approval switch toneveris stated and narrated; a resumed session reports its overrides back onsession/load. - The write boundary is config-fixed: an escalated
workspace-writerun may write under the launch directory (workspaceRoot: process.cwd()) plus the platform temp area — a per-session root is config-phase future work in the sandbox RFC. - No usable runner fails closed per command (structured
SANDBOX_UNAVAILABLE), and the filesystem tools stay unloaded for the same reason assandbox-agent: they would bypass the bash sandbox.
Tests: tests/escalation.e2e.ts — keyless, it boots the real cordis.yml through the Loader as an ACP subprocess, proves the whole tree (sandbox executor + approval service + bridge) initializes and opens a session, and drives the config options end to end (both advertised with composition currents, switches honored and echoed as complete state, out-of-vocabulary values rejected); with a key and a usable runner, a scripted ACP client plays the human — the real model gets denied, escalates, the client answers allow-once, and the retried write must land on disk. tests/acp.snapshot.ts (the shared snapshot kit over this composition's cordis.snapshot.yml replay overlay) pins four scenarios as committed wire bytes: the keyless config-option exchange, the recorded mode-switching arc (the suite's pinned header — both switches, their prompt-section deltas, one "changed by the user" notice per knob, and a confined write landing under the switched mode), and both recorded escalation branches (session/request_permission answered allow-once / reject-once). Replay re-executes every recorded bash call under the host's real runner — Seatbelt works out of the box on macOS; on Linux install bubblewrap (or build the Landlock launcher) first, exactly what ci.yml's snapshot lane does. No fixture carries a real denial: denial stderr is backend dialect and would pin a fixture to its recording platform (the rationale comment atop the suite file).