--- title: ACP (Agent Client Protocol) --- # ACP (Agent Client Protocol) > **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex) as child processes instead of using HTTP APIs. This gives you "CLI-as-backend" transport. --- ## What Is ACP? ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. Instead of intercepting HTTP API calls to AI providers, ACP **spawns CLI agents as child processes** and feeds prompts through their native interface. ### Why Use ACP? | Benefit | Description | | ---------------------- | ------------------------------------------ | | **No API keys needed** | Uses your existing CLI authentication | | **Native protocol** | Uses each CLI's native input/output format | | **Auto-discovery** | Detects installed CLIs on your system | | **14 built-in agents** | Pre-configured for popular CLI tools | | **Custom agents** | Add your own CLI tools via settings | | **Process management** | Handles lifecycle (spawn, send, kill) | --- ## Supported CLI Agents ACP supports **14 built-in CLI agents** out of the box: | Agent ID | Display Name | Binary | Protocol | | ------------- | ------------------ | ------------- | -------- | | `codex` | OpenAI Codex CLI | `codex` | stdio | | `claude` | Claude Code CLI | `claude` | stdio | | `goose` | Goose CLI | `goose` | stdio | | `openclaw` | OpenClaw | `openclaw` | stdio | | `aider` | Aider | `aider` | stdio | | `opencode` | OpenCode | `opencode` | stdio | | `cline` | Cline | `cline` | stdio | | `qwen-code` | Qwen Code | `qwen` | stdio | | `forge` | ForgeCode | `forge` | stdio | | `amazon-q` | Amazon Q Developer | `q` | stdio | | `interpreter` | Open Interpreter | `interpreter` | stdio | | `cursor-cli` | Cursor CLI | `cursor` | stdio | | `warp` | Warp AI | `warp` | stdio | ### Custom Agents You can add your own CLI agents via settings. Custom agents support the same features as built-in agents. --- ## Quick Start ### Step 1: Install a CLI Agent ```bash # Example: Install Claude Code CLI npm install -g @anthropic-ai/claude-code # Verify installation claude --version ``` ### Step 2: ACP Auto-Detection ACP automatically detects installed CLI agents on your system. No configuration needed! ### Step 3: Use ACP Transport Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available. --- ## How ACP Works ### Architecture ``` ┌─────────────────┐ │ OmniRoute │ │ (HTTP Proxy) │ └────────┬────────┘ │ │ spawn() ▼ ┌─────────────────┐ │ Child Process │ │ (CLI Agent) │ │ │ │ stdin ◄──────┤ Send prompt │ stdout ──────►│ Receive response │ stderr ──────►│ Receive errors └─────────────────┘ ``` ### Process Lifecycle 1. **Spawn** — ACP creates a child process for the CLI agent 2. **Send** — ACP writes prompts to the process's stdin 3. **Receive** — ACP reads responses from stdout/stderr 4. **Idle Detection** — ACP waits 2 seconds of inactivity before considering the response complete 5. **Kill** — ACP terminates the process (SIGTERM, then SIGKILL after 5s) ### Communication Protocol ACP uses **stdio** (standard input/output) for communication with CLI agents. The protocol is: 1. **Send prompt** — Write to stdin with a newline 2. **Wait for response** — Read from stdout until idle (2s of no output) 3. **Timeout** — Default 120 seconds (configurable) --- ## API Reference ### Registry Functions #### `detectInstalledAgents()` Detects all installed CLI agents on the system. Results are cached for 60 seconds. ```typescript import { detectInstalledAgents } from "@/lib/acp"; const agents = detectInstalledAgents(); // Returns: CliAgentInfo[] interface CliAgentInfo { id: string; // e.g., "codex", "claude" name: string; // Display name binary: string; // Binary name to spawn versionCommand: string; // Version detection command version: string | null; // Detected version (null if not installed) installed: boolean; // Whether the agent is installed providerAlias: string; // Provider ID in OmniRoute spawnArgs: string[]; // Arguments to pass when spawning protocol: "stdio" | "http"; // Communication protocol isCustom?: boolean; // Whether this is a user-defined custom agent } ``` #### `getAvailableAgents()` Gets only the agents that are installed and available for ACP. ```typescript import { getAvailableAgents } from "@/lib/acp"; const available = getAvailableAgents(); // Returns: CliAgentInfo[] (only installed agents) ``` #### `getAgentById(id)` Gets a specific agent by ID. ```typescript import { getAgentById } from "@/lib/acp"; const agent = getAgentById("claude"); // Returns: CliAgentInfo | undefined ``` #### `setCustomAgents(agents)` Sets custom agent definitions from settings. ```typescript import { setCustomAgents } from "@/lib/acp"; setCustomAgents([ { id: "my-custom-cli", name: "My Custom CLI", binary: "mycli", versionCommand: "mycli --version", providerAlias: "my-provider", spawnArgs: [], protocol: "stdio", }, ]); ``` ### Manager Functions #### `acpManager.spawn(agentId, binary, args, env)` Spawns a new CLI agent process. ```typescript import { acpManager } from "@/lib/acp"; const session = acpManager.spawn("claude", "claude", ["--print", "--output-format", "json"], { /* custom env vars */ }); // Returns: AcpSession ``` **Allowed agent IDs**: `["claude", "codex", "gemini", "qwen"]` #### `acpManager.sendPrompt(sessionId, prompt, timeoutMs)` Sends a prompt to a CLI agent and collects the response. ```typescript import { acpManager } from "@/lib/acp"; const response = await acpManager.sendPrompt( "acp-claude-1234567890-abc123", "What is 2+2?", 120000 // 2 minutes timeout ); // Returns: Promise ``` #### `acpManager.kill(sessionId)` Kills a session and cleans up. ```typescript import { acpManager } from "@/lib/acp"; const killed = acpManager.kill("acp-claude-1234567890-abc123"); // Returns: boolean ``` #### `acpManager.getActiveSessions()` Gets all active sessions. ```typescript import { acpManager } from "@/lib/acp"; const sessions = acpManager.getActiveSessions(); // Returns: AcpSession[] ``` #### `acpManager.killAll()` Kills all sessions. ```typescript import { acpManager } from "@/lib/acp"; acpManager.killAll(); ``` ### Session Interface ```typescript interface AcpSession { id: string; // Unique session ID agentId: string; // Agent ID (e.g., "claude") process: ChildProcess; // Child process handle alive: boolean; // Whether the process is alive stdoutBuffer: string; // Accumulated stdout buffer stderrBuffer: string; // Accumulated stderr buffer createdAt: Date; // Created timestamp } ``` ### Events The `AcpManager` extends `EventEmitter` and emits the following events: #### `stdout` Emitted when the CLI agent writes to stdout. ```typescript acpManager.on("stdout", ({ sessionId, data }) => { console.log(`[${sessionId}] stdout: ${data}`); }); ``` #### `stderr` Emitted when the CLI agent writes to stderr. ```typescript acpManager.on("stderr", ({ sessionId, data }) => { console.error(`[${sessionId}] stderr: ${data}`); }); ``` #### `exit` Emitted when the CLI agent process exits. ```typescript acpManager.on("exit", ({ sessionId, code, signal }) => { console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`); }); ``` #### `error` Emitted when the CLI agent process errors. ```typescript acpManager.on("error", ({ sessionId, error }) => { console.error(`[${sessionId}] error: ${error}`); }); ``` --- ## Configuration ### Environment Variables ACP inherits all environment variables from the parent process and can be extended with custom env vars: ```typescript acpManager.spawn("claude", "claude", [], { ANTHROPIC_API_KEY: "sk-...", DEBUG: "true", }); ``` ### Spawn Arguments Each agent has default spawn arguments defined in the registry. You can override them: ```typescript acpManager.spawn("claude", "claude", ["--print", "--verbose"], {}); ``` ### Timeouts Default prompt timeout is **120 seconds** (2 minutes). You can override: ```typescript await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes ``` ### Detection Cache Agent detection is cached for **60 seconds** to avoid expensive filesystem scans. Force refresh: ```typescript import { refreshAgentCache } from "@/lib/acp"; refreshAgentCache(); ``` --- ## Security ### Command Injection Prevention ACP validates version commands to prevent command injection attacks: ```typescript const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/; ``` Version commands containing these characters are rejected: - `;` — Command separator - `&` — Background process - `|` — Pipe - `<`, `>` — Redirection - `` ` `` — Command substitution - `$` — Variable expansion - `\r`, `\n` — Line breaks ### Binary Name Validation ACP validates that the version command binary matches the expected binary name (unless it's a custom agent). ### Process Isolation Each ACP session runs in its own child process. The process is killed when the session ends or times out. --- ## Performance ### Detection Performance - **First call**: ~50-200ms (runs `version` command for each agent) - **Cached calls**: <1ms (returns from cache) - **Cache TTL**: 60 seconds ### Prompt Performance - **Spawn**: ~50-100ms - **Send prompt**: ~10-50ms - **Wait for response**: Depends on CLI agent (typically 1-30 seconds) - **Kill**: ~5 seconds (SIGTERM) + immediate (SIGKILL) ### Resource Usage - **Memory per session**: ~10-50MB (depends on CLI agent) - **CPU**: Minimal (I/O bound) - **Disk**: None --- ## Troubleshooting ### "Unknown agent" Error **Problem**: `acpManager.spawn()` throws `Unknown agent: ` **Solution**: Only 4 agents are allowed in `spawn()`: - `claude` - `codex` - `gemini` - `qwen` Other agents must be spawned manually or via custom agent definitions. ### "Session not alive" Error **Problem**: `acpManager.sendPrompt()` throws `Session ${sessionId} is not alive` **Solution**: The session may have exited or been killed. Check session status: ```typescript const session = acpManager.getSession(sessionId); if (!session?.alive) { // Re-spawn the session acpManager.spawn("claude", "claude", [], {}); } ``` ### "ACP timeout" Error **Problem**: `acpManager.sendPrompt()` throws `ACP timeout after 120000ms` **Solution**: Increase the timeout: ```typescript await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes ``` ### CLI Not Detected **Problem**: `detectInstalledAgents()` doesn't find your CLI **Solutions**: 1. **Check PATH**: Ensure the CLI is in your system PATH 2. **Check version command**: Run `claude --version` manually 3. **Check permissions**: Ensure the CLI is executable 4. **Custom agent**: Add a custom agent definition for non-standard CLIs ### Permission Denied **Problem**: ACP can't execute the CLI **Solutions**: 1. **Check file permissions**: `chmod +x /usr/local/bin/claude` 2. **Check ownership**: Ensure OmniRoute has read/execute permissions 3. **Check SELinux/AppArmor**: May block process spawning --- ## Examples ### Example 1: Spawn and Use Claude Code ```typescript import { acpManager, detectInstalledAgents } from "@/lib/acp"; // Detect installed agents const agents = detectInstalledAgents(); const claude = agents.find((a) => a.id === "claude"); if (claude?.installed) { // Spawn a new session const session = acpManager.spawn("claude", claude.binary, ["--print", "--output-format", "json"]); // Send a prompt const response = await acpManager.sendPrompt( session.id, "Explain quantum computing in 100 words" ); console.log("Claude's response:", response); // Clean up acpManager.kill(session.id); } ``` ### Example 2: Auto-Discovery with Fallback ```typescript import { acpManager, getAvailableAgents } from "@/lib/acp"; const available = getAvailableAgents(); // Try Claude first, fallback to Codex let agentId = "claude"; if (!available.find((a) => a.id === "claude")) { if (available.find((a) => a.id === "codex")) { agentId = "codex"; } else { throw new Error("No ACP-compatible CLI agent found"); } } const agent = available.find((a) => a.id === agentId)!; const session = acpManager.spawn(agentId, agent.binary, agent.spawnArgs); const response = await acpManager.sendPrompt(session.id, "Hello!"); acpManager.kill(session.id); ``` ### Example 3: Custom Agent ```typescript import { setCustomAgents, detectInstalledAgents } from "@/lib/acp"; // Register a custom CLI agent setCustomAgents([ { id: "my-llm-cli", name: "My LLM CLI", binary: "myllm", versionCommand: "myllm --version", providerAlias: "my-llm-provider", spawnArgs: ["--format", "json"], protocol: "stdio", }, ]); // Now detectInstalledAgents() will include "my-llm-cli" const agents = detectInstalledAgents(); ``` --- ## What's Next? - **[API Reference](../reference/API_REFERENCE.md)** — REST API endpoints - **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 226 providers - **[MCP Server](./MCP-SERVER.md)** — Model Context Protocol integration - **[A2A Server](./A2A-SERVER.md)** — Agent-to-Agent protocol - **[Cloud Agent](./CLOUD_AGENT.md)** — Cloud-based agents --- ## Reference - [AionUi Project](https://github.com/iOfficeAI/AionUi) — Inspiration for ACP auto-detection - [ACP Source Code](../../src/lib/acp/) — Implementation details - `manager.ts` — Process lifecycle management - `registry.ts` — Agent discovery and registration - `index.ts` — Public API exports