Wrap an ACP Server
chio sits between an editor or IDE and an ACP coding agent the same way it sits between an agent and an MCP tool server. The agent runs unmodified as a subprocess, chio intercepts every JSON-RPC message flowing in either direction, and policy-critical methods are guarded before they reach the agent or the filesystem. This guide walks through the full lifecycle: picking an upstream ACP agent, writing a policy that matches ACP's message surface, running the adapter, and reading the audit log.
Adapter status: alpha
start_with_kernel constructor. Flags described below match the parallel MCP wrap guide so the two flows feel like siblings. Where a feature is gated, this page labels it.Why Wrap ACP
ACP is the Agent Client Protocol used by coding agents and IDE sidecars: editors talk to agents over JSON-RPC 2.0 on stdio, and the agent exposes methods like session/prompt, session/request_permission, fs/read_text_file, fs/write_text_file, and terminal/create. It is a message-based protocol, not a tool-call-based one. The agent drives the session and emits session/update notifications that carry tool-call events as they happen.
That shape matters for governance. With MCP, chio intercepts a symmetric request-response pair for every tool use. With ACP, chio intercepts ongoing bidirectional traffic and promotes the observed tool-call events inside session updates into audit entries. Wrapping an ACP agent with chio gives you:
- A boundary for the IDE to trust. The editor still speaks ACP. The agent still speaks ACP. chio is transparent on the wire.
- Filesystem and terminal guards that apply to
fs/read_text_file,fs/write_text_file, andterminal/createbefore the agent sees them. - An audit trail of tool-call events observed in session updates, each with a SHA-256 content hash and the session, tool-call, and server identifiers already filled in.
- A promotion path to signed receipts so unsigned ACP audit entries can be cross-linked with MCP and A2A receipts in a single compliance query.
What You Need
Three things, mirroring the MCP flow:
- An upstream ACP agent. Any agent that implements ACP over stdio JSON-RPC works. Examples below use a Claude-style coding agent binary invoked as
claude-code, but the adapter is agent-agnostic: the proxy spawns whatever command you give it and pipes JSON-RPC through. - A policy file describing which paths the agent can read or write, which shell commands it can launch through ACP's terminal surface, and which tool-call kinds are allowed.
- A signing seed if you want the ACP audit entries promoted into signed chio receipts. Without a seed the proxy still enforces guards and produces unsigned audit entries. With a seed the adapter runs in attestation-required mode and emits full receipts or an explicit attestation-gap artifact.
Reuse your MCP policy skeleton
path_allowlist, forbidden_paths, shell_commands, and secret_patterns sections are identical. Only the tool-access shape and the velocity window differ.The Wrap Command
The canonical invocation mirrors chio mcp serve:
$ chio acp serve --policy <file> --server-id <id> --preset code-agent \
-- <upstream-command> [args...]Each flag does the same job it does for MCP, with one ACP-specific detail: the --server-id is baked into every audit entry and receipt as the server_id field, and ACP session compliance certificates use the acp-session:{session_id} capability-ID prefix to scope evidence to a single session.
| Flag | Purpose |
|---|---|
--policy <file> | HushSpec policy YAML. Guards map to its path_allowlist, forbidden_paths, shell_commands, and tool_access rules. |
--server-id <id> | Stable identity string written to every audit entry and receipt. Used by compliance queries to join ACP evidence with MCP and A2A receipts. |
--preset code-agent | Bundles the deny-by-default guard set appropriate for coding agents: path allowlist enforcement, forbidden-path patterns, shell-command allowlisting, and secret scanning on session updates. |
--allow-path <prefix> | Append a path prefix to the ACP filesystem guard allowlist. Mirrors AcpProxyConfig::with_allowed_path_prefix. May be passed multiple times. |
--allow-command <name> | Append a command to the terminal guard allowlist (for terminal/create). Mirrors with_allowed_command. Repeatable. |
--seed <path> | Ed25519 signing seed. With a seed, tool-call events are promoted into signed receipts; without it, the proxy runs in the unsigned-compatibility path and labels events policy_enforced_but_unsigned. |
--bind <addr> | Planned. Exposes the wrapped ACP agent over a line-delimited JSON-RPC socket instead of stdio, for containerized deployments. |
-- <upstream-command> | Everything after -- is the agent subprocess command line. |
A concrete invocation against a coding agent with a project-scoped path allowlist and a short command allowlist:
$ chio acp serve --policy ./acp-codeagent.yaml --server-id srv-coder \
--preset code-agent \
--allow-path /home/dev/project \
--allow-command cargo \
--allow-command git \
--seed ./kernel.seed \
-- claude-code --stdioThe proxy spawns the agent, wires stdio, and begins interposing on JSON-RPC traffic:
INFO loaded policy for ACP edge
policy_path: ./acp-codeagent.yaml
server_id: srv-coder
guards: fs-guard, terminal-guard, permission-guard, tool-access
INFO spawning ACP agent
command: claude-code --stdio
transport: json-rpc/stdio
paths: /home/dev/project
commands: cargo, git
INFO attestation mode: required
signer: kernel-ed25519 (seed loaded)
INFO chio ACP edge ready
intercepting session/*, fs/*, terminal/*Keep the server ID stable
--server-id also forms the acp-session:{session_id} capability-ID prefix that compliance certificates depend on. Rotate it only when you rotate the signing seed; otherwise you break the evidence chain for sessions recorded under the old identity.How ACP Flows Through Chio
The proxy treats the wire as a symmetric JSON-RPC stream. The editor writes requests and notifications into chio; chio reads each one, routes it by method, and decides whether to forward, block, or forward-with-attestation. Agent-to-editor messages are handled the same way in reverse.
Method by method, here is what the interceptor does. Names match the AcpMethod discriminator the proxy parses from every request.
| ACP method | What chio does |
|---|---|
initialize, authenticate | Forward unchanged. chio records the session handshake but does not guard it. |
session/new, session/load, session/list | Forward. Session identifiers are captured for later correlation with tool-call events. |
session/prompt | Forward. Prompt content may be scanned by the secret-pattern guard before it reaches the agent. |
session/request_permission | Interpose. chio can auto-deny options that contradict policy and, in the kernel-integrated path, consult capability tokens so the user is not prompted for work that would fail anyway. |
fs/read_text_file, fs/write_text_file | Guarded by FsGuard. Path must match the allowed prefix set and not trip a forbidden-path pattern. |
terminal/create | Guarded by TerminalGuard. Command must appear in the allow list; args are scanned against shell forbidden patterns. |
terminal/kill, terminal/release, terminal/output, terminal/wait_for_exit | Forwarded. Lifecycle control for an already-allowed terminal is not guarded again. |
session/update (notification) | Observed. Tool-call events (ToolCall, ToolCallUpdate) are parsed, hashed, and emitted as audit entries or signed receipts. |
Guard failures are returned as JSON-RPC errors on the original request id, using the server error code -32000 with a message that names the guard. The editor surfaces those as ordinary tool errors, so no client-side changes are required.
Policy Shape for ACP
The policy below is a sibling of the MCP filesystem policy. The guard names are the same. The differences are ACP-specific: tool access is stated in terms of ACP method and tool-call kind rather than flat MCP tool names, and terminal commands are modeled as first-class allowlist entries because terminal/create is a dedicated ACP method.
hushspec: "0.1.0"
name: acp-codeagent
rules:
# ACP method + tool-call kind access. The proxy reads both the
# method name and, for session/update events, the reported kind.
tool_access:
enabled: true
default: block
allow_methods:
- session/new
- session/prompt
- session/update
- session/request_permission
- fs/read_text_file
- fs/write_text_file
- terminal/create
- terminal/kill
- terminal/output
- terminal/wait_for_exit
allow_kinds:
- read
- edit
- execute
# Path allowlist is enforced by FsGuard for both fs/* methods.
path_allowlist:
enabled: true
read:
- "/home/dev/project/**"
write:
- "/home/dev/project/src/**"
- "/home/dev/project/tests/**"
patch: []
forbidden_paths:
enabled: true
patterns:
- "**/.env"
- "**/.env.*"
- "**/*.pem"
- "**/*.key"
- "**/.ssh/**"
- "**/.git/config"
exceptions: []
# TerminalGuard: only allow explicitly listed commands.
shell_commands:
enabled: true
allow:
- cargo
- git
- rg
forbidden_patterns:
- "(?i)\\bsudo\\b"
- "(?i)\\bcurl\\b.*\\b(sh|bash|zsh)\\b"
# Scan prompts, tool-call params, and tool results.
secret_patterns:
enabled: true
# Velocity window sized for an interactive IDE session.
velocity:
enabled: true
max_invocations: 600
window_seconds: 300Two ACP-specific policy patterns are worth calling out:
- Split read vs write paths. ACP coding agents typically need a wide read surface and a narrow write surface. Put the whole project under
path_allowlist.read, but restrictpath_allowlist.writeto source and test directories. The FsGuard enforces the split per method. - Deny-by-default terminal with a short allow list. ACP agents love to shell out. Set
shell_commands.allowto the minimum set (build, test, and VCS), and useforbidden_patternsto stop pipe-to-shell tricks from making it through.
For the full policy grammar and how to test each guard offline with chio check, see Write a Policy.
Receipts for ACP Messages
Every session/update notification that carries a tool-call event produces an audit entry. The entry records the tool-call id, title, kind, status, session id, server id, a Unix timestamp, and a SHA-256 hex digest of the canonical JSON of the originating event. When a signing seed is loaded, the entry is also promoted into a chio receipt signed with the kernel's Ed25519 key.
The audit-entry shape the proxy emits before signing looks like this:
{
"toolCallId": "tc_01HZ7Q8YB3N8G0XHB0T6KQ4F9R",
"title": "edit src/main.rs",
"kind": "edit",
"status": "completed",
"sessionId": "sess_01HZ7Q8YB3N8G0XHB0T6KQ4F9R",
"timestamp": "1744993921",
"serverId": "srv-coder",
"contentHash": "8f3b2a4e9c1d0f7b6a5e2c3d4f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a",
"capabilityId": "acp-session:sess_01HZ7Q8YB3N8G0XHB0T6KQ4F9R",
"authorizationReceiptId": "rcpt-acp-01HZ7Q8YB3N8G0XHB0T6KQ4F9R",
"enforcementMode": "cryptographically_enforced"
}A few fields are ACP-specific:
toolCallIdis the ACP protocol's own stable id for the event. It is stable acrossToolCalland its subsequentToolCallUpdatestatus transitions, so you can fold related entries together downstream.capabilityIdtakes the formacp-session:<session_id>. Thechio cert acpcommand uses that prefix to pull every receipt that belongs to one session.enforcementModeiscryptographically_enforcedwhen a live capability check allowed the underlying operation before the event was forwarded, oraudit_onlywhen the proxy merely observed the event.
When the kernel-backed signer is active, the audit entry is wrapped into a ChioReceipt with metadata.protocol = "acp" and metadata.session_id set, so it lands in the same receipt store as MCP and A2A receipts. For the full receipt format, verification procedure, and the attestation-gap shape that appears when signing cannot complete, see Receipts.
Tail the audit log for a running session:
$ chio receipt list --tool-server srv-coder --protocol acp
RECEIPT LOG (4 entries)
#1 2026-04-18T14:11:03Z ALLOW fs/read_text_file
session: sess_01HZ7Q8YB...
tool_call: tc_01HZ7Q8YB...
enforcement: cryptographically_enforced
signature: ed25519:a3b4c5d6...
#2 2026-04-18T14:11:05Z ALLOW fs/write_text_file
session: sess_01HZ7Q8YB...
path: /home/dev/project/src/main.rs
signature: ed25519:e7f8a9b0...
#3 2026-04-18T14:11:09Z DENY terminal/create
session: sess_01HZ7Q8YB...
guard: terminal-guard
reason: command 'rm' not in allow list
signature: ed25519:c1d2e3f4...
#4 2026-04-18T14:11:12Z ALLOW session/update (tool_call: edit)
session: sess_01HZ7Q8YB...
content: 8f3b2a4e9c1d0f7b...
signature: ed25519:d9e0f1a2...
4 entries, 3 allowed, 1 deniedDifferences from the MCP Adapter
At a glance:
| Dimension | MCP adapter | ACP adapter |
|---|---|---|
| Shape | Request/response tool calls | Bidirectional JSON-RPC message stream |
| Primary interception point | tools/call request | Typed AcpMethod per message + session/update observation |
| Tool discovery | Proxied tools/list | Session-time capability advertisement, typically implicit |
| Guard surface | mcp-tool, path-allowlist, forbidden-path, shell, egress, secret, patch, velocity | fs-guard, terminal-guard, permission-guard, tool-access, secret, velocity |
| Receipt trigger | Every tool-call decision | Every tool-call event observed in a session update, plus guarded fs and terminal decisions |
| Capability-ID prefix | mcp-server:<id> | acp-session:<id> |
| Standalone (no kernel) mode | Produces unsigned decisions if seed absent | Produces policy_enforced_but_unsigned audit entries and flags the session ineligible for cross-protocol certification |
| Transport | stdio today, HTTP edge available | stdio today, line-based JSON-RPC; HTTP edge planned |
The practical upshot: MCP guards are evaluated at decision time, so every allow or deny is signed in line with the tool call. ACP guards are evaluated at decision time too, but the richest signal comes after the fact from session/update events. An ACP session produces a fan of receipts tied together by session_id, which is why ACP compliance certificates are session-scoped.
Troubleshooting
Common failure modes and how to read them from the proxy log.
| Symptom | Likely cause | Fix |
|---|---|---|
Editor shows access denied on every fs/read_text_file | No --allow-path prefix supplied, or the project root is symlinked outside the allowed prefix | Pass the canonical absolute path via --allow-path, resolving symlinks first |
| Path traversal error in the log | Agent requested a path containing .. segments that escape the allowed prefix | This is working as intended. If legitimate, normalize the path before sending |
terminal/create denied for a command that should work | Command not in shell_commands.allow or the binary was invoked through a shell wrapper | Add the command to the policy or adjust the agent to call the binary directly |
Audit entries appear but have enforcementMode: audit_only | Capability checker is disabled or the session has no bound capability token | Supply --seed and enable the kernel-backed checker. In alpha, this requires building with the kernel-integrated constructor |
Session flagged policy_enforced_but_unsigned | Proxy is running in UnsignedCompatibility mode. Policy was enforced but no receipt signer was available | Expected for standalone mode. For compliance-grade runs, switch to required mode by supplying a seed |
| Upstream agent exits immediately | Misparsed -- boundary, or the agent itself expects arguments the proxy did not forward | Check the log line spawning ACP agent and run the same command outside chio to confirm it starts cleanly |
Running the hello-acp example
examples/hello-acp walkthrough ships a minimal ACP edge, a run-edge.sh entrypoint, and a smoke.sh that exercises session/list_capabilities, tool/invoke, deferred tool/stream, and tool/resume. Use it as a sandbox before pointing the adapter at a real coding agent.Summary
Wrapping an ACP agent with chio gives you:
| Property | What chio adds |
|---|---|
| Policy enforcement | fs-guard, terminal-guard, permission-guard, tool-access, secret scanning, velocity |
| Auditability | Every tool-call event observed in session/update recorded with a SHA-256 content hash |
| Attestation | Audit entries promotable to signed chio receipts; sessions carry explicit attestation status so compliance consumers can reject gaps |
| Transparency | Editor and agent keep speaking ACP unchanged; the proxy is a subprocess boundary with no client SDK to adopt |
| Cross-protocol joins | ACP receipts share the same receipt store and key material as MCP and A2A receipts; one query covers all three |
Next Steps
- Wrap an MCP Server · the sibling guide for tool-call-based servers. Worth reading back-to-back with this one.
- Write a Policy · full HushSpec reference for the guards mentioned above.
- Receipts · receipt format, verification, and the attestation-gap states ACP introduces.
- Native Tool Server · when you want the agent to speak chio directly instead of sitting behind an ACP adapter.