Files
deepseek-harness/examples/sandbox-acp-agent
kingwl ca39fd89b0 feat(example): sandbox-acp-agent — the live composition; RFCs to implemented
The three-entry cordis.yml (dsh-sandbox-local + dsh-bash-sandbox at a
read-only default + dsh-approval) served over ACP: the first live approval
composition. Recorded snapshot scenarios pin the wire end to end —
config-options advertisement, the mode-switching arc as the suite pinned
header (both switches, the prompt-section delta, one changed-by-the-user
notice per knob, a confined write landing under the switched mode), and
both escalation branches over scripted permissionAnswers (a grant runs
confined under workspace-write; a rejection executes nothing and pins the
fail-closed text). The with-key escalation e2e drives a real model +
real runner + the real bridge answerer, world-verified; ci.yml snapshot
lane and e2e.yml install bubblewrap so the confined replays actually
execute.

Both RFCs move to implemented/ (Decision/Consequences form, deferred
phases tracked in their own sections), with every cross-reference flipped.
2026-07-10 15:44:38 +08:00
..

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-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 — no allow_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) and Approvals (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 to never is stated and narrated; a resumed session reports its overrides back on session/load.
  • The write boundary is config-fixed: an escalated workspace-write run 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 as sandbox-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).