-
Notifications
You must be signed in to change notification settings - Fork 3
Engine Acp
src/engine/acp implements Agent Client Protocol (ACP) v1 in two directions. The server side exposes Clio as an ACP agent to external frontends over stdio JSON-RPC. The delegation side starts external ACP peers as child processes and converts their protocol traffic into Clio delegation events. This area is part of the broader Engine and is covered by Contract tests.
-
serveClioAcpAgentinsrc/engine/acp/server.tsinstalls the ACP request handlers, prompt state, permission bridge, bus event forwarders, and session lifecycle controls. -
createAcpHandshakein the same file ownsinitialize,authenticate, andlogout, and projects the capability advertisement that strict clients can rely on. -
createStdioServerTransportinsrc/engine/acp/transport.tsaccepts inbound JSON-RPC frames from a client.StdioJsonRpcTransportandcreateStdioTransportspawn and communicate with an outbound ACP peer. -
startAcpDelegationRuninsrc/engine/acp/adapter.tsdrives one external ACP peer throughinitialize,session/new, optional model/thinking configuration, andsession/prompt. -
AcpToolMediatorinsrc/engine/acp/tool-mediator.tsanswers a peer'ssession/request_permissionby mapping the peer tool call onto Clio tool names and evaluating Clio safety policy. -
acpCommandControl,acpCommandCatalog, andinvokeAcpCommandinsrc/engine/acp/commands.tsexpose a fixed allowlist of slash commands to ACP clients. -
AcpEventMapperinsrc/engine/acp/event-mapper.tsconverts peersession/updateframes into Clio agent events for delegation. -
AcpRequestError,acpErrorMessage, and the fixed error constants insrc/engine/acp/errors.tskeep untrusted failure text out of the JSON-RPC channel. - Wire constants such as
ACP_MAX_STRING_BYTES,ACP_MAX_RAW_RECORD_BYTES, andACP_MAX_TOOL_CALL_ID_BYTESinsrc/engine/acp/types.tsbound client-rendered payloads.
The CLI entry point is runAcpCommand in src/cli/acp.ts. It parses --cwd and --permission-timeout, takes over stdout for JSON-RPC, and creates a createStdioServerTransport. When no --cwd is supplied, the CLI keeps the transport open and calls serveDeferredAcp from src/engine/acp/deferred-boot.ts; the first workspace request selects the boot root. When --cwd is supplied, the CLI enters the process eagerly and calls runClioCommand.
runClioCommand ultimately reaches the orchestrator in src/entry/orchestrator.ts. When options.acp is present, the orchestrator creates or accepts a transport, then calls serveClioAcpAgent with the chat loop, session contract, provider contract, settings projection, dispatch control, bus, tool registry, and command control. The orchestrator builds the command control with acpCommandControl, passing dispatch, bus, providers, and host callbacks such as runDoctor, submitOperatorNote, and submitTurn.
The handshake is installed before domain handlers. createAcpHandshake stores the initialization flag, tool-progress opt-in, enabled event kinds, and a workspace instance id. It intersects requested event kinds with the allowlist in ACP_FORWARDABLE_EVENT_KINDS and refuses malformed event opt-ins instead of silently accepting a subset.
serveClioAcpAgent then registers standard ACP methods. session/new requires initialization and authentication, binds the launch canonicalCwd, and refuses a second session while sessionCreated is true. It attaches client-declared stdio MCP servers when options.mcpCapabilities is present. session/prompt extracts text from content blocks through promptText, refuses a prompt while another prompt or streaming run is active, and either invokes a slash command if one is advertised as available or submits the text to options.chat.submit.
While a prompt is active, handleChatEvent maps Clio engine events to ACP session/update notifications:
-
text_deltabecomesagent_message_chunk, split byACP_MAX_CHUNK_BYTES. -
thinking_deltabecomesagent_thought_chunk. -
tool_execution_startbecomestool_callwith a boundedrawInputand optionallocations. -
tool_execution_endbecomes a terminaltool_call_update; duplicate terminal updates are dropped. -
message_endandagent_endmerge usage and apply the stop reason.
sequenceDiagram
participant Client
participant Transport as AcpJsonRpcPeerTransport
participant Server as serveClioAcpAgent
participant Chat as AcpServerChat
participant Registry as ToolRegistry
Client->>Transport: initialize
Transport->>Server: onRequest initialize
Server->>Client: agentCapabilities and authMethods
Client->>Transport: session/new
Server->>Server: bind canonicalCwd and one session
Client->>Transport: session/prompt
Server->>Chat: submit(promptText)
Chat->>Server: text_delta, tool_execution_start, message_end
Server->>Client: session/update frames
Registry->>Server: onPermissionRequired
Server->>Client: session/request_permission
Client-->>Server: allow-once, reject-once, or reject-and-stop
Beyond the standard protocol, _clio-coder/ methods expose controls supplied by the host. initialize advertises only the capabilities bound by the composition root; clients use that advertisement to enable controls.
| Control | Behavior |
|---|---|
| Session history and branches | Standard session/list and session/load discover and restore stored conversations. Extension methods expose the active tree, switch its turn, and fork a successor through the shared replay path. |
| Handoff | Prepare extracts a document for review without creating a successor. Commit seeds the reviewed document once. Conversation, branch, skills, or decisions changing during extraction or before commit invalidate the draft. Cancel discards it. |
| Fleet contracts | Preview compiles a named contract and presents its steps, resolved routes, command invocations, and budget. Run recompiles and requires the approved identity; a changed recipe, model route, command binding, plan, or budget requires another approval before dispatch. |
| Task and decision boards | Session board reads and explicit task/decision mutations use the domain stores. Completion notes remain distinct from verification evidence. |
| Context | The ledger method exposes chat context accounting. Allowlisted context commands provide recall, reset, and recovery through the shared host services. |
| Side questions and drafts | Out-of-turn questions and candidate drafts run beside the conversation. Failed provider answers stay in diagnostics; responses carry bounded host explanations. |
| Configuration and resources | Model and thinking selections, usage/quota, extensions, and library operations use the corresponding host controls. |
Prompt submission accepts text, bounded embedded text resources, and images when the host advertises image support. These inputs pass through the same prompt expansion used by the main chat loop.
installPermissionBridge subscribes to options.toolRegistry.onPermissionRequired. When a parked tool call needs approval, the bridge finds the wire id that the client has already rendered, reads that call's toolCallSnapshot, and sends session/request_permission. It offers exactly three option ids: allow-once, reject-once, and reject-and-stop.
-
allow-onceresumes the parked calls. -
reject-oncedenies the current request. -
reject-and-stopdenies and cancels the active prompt plus parked calls, because a released parked call could otherwise start another model request while cancellation is in flight. - A permission timeout emits a
permission_expiredresolution and fails the active prompt throughoptions.chat.cancel.
The bridge binds only to an open wire id that the client has seen; it does not mint ids for permission asks. This keeps the ask aligned with the tool_call frame the client rendered.
src/engine/acp/commands.ts projects the slash registry onto ACP through an allowlist, ACP_COMMAND_RULES. The module exports thirteen commands: mcp, doctor, share, archive, run, delegate, oracle, council, skill, context, tasks, memory, and export. Commands that are TUI-bound or absent from the allowlist are refused by name before parsing.
acpCommandCatalog derives the catalog from commandReference() rather than from a second table, so flags, positionals, subcommands, and closed value sets stay synchronized with the slash registry. invokeAcpCommand joins argv without quoting and refuses quotes, control characters, whitespace in non-final argv entries, oversized argv entries, and unsupported subcommands. Commands marked injectsUserTurn are refused while a prompt is active; the server tells the client to use _clio-coder/session/steer instead.
startAcpDelegationRun is the upstream entry point used by dispatch. src/domains/dispatch/extension.ts calls it after capacity admission and ledger resolution, forwarding the agent config, task, optional model and thinking level, system prompt, dynamic prompt messages, cwd, safety contract, readOnly flag, client version, and clocks.
The adapter creates a stdio peer transport with createStdioTransport, resolves {env:NAME} environment references for the child, then performs the ACP v1 sequence:
-
initializewithprotocolVersion: 1and empty client capabilities. -
session/newwith the delegation cwd. -
session/set_config_optionwhen the caller requests a model or thinking level that the peer offers and the peer has not already selected. -
session/promptwith the flattened system prompt, dynamic messages, and task.
Peer session/update frames are mapped by AcpEventMapper. Permission requests from the peer are handled by AcpToolMediator. The returned handle exposes the peer pid, an async event iterator, the final result promise, abort, kill, heartbeat stamps, and the tool call log snapshot.
AcpToolMediator.handle maps the peer's tool call into Clio tool names and then evaluates every mapped target through SafetyContract.evaluate. Mapping recognizes read, edit, delete, move, search, execute, and fetch shapes, and it infers some tool classes from rawInput when a peer omits ACP metadata.
The governance branches are:
-
clio-coder-policy: safety policy decides; anaskdisposition is denied because delegation has no interactive operator. -
agent-managed: the call is approved. -
deny-all: the call is denied. - unknown tool: denied unless the rawInput shape is inferred into a known Clio tool.
Approval never selects allow_always. It selects only allow_once. If the peer offers no allow_once, the approved call is rejected with a reason that records this rule. Denial prefers reject_once, then reject_always, then any reject* option. The mediator records requested/approved/denied counters and a tool call log.
-
One session per process.
sessionCreatedinsrc/engine/acp/server.tsprevents a secondsession/newwhile a session is bound. The contract test intests/contracts/acp-v1-basics.test.tsasserts the secondsession/newrejects with-32602. -
Workspace root binding. The server canonicalizes
options.cwd ?? process.cwd()and compares every session cwd against it.serveDeferredAcpbinds the first workspace request and later requests that name a different root fail with-32602. -
Initialization order. Workspace methods require
initialize.serveDeferredAcpalso refuses workspace binding after logout, which keeps the workspace unbound. -
Prompt exclusivity.
session/promptrefuses another prompt whilesession.activePromptis non-null oroptions.chat.isStreaming()is true. Configuration changes and safe settings patches are also refused during an active prompt. -
Wire cardinality. Text chunks are bounded by
ACP_MAX_CHUNK_BYTES, raw records byACP_MAX_RAW_RECORD_BYTES, strings byACP_MAX_STRING_BYTES, tool-call ids byACP_MAX_TOOL_CALL_ID_BYTES, and live tool-call ids byACP_MAX_LIVE_TOOL_CALLS. A 129th tool start setstoolCallLimitReachedand fails the prompt withmax_turn_requests. -
Error boundary. Provider prose and unclassified handler failures do not cross the JSON-RPC wire.
errors.tsdefines fixed messages:ACP_TURN_FAILED_MESSAGE,ACP_INTERNAL_ERROR_MESSAGE, andACP_METHOD_NOT_FOUND_MESSAGE. The v1 basics test asserts that an unclassified handler failure becomes code-32603and messageinternal error. -
Opt-in streams. Tool progress and forwarded bus events are not enabled by default. Tool progress requires the client to send
clio-coder/toolProgressversion 1 metadata. Forwarded events require the client to request kinds fromACP_FORWARDABLE_EVENT_KINDS.
-
New ACP method: add a
transport.onRequesthandler insideserveClioAcpAgent. UseassertParamKeysto reject unknown parameters, and namespace extension methods under_clio-coder/so strict ACP clients see only the stable protocol surface. -
New forwarded bus event: add the literal
BusChannelsvalue toACP_FORWARDABLE_EVENT_KINDS, then subscribe inserveClioAcpAgentand project payload fields throughsafeStoredIdentifier,safeStoredString, orsafeCountbefore sending_clio-coder/event. -
New operator command: add the command to
ACP_COMMAND_RULESinsrc/engine/acp/commands.ts, and add host requirements inCOMMAND_REQUIREMENTSorCONTEXT_REQUIREMENTSwhen the command needs a host callback. The name must already exist inBUILTIN_SLASH_COMMANDS; a module-level check throws on load if the allowlist names an unregistered command. -
New delegated ACP agent: configure a
DelegationAgentConfigfor dispatch, or callstartAcpDelegationRundirectly with agent command, args, task, cwd, and safety contract. -
New peer tool mediation: extend
mapToolCalland the mutation/path extraction helpers insrc/engine/acp/tool-mediator.ts. Unknown rawInput shapes default to denial unless they are inferred into a known Clio tool.
-
tests/contracts/acp-v1-basics.test.tscallsinitializewithprotocolVersion: 42andclientCapabilities: { auth: { terminal } }. It asserts the responseprotocolVersionis1, thatauthMethodscontains["auth", "login"]only when terminal auth is requested, thatauthenticatewith an unknown method rejects with-32602, and thatsession/newafterlogoutrejects with-32000. It also verifies that a secondsession/newrejects with-32602and that a prompt containing a resource link forwards the URI into the chat submission. -
tests/contracts/acp-permission-options.test.tsconstructsAcpToolMediatorwithcreateWorkerSafetyandtoolGovernance: "clio-coder-policy". For an approved read ofnotes.txt, it passes options includingallow_always,allow_once, andreject_once, then asserts the response selectsopt-allow_once. It also asserts that an approved call with noallow_onceoption is denied with a reason matchingno allow_once optionandnever selects allow_always. -
tests/contracts/acp-deferred-boot.test.tssendsinitialize, thensession/newwith a temporary root, thensession/newwith a different root. It asserts the first root binds the boot process, the later cwd mismatch rejects with-32602, and preboot logout keeps the workspace unbound. -
tests/contracts/acp-client-mcp.test.tscallssession/newwith a stdio MCP fixture, invokes the gateway capability, and asserts thatsession/closedetaches the client MCP server and removes its tools from the registry. -
tests/contracts/acp-peer-error.test.tsstreams a provider-style error envelope throughAcpEventMapper.reportedFailureand asserts thatstartAcpDelegationRunreturnsexitCode: 1,stopReason: "error", and a failure message that names the peer-reported HTTP status. -
tests/extended/acp-commands.test.tsasserts thatacpCommandCatalogcontains the thirteen allowlisted commands, that/contextand/tasksare exposed only through projected subcommands, that non-allowlisted names reject withcommand_not_exposed, and that/skillexpands throughparsePendingSkillRequestsbeforesubmitTurn.
- Stdout is JSON-RPC only. Put operator-facing detail in
diagnosticsso it can land on stderr; do not echo provider bodies, file contents, or unbounded error messages into responses. - Extensions must ride
_metakeys. Adding a top-level field to a standard response can break strict ACP clients. - The permission option set is closed. A client returning an option id outside
ACP_PERMISSION_OPTION_IDSis denied; do not add prefix matching such asstartsWith("allow"). - Wire ids are unique per prompt. A permission ask, progress frame, or terminal update must resolve through
openWireIdForor the snapshot map; minting a new id for an update would announce a call the client never started. - The command allowlist is a security boundary.
invokeAcpCommandmust refuse TUI-bound commands beforedispatchSlashCommandcan reach them, because those commands may dereference missing host members such askeyboardActions. - Deferred boot answers handshake methods before boot, but workspace binding must not begin until
initializesucceeds. Logout before boot must leave the workspace unbound. - ACP v1 has no error
stopReason. A failed prompt turn is signaled by throwingAcpRequestErrorfromsession/prompt, with the machine-readable code indetailand the original failure text in diagnostics. - Steering and user-turn commands are different paths. Commands that inject a user turn are refused while a prompt is active; use
_clio-coder/session/steerfor mid-run guidance.
Source and generation metadata
title: "Engine acp"
summary: "Agent Client Protocol v1 server and delegation client for Clio, including the stdio JSON-RPC transport, permission mediation, headless command catalog, and ACP peer lifecycle."
sources:
- "src/engine/acp/server.ts"
- "src/engine/acp/transport.ts"
- "src/engine/acp/adapter.ts"
- "src/engine/acp/tool-mediator.ts"
- "src/engine/acp/commands.ts"
- "src/engine/acp/types.ts"
- "src/engine/acp/event-mapper.ts"
- "src/engine/acp/errors.ts"
- "src/engine/acp/deferred-boot.ts"
- "src/cli/acp.ts"
- "src/entry/orchestrator.ts"
- "src/domains/dispatch/extension.ts"
symbols:
- "serveClioAcpAgent"
- "createAcpHandshake"
- "startAcpDelegationRun"
- "AcpToolMediator"
- "createStdioServerTransport"
- "createStdioTransport"
- "acpCommandControl"
- "AcpRequestError"
tests:
- "tests/contracts/acp-v1-basics.test.ts"
- "tests/contracts/acp-permission-options.test.ts"
- "tests/contracts/acp-deferred-boot.test.ts"
- "tests/contracts/acp-client-mcp.test.ts"
- "tests/contracts/acp-peer-error.test.ts"
- "tests/extended/acp-commands.test.ts"
invariants:
- "The ACP server answers initialize with protocolVersion 1 and advertises terminal authentication only when the client capability requests it."
- "A delegated ACP permission approval selects allow_once and never selects allow_always."
- "Unclassified handler failures on the ACP transport are serialized as JSON-RPC code -32603 with the fixed message internal error."
- "The ACP server hosts one session per process and refuses a second session/new while the slot is occupied."
validate:
- "node --import tsx --import ./tests/harness/tmp-root.ts --test tests/contracts/acp-v1-basics.test.ts"Clio Coder · Repository · Website · Documentation
Wiki v0.1 · Developing implementation reference · Source snapshot: 657dce13d. Authored architecture documents define the product contracts.
- Clio Coder GUI Client
- apps / clio-coder-gui
- Apps clio coder gui server
- Apps clio coder gui tests
- apps
- Architecture
- Command-line surfaces
- Core
- Domains agents
- Config Domain
- Context Domain
- Dispatch domain
- Domains evidence
- Domains extensions
- Domains gateway
- domains
- Domains interop
- Domains lifecycle
- Domains memory
- Middleware Domain
- Domains mux
- Domains observability
- Domains plugins
- Prompt Compiler
- Domains providers
- Domains quota
- Domains resources
- Domains safety
- Domains scheduling
- Domains session
- Vendored Tool Registry and Resolution
- Engine
- Engine acp
- Engine apis
- engine
- Entry point
- Interactive
- interactive
- Interactive overlays
- Interactive renderers
- clio-coder wiki
- Scripts
- Contract tests
- Tests extended
- tests
- Tools
- Tools data
- tools
- Tools verify
- Worker runtime