Files
deepseek-harness/examples/sandbox-acp-agent
Yichen Jiang 740106a51a Merge remote-tracking branch 'origin/master' into codex/rfc-subagent-background-tasks
# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/bash.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	docs/tool-catalog.md
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	packages/README.md
#	packages/bash/README.md
#	packages/bash/bash-local/src/index.ts
#	packages/bash/bash/README.md
#	packages/bash/bash/package.json
#	packages/bash/bash/src/index.ts
#	packages/bash/bash/src/types.ts
#	packages/bash/bash/tests/service.spec.ts
#	packages/bash/bash/tsconfig.json
#	packages/bash/tool-bash/README.md
#	packages/bash/tool-bash/src/index.ts
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/bash/tool-bash/tsconfig.json
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-core/README.md
#	packages/core/agent-core/package.json
#	packages/core/agent-core/src/index.ts
#	packages/core/agent-core/tests/agent-core.spec.ts
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
#	packages/hooks/hook-protocol/tests/runner.spec.ts
#	packages/ui/acp-agent/tests/acp-agent.spec.ts
#	packages/ui/stdio-agent/tests/stdio-agent.spec.ts
#	pnpm-lock.yaml
#	scripts/doc-budgets.manifest.json
#	scripts/gen-tool-catalog.ts
2026-07-11 23:04:27 +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-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 — 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).