File size: 14,383 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
---
title: ACP (Agent Client Protocol)
---

# ACP (Agent Client Protocol)

> **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex, Gemini CLI) 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    |
| `gemini-cli`  | Gemini CLI         | `gemini`      | 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<string>
```

#### `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: <id>`

**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