Chio/Docs

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

The ACP adapter is alpha. The JSON-RPC interceptor, filesystem and terminal guards, and unsigned audit-entry path are implemented today. Kernel-backed receipt signing and capability token checking are specified in the ACP kernel integration design and are rolling out behind the 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, and terminal/create before 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:

  1. 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.
  2. 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.
  3. 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

Most of a policy from the MCP wrap guide ports directly: the 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:

bash
$ 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.

FlagPurpose
--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-agentBundles 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:

bash
$ 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 --stdio

The proxy spawns the agent, wires stdio, and begins interposing on JSON-RPC traffic:

bash
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

The --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.

rendering…
Editor talks JSON-RPC to chio acp serve; chio routes by method, runs guards, forwards or blocks, and seeds an audit log + signed receipt on every tool_call it sees.

Method by method, here is what the interceptor does. Names match the AcpMethod discriminator the proxy parses from every request.

ACP methodWhat chio does
initialize, authenticateForward unchanged. chio records the session handshake but does not guard it.
session/new, session/load, session/listForward. Session identifiers are captured for later correlation with tool-call events.
session/promptForward. Prompt content may be scanned by the secret-pattern guard before it reaches the agent.
session/request_permissionInterpose. 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_fileGuarded by FsGuard. Path must match the allowed prefix set and not trip a forbidden-path pattern.
terminal/createGuarded 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_exitForwarded. 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.

acp-codeagent.yaml
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: 300

Two 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 restrict path_allowlist.write to 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.allow to the minimum set (build, test, and VCS), and use forbidden_patterns to 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:

acp-audit-entry.json
{
  "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:

  • toolCallId is the ACP protocol's own stable id for the event. It is stable acrossToolCall and its subsequent ToolCallUpdate status transitions, so you can fold related entries together downstream.
  • capabilityId takes the form acp-session:<session_id>. The chio cert acp command uses that prefix to pull every receipt that belongs to one session.
  • enforcementMode is cryptographically_enforced when a live capability check allowed the underlying operation before the event was forwarded, or audit_only when 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:

bash
$ 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 denied

Differences from the MCP Adapter

At a glance:

DimensionMCP adapterACP adapter
ShapeRequest/response tool callsBidirectional JSON-RPC message stream
Primary interception pointtools/call requestTyped AcpMethod per message + session/update observation
Tool discoveryProxied tools/listSession-time capability advertisement, typically implicit
Guard surfacemcp-tool, path-allowlist, forbidden-path, shell, egress, secret, patch, velocityfs-guard, terminal-guard, permission-guard, tool-access, secret, velocity
Receipt triggerEvery tool-call decisionEvery tool-call event observed in a session update, plus guarded fs and terminal decisions
Capability-ID prefixmcp-server:<id>acp-session:<id>
Standalone (no kernel) modeProduces unsigned decisions if seed absentProduces policy_enforced_but_unsigned audit entries and flags the session ineligible for cross-protocol certification
Transportstdio today, HTTP edge availablestdio 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.

SymptomLikely causeFix
Editor shows access denied on every fs/read_text_fileNo --allow-path prefix supplied, or the project root is symlinked outside the allowed prefixPass the canonical absolute path via --allow-path, resolving symlinks first
Path traversal error in the logAgent requested a path containing .. segments that escape the allowed prefixThis is working as intended. If legitimate, normalize the path before sending
terminal/create denied for a command that should workCommand not in shell_commands.allow or the binary was invoked through a shell wrapperAdd the command to the policy or adjust the agent to call the binary directly
Audit entries appear but have enforcementMode: audit_onlyCapability checker is disabled or the session has no bound capability tokenSupply --seed and enable the kernel-backed checker. In alpha, this requires building with the kernel-integrated constructor
Session flagged policy_enforced_but_unsignedProxy is running in UnsignedCompatibility mode. Policy was enforced but no receipt signer was availableExpected for standalone mode. For compliance-grade runs, switch to required mode by supplying a seed
Upstream agent exits immediatelyMisparsed -- boundary, or the agent itself expects arguments the proxy did not forwardCheck the log line spawning ACP agent and run the same command outside chio to confirm it starts cleanly

Running the hello-acp example

The 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:

PropertyWhat chio adds
Policy enforcementfs-guard, terminal-guard, permission-guard, tool-access, secret scanning, velocity
AuditabilityEvery tool-call event observed in session/update recorded with a SHA-256 content hash
AttestationAudit entries promotable to signed chio receipts; sessions carry explicit attestation status so compliance consumers can reject gaps
TransparencyEditor and agent keep speaking ACP unchanged; the proxy is a subprocess boundary with no client SDK to adopt
Cross-protocol joinsACP 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.
Wrap an ACP Server · Chio Docs