v3.8.40 cycle integration → main. All test gates green (Unit/Integration/Coverage/Node-compat/Quality-Ratchet). The only red check, 'PR Test Policy', is the test-masking heuristic firing on the cumulative ~57-commit release diff (legitimate assert consolidations already reviewed per-PR — Gemini CLI removal #5246, retired GPT models #5280, provider catalog refreshes); overridden with --admin per the documented release-PR convention. CodeQL/SonarQube advisory scans non-blocking; #5278's code already passed CodeQL on main. Homologated on VPS 192.168.0.15 (v3.8.40 healthy).
14 KiB
| 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
# 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
- Spawn — ACP creates a child process for the CLI agent
- Send — ACP writes prompts to the process's stdin
- Receive — ACP reads responses from stdout/stderr
- Idle Detection — ACP waits 2 seconds of inactivity before considering the response complete
- 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:
- Send prompt — Write to stdin with a newline
- Wait for response — Read from stdout until idle (2s of no output)
- Timeout — Default 120 seconds (configurable)
API Reference
Registry Functions
detectInstalledAgents()
Detects all installed CLI agents on the system. Results are cached for 60 seconds.
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.
import { getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Returns: CliAgentInfo[] (only installed agents)
getAgentById(id)
Gets a specific agent by ID.
import { getAgentById } from "@/lib/acp";
const agent = getAgentById("claude");
// Returns: CliAgentInfo | undefined
setCustomAgents(agents)
Sets custom agent definitions from settings.
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.
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.
import { acpManager } from "@/lib/acp";
const response = await acpManager.sendPrompt(
"acp-claude-1234567890-abc123",
"What is 2+2?",
120000 // 2 minutes timeout
);
// Returns: Promise<string>
acpManager.kill(sessionId)
Kills a session and cleans up.
import { acpManager } from "@/lib/acp";
const killed = acpManager.kill("acp-claude-1234567890-abc123");
// Returns: boolean
acpManager.getActiveSessions()
Gets all active sessions.
import { acpManager } from "@/lib/acp";
const sessions = acpManager.getActiveSessions();
// Returns: AcpSession[]
acpManager.killAll()
Kills all sessions.
import { acpManager } from "@/lib/acp";
acpManager.killAll();
Session Interface
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.
acpManager.on("stdout", ({ sessionId, data }) => {
console.log(`[${sessionId}] stdout: ${data}`);
});
stderr
Emitted when the CLI agent writes to stderr.
acpManager.on("stderr", ({ sessionId, data }) => {
console.error(`[${sessionId}] stderr: ${data}`);
});
exit
Emitted when the CLI agent process exits.
acpManager.on("exit", ({ sessionId, code, signal }) => {
console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`);
});
error
Emitted when the CLI agent process errors.
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:
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:
acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});
Timeouts
Default prompt timeout is 120 seconds (2 minutes). You can override:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
Detection Cache
Agent detection is cached for 60 seconds to avoid expensive filesystem scans. Force refresh:
import { refreshAgentCache } from "@/lib/acp";
refreshAgentCache();
Security
Command Injection Prevention
ACP validates version commands to prevent command injection attacks:
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
versioncommand 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: <id>
Solution: Only 4 agents are allowed in spawn():
claudecodexgeminiqwen
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:
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:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
CLI Not Detected
Problem: detectInstalledAgents() doesn't find your CLI
Solutions:
- Check PATH: Ensure the CLI is in your system PATH
- Check version command: Run
claude --versionmanually - Check permissions: Ensure the CLI is executable
- Custom agent: Add a custom agent definition for non-standard CLIs
Permission Denied
Problem: ACP can't execute the CLI
Solutions:
- Check file permissions:
chmod +x /usr/local/bin/claude - Check ownership: Ensure OmniRoute has read/execute permissions
- Check SELinux/AppArmor: May block process spawning
Examples
Example 1: Spawn and Use Claude Code
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
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
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 — REST API endpoints
- Provider Reference — All 226 providers
- MCP Server — Model Context Protocol integration
- A2A Server — Agent-to-Agent protocol
- Cloud Agent — Cloud-based agents
Reference
- AionUi Project — Inspiration for ACP auto-detection
- ACP Source Code — Implementation details
manager.ts— Process lifecycle managementregistry.ts— Agent discovery and registrationindex.ts— Public API exports