Aleph
Architecture

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_response surface has been replaced by AcpAdapterManager (src/acp/manager/). Session lifecycle, persistence, and re-authorization all live under the manager/ 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, Codex exec). 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.rs

Core 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_persistence to restore sessions from disk
  • Concurrent prompts in the same (harness, cwd) serialize via the inner mutex; different keys progress in parallel
  • cancel does 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 stdin

AcpSession::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)

GroupMethods
Adapter CRUDacp.create, acp.delete, acp.get, acp.list, acp.update
Adapter presetsacp.presets, acp.presets_meta, acp.test, acp.set_enabled
Sessionsacp.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

On this page