|
| 1 | +# anet 支持 codex CLI 的完整方案(深度设计) |
| 2 | + |
| 3 | +> **作者**: 通信龙(hands-on 跑 codex CLI 0.130 + 整合通信SDK马 + 通信工程马 quick estimates) |
| 4 | +> **日期**: 2026-05-13 |
| 5 | +> **状态**: 草案 — 决策依据 doc,不是 RFC(RFC-006 由通信SDK马起完整 RFC) |
| 6 | +> **触发**: Vincent telegram 4019 "2.1.8 大 feature = codex-cli 成功支持" + 4040+4048 PR #21424 pointer + 4060+4061 "出方案 + 研究深刻一点" |
| 7 | +> **关联**: RFC-005 (TUI mode, superseded) / RFC-002 Phase 2 (SDK runtimes Telegram bind) |
| 8 | +
|
| 9 | +## TL;DR |
| 10 | + |
| 11 | +🎯 **架构选定**: agent-node 内 spawn `codex app-server --listen stdio` 作 child process,通过 JSON-RPC stdio 跟 codex daemon 双向通信。**Push-driven 真 daemon**,跟 claude-code-cli 行为对称。 |
| 12 | + |
| 13 | +📊 **工作量**: agent-node +150-200 行 bridge / cli.ts +30-50 行 (delegate) / Docker E2E +300 行 / 总约 500-600 行。**2-3 天 ship 2.1.8 大 feature**。 |
| 14 | + |
| 15 | +⚠️ **关键 timing**: codex 0.130 已 ship JSON-RPC app-server protocol(stable,跟 PR #21424 unrelated 不必等)。protocol 已 dump 75 ClientRequest + 63 ServerNotification + 9 ServerRequest method enum 实测。 |
| 16 | + |
| 17 | +✅ **架构对称性**: 用户对 anet network 视角看,`codex-code-cli` runtime 等价 `claude-code-cli` runtime — 都能接收 send_task 自动响应。 |
| 18 | + |
| 19 | +## 1. 现状 + Gap |
| 20 | + |
| 21 | +### 1.1 anet 现有 4 个 runtime |
| 22 | + |
| 23 | +`agent-network/bin/cli.ts:140`: |
| 24 | + |
| 25 | +```ts |
| 26 | +type RuntimeName = "claude-code-cli" | "codex-sdk" | "claude-agent-sdk" | "http-api"; |
| 27 | +``` |
| 28 | + |
| 29 | +| Runtime | 协议 | daemon? | claude/codex | |
| 30 | +|---|---|---|---| |
| 31 | +| `claude-code-cli` | spawn `claude` 二进制 + channel plugin daemon layer | ✅ yes | claude | |
| 32 | +| `claude-agent-sdk` | npm SDK `@anthropic-ai/claude-agent-sdk` (programmatic) | ✅ yes | claude | |
| 33 | +| `codex-sdk` | npm SDK `@openai/codex-sdk` | ✅ yes | codex | |
| 34 | +| `http-api` | OpenAI-compatible HTTP fetch | ✅ yes | both | |
| 35 | + |
| 36 | +**对称性 gap**: claude 侧有 CLI 二进制 path (claude-code-cli) + SDK path (claude-agent-sdk) 双轨;codex 侧仅有 SDK,**缺 codex CLI 二进制 path**。 |
| 37 | + |
| 38 | +### 1.2 用户实证需求 |
| 39 | + |
| 40 | +Vincent `~/.codex/config.toml` 含半成品配置: |
| 41 | + |
| 42 | +```toml |
| 43 | +[mcp_servers.commhub-proxy] |
| 44 | +command = "bun" |
| 45 | +args = ["/home/vansin/agent-orchestra/proxy/commhub-proxy.ts"] |
| 46 | +``` |
| 47 | + |
| 48 | +但 `commhub-proxy.ts` 不存在。用户**真实尝试过**这条路径,但**没产品化**。 |
| 49 | + |
| 50 | +### 1.3 codex CLI 0.130 protocol stable |
| 51 | + |
| 52 | +```bash |
| 53 | +$ codex --version |
| 54 | +codex-cli 0.130.0 |
| 55 | +``` |
| 56 | + |
| 57 | +跑 `codex app-server generate-json-schema --out /tmp/codex-schema` dump 实测 39 schema files,含 `ClientRequest.json` / `ServerNotification.json` / `ServerRequest.json` / `JSONRPCMessage.json` etc. |
| 58 | + |
| 59 | +**关键 API**: |
| 60 | + |
| 61 | +- 75 ClientRequest methods (anet → codex daemon 单向 send) |
| 62 | +- 63 ServerNotification methods (codex daemon → anet streaming push) |
| 63 | +- 9 ServerRequest methods (codex daemon → anet 反向 ask, anet 必须 reply) |
| 64 | +- 1 ClientNotification method |
| 65 | + |
| 66 | +## 2. 完整 Protocol Flow |
| 67 | + |
| 68 | +### 2.1 Push 入口 — `turn/start` |
| 69 | + |
| 70 | +```json |
| 71 | +{ |
| 72 | + "jsonrpc": "2.0", |
| 73 | + "id": 1, |
| 74 | + "method": "turn/start", |
| 75 | + "params": { |
| 76 | + "threadId": "<uuid>", |
| 77 | + "input": [{"type": "text", "text": "<task content from commhub_send_task>"}] |
| 78 | + } |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +anet wrapper 接到 commhub `new_task` event → 立即 send 这个 RPC → codex daemon 处理 turn |
| 83 | + |
| 84 | +### 2.2 codex daemon Streaming output (ServerNotification) |
| 85 | + |
| 86 | +``` |
| 87 | +codex daemon → anet wrapper stream notifications: |
| 88 | + - turn/started (turn 开始) |
| 89 | + - item/started (item 处理开始) |
| 90 | + - item/agentMessage/delta (流式输出,chunks) |
| 91 | + - item/completed (item 完成) |
| 92 | + - turn/completed (turn 结束 — anet 看到这个 = 全部完成) |
| 93 | +``` |
| 94 | + |
| 95 | +anet wrapper 聚合 `item/agentMessage/delta` → 拿 final output → 通过 commhub MCP tools (codex 内部 call) 自动回复 |
| 96 | + |
| 97 | +### 2.3 反向 ask — ServerRequest 9 个 approval/elicitation 流程 |
| 98 | + |
| 99 | +codex daemon 处理过程中 push reverse request 给 anet wrapper: |
| 100 | + |
| 101 | +| ServerRequest | 用途 | anet wrapper 应对 | |
| 102 | +|---|---|---| |
| 103 | +| `applyPatchApproval` | 改文件前 ask | Smart policy: auto-approve safe / escalate dangerous | |
| 104 | +| `execCommandApproval` | 执行 shell 命令前 ask | Smart policy: white-list read-only / escalate destructive | |
| 105 | +| `item/permissions/requestApproval` | 通用 permission | escalate | |
| 106 | +| `item/tool/call` | call MCP tool | anet wrapper 转给 commhub tool call | |
| 107 | +| `item/tool/requestUserInput` | 需要用户输入 | escalate 指挥室 via commhub_send_task | |
| 108 | +| `mcpServer/elicitation/request` | MCP server 询问 | escalate 或 auto-fill from config | |
| 109 | +| `applyPatchApproval/grantRoot` | 文件 root permission | 慎重 escalate | |
| 110 | +| `account/chatgptAuthTokens/refresh` | OpenAI auth refresh | passthrough | |
| 111 | +| `execCommandApproval` parsedCmd | 命令解析 metadata | base auto-approve 判断 | |
| 112 | + |
| 113 | +### 2.4 完整 sequence diagram |
| 114 | + |
| 115 | +``` |
| 116 | +[commhub-server] |
| 117 | + | |
| 118 | + | SSE (new_task event for codex-bot) |
| 119 | + v |
| 120 | +[agent-node (codex-code-cli runtime)] |
| 121 | + | |
| 122 | + | JSON-RPC stdio |
| 123 | + v |
| 124 | +[codex app-server child process] |
| 125 | + | |
| 126 | + 1. Init: {method: "initialize"} → ack |
| 127 | + 2. Thread: {method: "thread/start", params: {...}} → threadId |
| 128 | + 3. Push prompt: {method: "turn/start", params: {threadId, input: [task]}} |
| 129 | + 4. codex inference (LLM call, may use commhub MCP tools) |
| 130 | + 5. (codex 想 call shell) |
| 131 | + <-- {method: "execCommandApproval", params: {command, cwd, ...}} ServerRequest |
| 132 | + --> {result: {approved: true}} ServerResponse (anet 决策) |
| 133 | + 6. Stream: notifications/item/agentMessage/delta (output chunks) |
| 134 | + 7. Done: notifications/turn/completed |
| 135 | + | |
| 136 | + v |
| 137 | +[agent-node bridge] |
| 138 | + - Aggregate final output |
| 139 | + - POST commhub reply (via commhub_send_task or commhub_reply MCP tool) |
| 140 | + | |
| 141 | + v |
| 142 | +[commhub] |
| 143 | +``` |
| 144 | + |
| 145 | +## 3. Implementation Plan |
| 146 | + |
| 147 | +### 3.1 cli.ts 改动 (~30-50 行) |
| 148 | + |
| 149 | +```ts |
| 150 | +// L140 RuntimeName enum |
| 151 | +type RuntimeName = "claude-code-cli" | "codex-sdk" | "codex-code-cli" | "claude-agent-sdk" | "http-api"; |
| 152 | + |
| 153 | +// L142-149 normalizeRuntime |
| 154 | +function normalizeRuntime(r: string): RuntimeName { |
| 155 | + switch (r) { |
| 156 | + case "codex-cli": |
| 157 | + case "codex-code-cli": |
| 158 | + return "codex-code-cli"; |
| 159 | + // ... existing branches |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +// L596 assertStartCompatibility |
| 164 | +if (profile.runtime === "codex-code-cli") { |
| 165 | + const v = execFileSync("codex", ["--version"], {encoding: "utf-8"}).trim(); |
| 166 | + // verify codex >= 0.130.0 |
| 167 | +} |
| 168 | + |
| 169 | +// L634 checkRuntimeDependency |
| 170 | +if (profile.runtime === "codex-code-cli") { |
| 171 | + if (!commandExists("codex")) { |
| 172 | + warn("Install codex CLI: npm i -g @openai/codex@latest"); |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +// L1612-1701 launchAgent — delegate 给 agent-node (类比 codex-sdk runtime path) |
| 177 | +case "codex-code-cli": |
| 178 | + return spawnAgentNode(profile, {runtime: "codex-code-cli"}); |
| 179 | +``` |
| 180 | + |
| 181 | +### 3.2 agent-node 改动 (~150-200 行) |
| 182 | + |
| 183 | +新加 `agent-node/src/runtime/codex-code-cli.ts`: |
| 184 | + |
| 185 | +```ts |
| 186 | +// Spawn codex app-server child + JSON-RPC bridge |
| 187 | +class CodexCodeCliRuntime { |
| 188 | + private child: ChildProcess; |
| 189 | + private threadId?: string; |
| 190 | + private pendingRequests = new Map<RequestId, Resolver>(); |
| 191 | + |
| 192 | + async start(profile: Profile) { |
| 193 | + this.child = spawn("codex", ["app-server"], { |
| 194 | + stdio: ["pipe", "pipe", "inherit"], |
| 195 | + env: {...process.env, COMMHUB_TOKEN: profile.token, ...} |
| 196 | + }); |
| 197 | + |
| 198 | + // Stream JSON-RPC notifications/responses from child.stdout |
| 199 | + this.startNotificationLoop(); |
| 200 | + |
| 201 | + // Initialize handshake |
| 202 | + await this.rpc("initialize", {protocolVersion: "2024-11-05", ...}); |
| 203 | + |
| 204 | + // Start persistent thread for this agent-node session |
| 205 | + const r = await this.rpc("thread/start", {/* threadId or auto-gen */}); |
| 206 | + this.threadId = r.threadId; |
| 207 | + } |
| 208 | + |
| 209 | + // commhub SSE handler — new task arrives |
| 210 | + async onNewTask(task: TaskEvent) { |
| 211 | + // Push prompt to codex |
| 212 | + await this.rpc("turn/start", { |
| 213 | + threadId: this.threadId, |
| 214 | + input: [{type: "text", text: task.task}] |
| 215 | + }); |
| 216 | + // Notifications stream in via startNotificationLoop |
| 217 | + } |
| 218 | + |
| 219 | + // Handle codex push notifications + reverse requests |
| 220 | + private startNotificationLoop() { |
| 221 | + this.child.stdout.on("data", (chunk) => { |
| 222 | + for (const msg of parseJsonRpcStream(chunk)) { |
| 223 | + if (msg.method === "turn/completed") { |
| 224 | + this.flushTaskReply(); // aggregated output → commhub |
| 225 | + } else if (msg.method === "execCommandApproval") { |
| 226 | + this.handleApproval(msg); // smart policy |
| 227 | + } else if (msg.method === "item/agentMessage/delta") { |
| 228 | + this.appendOutput(msg.params); // streaming chunks aggregate |
| 229 | + } |
| 230 | + // ... other 60+ notification types |
| 231 | + } |
| 232 | + }); |
| 233 | + } |
| 234 | + |
| 235 | + // Smart approval policy |
| 236 | + private handleApproval(req: ServerRequest) { |
| 237 | + if (req.method === "execCommandApproval") { |
| 238 | + const cmd = req.params.parsedCmd[0]?.cmd; |
| 239 | + const safe = ["ls", "cat", "grep", "git status", "git log"].includes(cmd); |
| 240 | + if (safe) return this.reply(req.id, {approved: true}); |
| 241 | + // Escalate dangerous to 指挥室 via commhub_send_task |
| 242 | + this.escalate(req).then(approval => this.reply(req.id, approval)); |
| 243 | + } |
| 244 | + // ... 9 server request types |
| 245 | + } |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +### 3.3 Approval Policy 决策矩阵 |
| 250 | + |
| 251 | +| ServerRequest | 安全级别 | 默认策略 | 配置 override | |
| 252 | +|---|---|---|---| |
| 253 | +| `execCommandApproval` 白名单 (ls/cat/grep/git status/git log) | safe | auto-approve | `profile.flags.codex.autoApproveCmd: ["..."]` | |
| 254 | +| `execCommandApproval` 其他 | dangerous | escalate 指挥室 | `profile.flags.codex.autoApproveAll: true` (危, dev only) | |
| 255 | +| `applyPatchApproval` 限同 cwd 内 | medium | auto-approve | escalate threshold by 文件数/lines | |
| 256 | +| `applyPatchApproval` 涉及 grantRoot | dangerous | escalate | 永不 auto-approve | |
| 257 | +| `item/permissions/requestApproval` | dangerous | escalate | — | |
| 258 | +| `mcpServer/elicitation/request` | depends | escalate 或 auto-fill commhub fields | — | |
| 259 | +| `item/tool/call` (codex 调 MCP tool) | safe | passthrough (anet 不拦) | — | |
| 260 | +| `item/tool/requestUserInput` | medium | escalate 指挥室 | — | |
| 261 | + |
| 262 | +### 3.4 Docker E2E test L0-L7 (~300 行) |
| 263 | + |
| 264 | +通信测试马 PR #43 scaffold 复用 + L3/L6 改 + 新增 L7: |
| 265 | + |
| 266 | +| Level | Check | 命令 | |
| 267 | +|---|---|---| |
| 268 | +| L0 | prerequisites | `which codex && which anet` | |
| 269 | +| L1 | hub up | `anet hub start` + `curl /health` | |
| 270 | +| L2 | node create | `anet node create test-bot --runtime codex-code-cli` + config 写盘 | |
| 271 | +| L3 | child spawn verify | `pgrep -f "codex app-server"` (代替 codex no-subcommand) | |
| 272 | +| L4 | env injection | `/proc/<pid>/environ` 验 `COMMHUB_TOKEN` | |
| 273 | +| L5 | JSON-RPC handshake | netcat / fifo 读 child stdin/stdout 验 initialize ack | |
| 274 | +| L6 | push verify | `anet commhub_send_task --alias test-bot --task "hello"` → 验 codex 收到 `turn/start` → 流式 output → reply 回 commhub | |
| 275 | +| L7 | cross-runtime | 起 codex-code-cli + claude-code-cli 两 node → A `send_task` B → 都能接收 daemon push | |
| 276 | + |
| 277 | +### 3.5 cases doc (通信文档马 follow-up PR) |
| 278 | + |
| 279 | +`docs-site/docs/cases/codex-code-cli-bot.md` ZH+EN — 5 步 user walkthrough: |
| 280 | + |
| 281 | +1. 装 codex CLI: `npm i -g @openai/codex@latest` |
| 282 | +2. 升 anet: `npm i -g @sleep2agi/agent-network@latest` |
| 283 | +3. 创节点: `anet node create my-codex --runtime codex-code-cli` |
| 284 | +4. 启动: `anet node start my-codex` |
| 285 | +5. 跟其他 agent 通信: `anet commhub_send_task --alias my-codex --task "task content"` |
| 286 | + |
| 287 | +## 4. 风险评估 + Mitigation |
| 288 | + |
| 289 | +| Risk | Severity | Mitigation | |
| 290 | +|---|---|---| |
| 291 | +| codex 0.130 protocol stabilize 本周 (PR #22404 #22414 #22386 仍在改) | 🔴 high | Pin `codex@0.130.0` exact + 监控 release notes + integration test runs daily on `latest` | |
| 292 | +| Single-client daemon limit (issue #21551 just merged design RFC) | 🟡 medium | anet wrapper 一节点对应一 codex daemon 1:1,用户不能并发 attach | |
| 293 | +| Approval flow 决策 false positive (auto-approve 错的命令) | 🔴 high | Conservative whitelist (read-only only by default) + sandbox flags + escalate 指挥室 fallback | |
| 294 | +| Token bridge (codex daemon credential vs anet ntok_) | 🟡 medium | Phase 1: 不让 codex daemon 自己 auth (用本地 stdio 不需 token),Phase 2: 如要 ws transport 再加 JWT | |
| 295 | +| codex daemon crash / supervision | 🟡 medium | agent-node supervisor watcher: crash 自动 respawn (类比 claude-agent-sdk pattern) | |
| 296 | +| 多 agent-node 同机器 stdio port 冲突 | 🟢 low | stdio 不用 port,per-child 隔离 OK | |
| 297 | +| API protocol breaking change in 0.131+ | 🟡 medium | Schema codegen + version check, fail-fast if mismatch | |
| 298 | + |
| 299 | +## 5. Timeline (2-3 天 ship 2.1.8 大 feature) |
| 300 | + |
| 301 | +**Day 1** (today): |
| 302 | +- ✅ 通信龙 hands-on 调研完成 (本 doc) |
| 303 | +- ⏳ 通信SDK马 起 RFC-006 design doc (90-120 min) |
| 304 | +- ⏳ 通信工程马 dive `codex-sdk` runtime 现有 agent-node spawn pattern (60 min) |
| 305 | + |
| 306 | +**Day 2**: |
| 307 | +- 通信工程马 实施 agent-node `codex-code-cli.ts` runtime adapter (~200 行) — focus JSON-RPC bridge + push handler + approval smart policy |
| 308 | +- 通信工程马 cli.ts 加 RuntimeName + setup wizard entry (~50 行) |
| 309 | +- 通信SDK马 review code + spawn 协议 sanity check |
| 310 | +- 通信测试马 起 PR #43 update + 新加 L6 push verify + L7 cross-runtime |
| 311 | + |
| 312 | +**Day 3**: |
| 313 | +- 联合 smoke test 跑通 1 个 commhub_send_task → codex 响应 → reply |
| 314 | +- Vincent mac mini 亲测 |
| 315 | +- Ship preview `2.1.8-preview.N` (含 codex-code-cli runtime) |
| 316 | +- Vincent 亲测通过 → 升 latest `2.1.8` stable **大 feature** |
| 317 | + |
| 318 | +## 6. RFC-005 → RFC-006 Migration |
| 319 | + |
| 320 | +- **RFC-005** mark `Superseded by RFC-006` (TUI mode + pull-on-prompt 是 wrong abstraction) |
| 321 | +- **RFC-006** codex-code-cli runtime via app-server daemon mode — 完整 push-driven,跟 claude-code-cli 行为对称 |
| 322 | +- 通信工程马 `~/anet-work/rfc-005-codex-code-cli/` 6 edit worktree git stash 保留 backup (5/6 edit 可复用,仅 launchAgent spawn 段重写) |
| 323 | +- 通信SDK马 起 RFC-006 时引用本设计 doc + 之前 6429bc0 codex CLI 研究 doc + PR #21424 + protocol schema dump |
| 324 | + |
| 325 | +## 7. Open Questions (待 Vincent 拍板) |
| 326 | + |
| 327 | +1. **Approval policy whitelist** — 默认 auto-approve 哪些 shell 命令? (我建议 read-only: ls / cat / grep / git status / git log) |
| 328 | +2. **escalate target** — 当 codex 询问 dangerous approval 时,escalate 哪个 alias? (建议 `指挥室` = Vincent,但用户的个人 codex node escalate 给"用户自己",可能 escalate via telegram channel) |
| 329 | +3. **Sandbox flags default** — `--ignore-user-config` + `--sandbox` mode? (建议 sandbox = "workspace-write" 限制 in cwd) |
| 330 | +4. **Thread lifecycle** — per agent-node session 一个 thread (长 context) 还是 per task new thread (clean isolation)? (建议 per session,跟 claude-code-cli 一致) |
| 331 | +5. **Multi-agent codex daemon** — 单机器多 codex-code-cli node 是否限制? (建议 limit by RAM, document 1 daemon ~200MB) |
| 332 | + |
| 333 | +## 8. 结论 |
| 334 | + |
| 335 | +✅ **anet 支持 codex CLI 完全可行** — codex 0.130 已 ship 完整 daemon RPC protocol |
| 336 | +✅ **架构方向定了** — agent-node + codex app-server stdio bridge |
| 337 | +✅ **工作量 manageable** — ~500-600 行总量,2-3 天 ship |
| 338 | +⚠️ **timing 注意** — Pin codex@0.130 + 监控 protocol stabilize 本周变化 |
| 339 | +⚠️ **Approval policy 需 Vincent 拍板** — 7 个 open questions 决定后实施 |
| 340 | + |
| 341 | +后续动作: |
| 342 | +- 通信SDK马 起 RFC-006 design (90-120 min) |
| 343 | +- 通信工程马 dive codex-sdk runtime 现有 pattern (60 min) |
| 344 | +- Vincent 答 7 个 open questions |
| 345 | +- 实施开干 |
| 346 | + |
| 347 | +— END — |
0 commit comments