ACP Protocol
Agent Client Protocol — integrate external CLIs (Claude Code, Codex, Gemini) as ACP adapters; AcpAdapterManager owns the session lifecycle, the process-isolation kernel backs sandbox safety.
The acp module implements the Agent Client Protocol (ACP), integrating external CLI tools as first-class ACP adapters.
The historical
AcpManager::create_session / send_message / stream_responsesurface has been replaced byAcpAdapterManager(src/acp/manager/). Session lifecycle, persistence, and re-authorization all live under themanager/submodule.
Overview
ACP manages external CLI tools as ACP adapters, supporting two execution modes:
NativeAcp— Full ACP protocol over persistent stdio (Gemini CLI--acp). Lazy-start sessions that are automatically respawned when the child dies.Oneshot— Fresh process per prompt (Claude Code--print, Codexexec). No persistent session.
Supported adapters: Claude Code, Codex (OpenAI), Gemini CLI, plus any CLI harness exposed via acp/adapters/custom.rs / acp/adapters/generic.rs.
Architecture
src/acp/
├── mod.rs # entry; re-exports AcpAdapterManager + AcpSession + AcpSessionEvent
├── adapter.rs # AcpAdapter trait + AdapterMode (NativeAcp | Oneshot)
├── adapters/ # concrete adapters
│ ├── mod.rs
│ ├── custom.rs # custom CLI configuration
│ └── generic.rs # generic harness
├── protocol.rs # ACP message protocol
├── transport.rs # communication layer
├── output_format.rs # output formatting
├── incoming.rs # inbound message parsing
├── session.rs # AcpSession + AdapterConfig + PersistedAcpSession + CancelHandle
├── manager/ # AcpAdapterManager
│ ├── mod.rs # struct + SessionSnapshot
│ ├── harness_admin.rs # harness CRUD / queries / hooks / restore_sessions / list_sessions
│ ├── lifecycle.rs # ensure_session / prompt / cancel / control RPCs / shutdown_*
│ ├── persistence.rs # disk persistence + wire_persistence boot helper
│ └── session_key.rs # SessionKey + path canonicalization
├── mock_server.rs # test mock
└── tests.rsCore components
AcpAdapterManager (src/acp/manager/mod.rs)
pub struct AcpAdapterManager {
pub(super) adapters: RwLock<HashMap<String, Arc<dyn AcpAdapter>>>,
pub(super) configs: RwLock<HashMap<String, AcpAdapterEntry>>,
pub(super) sessions: /* active NativeAcp sessions, keyed by (harness_id, cwd) */,
}
pub struct SessionEntry {
pub session: Arc<AsyncMutex<AcpSession>>,
pub cancel: CancelHandle, // writes session/cancel directly to the child's stdin
}Supports:
- Runtime dynamic harness registration / unregistration (
harness_admin) - Boot-time
wire_persistenceto restore sessions from disk - Concurrent prompts in the same
(harness, cwd)serialize via the inner mutex; different keys progress in parallel canceldoes NOT need to hold the session mutex — can interrupt an in-flight prompt
AcpAdapter trait (src/acp/adapter.rs)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdapterMode {
NativeAcp, // full ACP protocol (initialize → session/new → session/prompt)
Oneshot, // fresh process per prompt, read stdout, done
}
#[async_trait]
pub trait AcpAdapter: Send + Sync {
fn id(&self) -> &str; // "claude-code" / "codex" / "gemini"
fn is_available(&self) -> bool;
async fn execute(&self, cwd: &str, args: &[String]) -> Result<AcpOutput>;
/* … */
}AdapterMode::as_str gives a stable wire label (native_acp / oneshot), aligned with serde rename.
AcpSession (src/acp/session.rs)
pub struct AdapterConfig { /* command / args / env / mode */ }
pub struct PersistedAcpSession { /* harness_id / acp_session_id / cwd / session_name / created_at */ }
pub struct AcpSession { /* harness_id / acp_session_id / cwd / adapter */ }
pub struct CancelHandle; // cloneable; writes session/cancel directly to the child's stdinAcpSession::harness_id() / acp_session_id() / is_alive() / cancel_handle() are the common handles.
Session events
pub enum AcpSessionEvent {
Created { harness_id, acp_session_id, cwd, session_name: Option<String> },
Removed { harness_id, cwd, session_name: Option<String> },
}
pub type PersistenceHook = Arc<dyn Fn(AcpSessionEvent) + Send + Sync>;session_name is None for the default unnamed session; Some(name) for parallel named sessions in the same (harness, cwd) slot — mirrors acpx's -s backend / -s frontend shape. PersistenceHook covers disk persistence; the separate live-broadcast AcpSessionsChangedNotifier drives acp.sessions.changed push.
Modes of operation
Tool Mode
In Tool Mode, the LLM dispatches read-only operations through ACP:
LLM ──> Tool Call ──> ACP Adapter ──> External CLI
(read-only) (claude-code read)Agent Mode
In Agent Mode, the user converses directly with the external tool:
User ──> Aleph ──> AcpAdapterManager ──> External CLI
(full read/write)RPC surface (wired at startup by register_acp_handlers)
| Group | Methods |
|---|---|
| Adapter CRUD | acp.create, acp.delete, acp.get, acp.list, acp.update |
| Adapter presets | acp.presets, acp.presets_meta, acp.test, acp.set_enabled |
| Sessions | acp.sessions_list, acp.sessions_cancel, acp.sessions_shutdown |
Wiring lives in src/bin/aleph-server/commands/start/builder/handlers/agents.rs::register_acp_handlers (sibling of register_cron_handlers / register_heartbeat_handlers).
Configuration
[acp]
enabled = true
default_adapter = "claude-code"
[acp.adapters.claude-code]
command = "claude"
args = ["--output", "json"]
[acp.adapters.codex]
command = "codex"
args = []Process-isolation kernel
ACP sessions are backed by the process-isolation kernel — sandbox restricted-token / job-object / AppContainer / integrity-level / SID·ACL syscalls are issued in-place at spawn (R1 carve-out).
See Also
- A2A — Agent-to-Agent protocol for internal agents
- Tools — Built-in tool system
- ACP Concept — full concept
- Sandbox — isolation mechanisms
- Gateway RPC
acp.*
A2A Protocol
A2A protocol adapter: AgentCard / Task / Message domain, port traits, A2AService sub-agent delegation, CardRegistry / SmartRouter / LlmMatcher.
ClawHub
Historical reference: ClawHub survives in the Hub catalog as a `via: "clawhub"` provenance label; the `src/clawhub/` module has been removed.