ACP Protocol
ACP (Agent Client Protocol) bridge that manages external CLI agents (Claude Code, Codex, Gemini) as Aleph subagents.
The acp module implements the Agent Client Protocol (ACP) bridge — Aleph acts as an ACP client that spawns and drives external ACP-capable CLI tools (Claude Code, Codex, Gemini CLI) as subagents. The bridge runs each external agent as a child process and talks to it over NDJSON-encoded JSON-RPC 2.0 on stdin/stdout.
Design Philosophy
ACP integration follows three principles:
- Subprocess isolation — Each ACP harness runs in its own OS process, owned by Aleph. A misbehaving agent cannot affect peers.
- Two execution modes —
NativeAcpfor agents that speak full persistent ACP (Gemini CLI--acp);Oneshotfor agents that complete in a single CLI invocation (Claude Code--print, Codexexec). - Crash-recoverable sessions — Active sessions are snapshotted to
~/.aleph/data/acp_sessions.jsonand restored at boot viawire_persistence.
Protocol Overview
ACP is JSON-RPC 2.0 framed as newline-delimited JSON. Aleph sends requests on the child's stdin and reads responses on stdout:
┌─────────────┐ NDJSON / JSON-RPC 2.0 ┌─────────────┐
│ Aleph │ ──────────────────────────────────────▶ │ External │
│ ACP client │ initialize → session/new → session/... │ CLI agent │
│ │ ◀────────────────────────────────────── │ (claude, │
│ │ responses + session/update notifications │ codex, │
│ │ │ gemini) │
└─────────────┘ └─────────────┘Core methods Aleph sends
| Method | Purpose |
|---|---|
initialize | Negotiate protocol version + capabilities (Aleph advertises fs.readTextFile, fs.writeTextFile, session.requestPermission). |
session/new | Open a new ACP session bound to a cwd. |
session/prompt | Send a prompt; streams chunks via AcpChunkCallback. |
session/cancel | Cooperative cancel; written to stdin via CancelHandle so it does not block on the per-session mutex held by an in-flight prompt. |
fs/read_text_file, fs/write_text_file | Bidirectional — the agent can ask Aleph to read/write files under the project root. |
session/request_permission | Bidirectional — the agent can ask the user for permission before a sensitive action. |
The transport (src/acp/transport.rs) wraps the child's stdin/stdout in a background reader task that demultiplexes responses and notifications onto an mpsc channel, and services inbound fs/* + session/request_permission requests through IncomingHandler (src/acp/incoming.rs).
Execution Modes
pub enum AdapterMode {
/// Persistent stdio ACP subprocess (e.g. Gemini CLI `--acp`).
NativeAcp,
/// One CLI invocation per prompt (e.g. Claude `--print`, Codex `exec`).
Oneshot,
}NativeAcp— Aleph keeps the child alive across prompts in a slot keyed by(harness_id, cwd)(SessionKey). Lazy-start, auto-respawn on death. Persistent stdio means a long session can stream incremental chunks and surfacefs/*requests mid-prompt.Oneshot— Each prompt spawns a fresh process; Aleph reads its full stdout and tears down. Cheaper setup, but no incremental streaming and no mid-prompt inbound requests.
A TieredAuthenticator-style tier system does not apply at the Aleph side; authentication to the external CLI is whatever the harness itself enforces.
Preset Adapters
The acp module ships built-in presets for the most common external CLIs (src/acp/adapters/):
| Preset id | Display name | Default mode | Notes |
|---|---|---|---|
claude-code | Claude Code | Oneshot | Uses claude --print .... |
codex | Codex | Oneshot | Uses codex exec .... |
gemini | Gemini CLI | NativeAcp | Uses gemini --acp. |
Additional custom harnesses can be registered through acp.create and persist alongside the presets in the same config store.
Harness Lifecycle RPCs
The gateway exposes a full CRUD + ops surface for harnesses and live sessions (src/gateway/handlers/acp_config.rs):
Harness configuration
| Method | Description |
|---|---|
acp.list | List all ACP harnesses (presets + custom). |
acp.get | Get a single harness by id. |
acp.create | Register a new custom harness. |
acp.update | Update an existing harness configuration. |
acp.delete | Remove a custom harness (presets cannot be deleted). |
acp.test | Probe the harness executable (executable --version, 5s timeout) and report success + duration. |
acp.set_enabled | Enable or disable a harness. |
acp.presets | List all built-in harness presets. |
acp.presets_meta | Static metadata for the preset catalogue (display name, executable, supported modes). |
Live session pool
| Method | Description |
|---|---|
acp.sessions.list | Enumerate live NativeAcp sessions. |
acp.sessions.cancel | Send session/cancel to a live session without blocking on the per-session prompt mutex. |
acp.sessions.shutdown | Shut a session down and remove it from the pool. |
Mutations publish acp.sessions.changed on the gateway event bus so the panel drops polling.
Process-Isolation Kernel
The Aleph server (the aleph-server binary) is the spawner for ACP subprocesses. When a child harness is launched, the spawn path runs through the same process-isolation kernel the daemon uses for its own children — restricted tokens / job objects / AppContainer / integrity level / SID·ACL syscalls are issued in place at spawn. This is a deliberate carve-out from R1 (Brain / Limb separation): the ACP subprocess is a separate OS process from the moment of Command::spawn, so the kernel-level isolation replaces the harness-level isolation that the rest of the system enforces. See Architectural Redlines R1 Process-Isolation Kernel carve-out.
Integration Points
ACP integrates with:
- Gateway —
extensions.*is not ACP; ACP harness CRUD isacp.*(acp.list,acp.create,acp.update,acp.delete,acp.test,acp.set_enabled,acp.presets,acp.presets_meta,acp.sessions.list,acp.sessions.cancel,acp.sessions.shutdown). - Spawner / SubAgent — The
acp_memberbuiltin tool wraps a configured harness as a team member; theacp_delegatetool dispatches one-shot tasks to a named harness. - Event bus —
acp.sessions.changedis published on every pool mutation. - Persistence —
~/.aleph/data/acp_sessions.jsonsnapshots live sessions and is replayed at boot.
Code Location
src/acp/mod.rs— Module entry point,AcpSessionEvent,PersistenceHook,GatewayChangeHook,AcpChunkCallbacksrc/acp/adapter.rs—AcpAdaptertrait,AdapterMode(NativeAcp/Oneshot)src/acp/adapters/— Built-in presets:claude_code,codex,gemini,custom,genericsrc/acp/manager/—AcpAdapterManager(harness_admin,lifecycle,persistence,session_key)src/acp/protocol.rs— JSON-RPC request/response/notification typessrc/acp/transport.rs—StdioTransport(NDJSON framing, background reader,CancelHandle)src/acp/session.rs—AcpSession,AdapterConfig,PersistedAcpSessionsrc/acp/incoming.rs—IncomingHandler(servicesfs/*+session/request_permissionfrom the agent)src/acp/output_format.rs—OutputFormat(text/markdown/json shaping for the model's view)src/gateway/handlers/acp_config.rs—acp.*RPC handlers
See Also
- Agent Runtime — How subagents are spawned and managed
- Extensions Store — Unified catalog for Skills / Plugins / MCP (distinct from ACP, which is for external CLI harnesses)
- Multi-Agent System — How ACP harnesses fit as team members