A preset names a bundle of the two mechanism knobs — request = workspace-write + ask, yolo = danger-full-access + never — so the editor shows ONE 'Permissions' select where the sandbox-mode and approval-policy tiers stay orthogonal capabilities (the Codex /approvals shape: presets over two dials). ctx.permission (dsh-permission) owns the config-defined table, validates the default preset's bundle against the composed knob defaults at load (fails loud), and writes a switch THROUGH: one log-only permission/preset event (the audit fact reverse-mapping cannot recover — the planned 'agent' preset shares request's knob values and differs only in composed policy) plus each knob event via its own setter, deduped — a net-zero switch appends nothing. Every knob consumer keeps reading its own fold, untouched. The current preset DERIVES from the effective knob values — the fold breaks bundle ties, a knob state outside the table is the reserved 'custom' value (a state, not an error: shown while it holds, switchable FROM, never a target), and defaultPreset disappears (zero-event state reverse-maps from the composition defaults). The ACP bridge drops the two per-knob selects for the one preset select (advertised only when ctx.permission is composed); pending/anchor/no-op semantics carry over unchanged, with the no-op echo acknowledged before vocabulary validation so a client re-pushing a derived 'custom' current never errors. The sandbox variant example composes the service with a workspace-write default; the permission-switching, escalation-approved and escalation-rejected scenarios are re-recorded under it (escalations now target an outside-workspace /tmp path under danger-full-access, self-cleaning) and config-options is re-authored on the single-select wire.
8.2 KiB
acp-agent example
The DeepSeek Harness SDK agent demo exposed as an Agent Client Protocol (ACP) server over JSON-RPC stdio — drive it from Zed or any other ACP client.
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
pnpm run demo:code-mode acp # the same server in Code Mode: one wire tool, run_code
This example is just a leaf cordis.yml: it loads the @deepseek-ai/dsh-acp-agent app (which bundles the @deepseek-ai/dsh-agent-core spine, JSONL session persistence, and the @deepseek-ai/dsh-acp bridge — with no pre-created agents, since ACP session/new creates them on demand), the swappable DeepSeek, bash, and filesystem backends, the model-facing read/write/edit/subagent/subagent_fork/todo_write tool entries, and the advisory repeat-tool-guard loop-hygiene plugin. The app package bakes in the no-stdout-logger cluster, so a leaf has no logger entry to get wrong by default — keeping stdout pure for JSON-RPC. demo:code-mode acp boots the same tree through the code-mode.cordis.yml overlay — the tool surface collapses to run_code + the generated TypeScript SDK, dispatching through the worker-thread code runtime (see the dsh-tools Code Mode section).
stdout is the protocol
This example loads no stdout logger — stdout carries the JSON-RPC frames, and any other write corrupts them. @deepseek-ai/dsh-acp-agent includes no logger entry, so this leaf has none to get wrong by default; do not add one (use a stderr exporter if you need logs).
Zed configuration
Add to your Zed settings.json under agent_servers:
{
"agent_servers": {
"DeepSeek Harness": {
"command": "pnpm",
"args": ["--dir", "/path/to/deepseek-harness", "run", "demo:acp"],
"env": { "DEEPSEEK_API_KEY": "sk-…" }
}
}
}
The editor sets each session's cwd to the project it opens; both the agent's bash tools and the read/write/edit filesystem tools resolve relative paths against that per-session workspace (see the per-session cwd note in packages/ui/acp and the per-session cwd RFC), so the server can be launched anywhere and each session still acts on its own project directory.
Snapshot tests (record-once / replay-deterministic)
This example is the home of the harness's snapshot tests — they boot this server as a real subprocess, drive it with a deterministic input script, and diff its normalized output against committed golden files. The model is made deterministic by @deepseek-ai/dsh-llm-replay, a function/namespace plugin that installs an llm/stream waterfall listener and short-circuits it, serving model streams reconstructed from a recorded session JSONL fixture (<scenario>/session.jsonl) — so replay needs no API key. The fixture IS the persisted session log: its assistant/chunk events carry every StreamChunk, so grouping them by (turn, step) reconstructs each stream() call (one model call per loop step). Recording is therefore "run the real agent once and harvest the .jsonl"; use pnpm run test:snapshot:record when the model transcript itself should change, and pnpm run test:snapshot:refresh when the committed model transcript is still the right mock input and only the current replay output/goldens need to be rewritten. The two failure modes not expressible as logged chunks — a pure throw before any chunk, and cancel/hang — use an optional <scenario>/replay.override.json sidecar (a ReplayEntry[] that replaces the derived script). A scenario that needs the agent to operate on existing files ships an optional <scenario>/workspace/ directory — the harness copies its contents into the temp cwd before the run (see workspace-edit). See the ACP snapshot tests RFC for the full design.
The sandbox variant (sandbox.cordis.yml)
The same server with the 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) plus @deepseek-ai/dsh-user-approval — the 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). Run it with pnpm run demo:sandbox-acp; Zed setup is the same as above with this command.
- 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. - One session config option is live (sandbox RFC § Per-session mode switching): a capable client shows a
Permissionsselect per session (request= workspace-write + ask,yolo= danger-full-access + never — the@deepseek-ai/dsh-permissionpreset table), and a switch is one log-onlypermission/presetevent written through to the two knob events, execution following the knobs; 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 variant loads no filesystem tools: they would bypass the bash sandbox.
Variant tests, in this example's suites: tests/escalation.e2e.ts — keyless, it boots the real sandbox.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; with a key and a usable runner, a scripted ACP client plays the human — the real model escalates a user-asserted denial, the client answers allow-once, and the retried write must land on disk. Four scenarios in tests/acp.snapshot.ts run against the variant's sandbox.cordis.snapshot.yml replay overlay under the sandbox header class: the keyless config-options exchange, the recorded permission-switching arc (that class's pinned header — one request→yolo preset switch, its approval prompt-section delta and "changed by the user" notice), 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 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).
MVP limitations
The bridge supports N concurrent sessions per connection, each in its own workspace cwd (RFC 011). Remaining limits: prompts support ACP's baseline text and resource_link blocks only, and additionalDirectories and mcpServers are rejected. Permission prompts (session/request_permission) are wired through the approval seam; the MAIN tree composes no ask-producing policy, so its tools run with the executor's full authority — the sandbox variant above is the composition that exercises the live prompt. See packages/ui/acp/README.md for the full contract.