Skip to content

feat(cli): add codex app-server backend (protocol + profile)#2216

Merged
KevinZhao merged 8 commits into
masterfrom
feat/codex-backend
Jun 21, 2026
Merged

feat(cli): add codex app-server backend (protocol + profile)#2216
KevinZhao merged 8 commits into
masterfrom
feat/codex-backend

Conversation

@KevinZhao

Copy link
Copy Markdown
Owner

概述

实现 docs/rfc/codex-backend.md Phase 1:把 OpenAI Codex CLI 作为 naozhi 第三个 CLI backend(继 claude / kiro 之后),走 codex app-server 的 JSON-RPC 2.0 over stdio 长连接协议。

先做了 Phase 0 可行性验证(codex-cli 0.141.0 实测),把 RFC 从 Draft v1 升到 v2 并纠正了一处关键过时论断。

可行性验证结论(详见 docs/rfc/codex-backend-validation.md

  • app-server 协议完整跑通initialize → initialized → thread/start → turn/start → turn/started → item/* → turn/completed,RFC §2.4 事件流与 method 名经 generate-json-schema + 实测全部命中。
  • 🔥 纠正 RFC §7:v1 称 "Codex 只能走 OpenAI 自家不像 claude 走 Bedrock" —— 过时。codex 0.141 内置 amazon-bedrock provider(openai/codex PR #18744)。实测 bedrock-mantle.us-west-2.api.aws/v1/responses + gpt-oss-120b 连通。
  • ⚠️ 两个 Bedrock 约束:① 内置 provider 打到 /openai/v1/responses,gpt-oss 只在 /v1/responses 服务 → 需自定义 provider;② Bedrock gpt-oss responses 拒绝 codex 内置 agentic 工具的 namespace 变体(只认 function/mcp)→ Bedrock 路径仅支持纯对话/function-calling,完整 shell-agentic 需 OpenAI 凭据 + gpt-5.x-codex。

改动

新增

  • internal/cli/protocol_codex.goCodexProtocol 实现 cli.Protocol(BuildArgs / Init 握手 / WriteMessage(turn/start, UserInput[]) / WriteInterrupt(turn/interrupt) / ReadEvent 翻译层 / HandleEvent 自动授权 / Capabilities)。模式对齐 ACPProtocol(atomic.Pointer threadID、readUntilResponse goroutine+timeout+shim-deadline-pulse、mu-guarded textBuf、turn 边界 flush assistant 帧 + result 元数据二段式)。
  • internal/cli/protocol_codex_test.go — 表驱动单测(握手/turn/interrupt/approval/tokenUsage/未知 method/错误响应/partial-text-on-failure)。
  • internal/cli/backend/profile_codex.gocodexProfile()(ID=codex, Tag=cdx, ChipColor=OpenAI 绿, CostUnit=tokens, HistoryDir=~/.codex/sessions/, RequiredNodeCaps=[codex-app-server])。
  • docs/rfc/codex-backend-validation.md — Phase 0 实测报告。

修改

  • internal/cli/backend/profile.goRegisterDefaultscodexProfile()注册 ≠ 默认开启EnabledBackends 仍由 cli.backends 配置驱动,默认只有 claude。
  • internal/cli/detect.goknownBackends 增 codex 行(满足 mirror 契约测试)。
  • internal/cli/backend/profile_test.go — 默认 backend 数 2→3 + codex 字段断言。
  • docs/rfc/codex-backend.md / README.md — 实测回写(input 形态、token usage 位置、反向请求名、Bedrock 小节、状态升 v2)。

Code Review

经 go-reviewer 审查,修复 2 个 CRITICAL(turn 边界缺 assistant 帧→dashboard 无气泡;failed turn 丢弃 partial text)+ 2 个 HIGH(WriteInterrupt 缺 cancel metric;post-handshake 错误响应被静默吞)+ 1 个 MEDIUM(id round-trip 改用 strconv.Atoi 对齐 ACP、处理负数 id)。

测试计划

  • go build ./...
  • go vet ./internal/cli/...
  • gofmt -l(无 diff)
  • go test -race ./internal/cli/(含新 codex 表驱动测试)
  • go test ./internal/{server,discovery,config,session,metrics}/(无回归)
  • codex 0.141.0 真实 app-server 端到端握手 + turn(验证报告)

后续(不在本 PR)

  • Phase 1 余项:internal/history/codexjsonl/ 历史 source。
  • Phase 2:Dashboard chip/cost/Features 接入、反向请求→AskUserQuestion 卡片、turn/steer/urgent

🤖 Generated with Claude Code

实现 docs/rfc/codex-backend.md Phase 1:OpenAI Codex CLI 作为第三个 backend,
走 `codex app-server` JSON-RPC 2.0 over stdio。

实现:
- internal/cli/protocol_codex.go — CodexProtocol 实现 cli.Protocol:
  initialize+initialized+thread/start 握手、turn/start(UserInput[])、
  turn/interrupt、item/agentMessage/delta 流式累积、turn/completed 边界
  flush assistant 帧 + result、thread/tokenUsage/updated → metadata、
  */requestApproval 反向请求自动 allow。模式对齐 ACPProtocol。
- internal/cli/backend/profile_codex.go — codexProfile(),注册到 RegisterDefaults
  (注册≠默认开启,仍需 cli.backends 显式配置)。
- internal/cli/detect.go — knownBackends 增 codex 行。

可行性验证(codex 0.141.0 实测,docs/rfc/codex-backend-validation.md):
- app-server 协议完整跑通,RFC §2.4 事件流 + method 名命中。
- 纠正 RFC §7:codex 原生支持 Amazon Bedrock(内置 amazon-bedrock provider)。
  实测 bedrock-mantle/v1/responses + gpt-oss-120b 连通;两个约束:内置 provider
  路径 bug(需自定义 provider 指 /v1)、gpt-oss responses 拒 codex namespace
  工具(agentic 受限,纯对话/function-calling 可用)。

测试:protocol_codex_test.go 表驱动覆盖握手/turn/interrupt/approval/
tokenUsage/未知method/错误响应;profile 注册断言更新为 3 backend。
go build/vet/test -race ./internal/cli/... 全绿。
- internal/upstream/caps_test.go: derivedCaps() 现含 codex 的
  RequiredNodeCaps "codex-app-server",更新断言为 ["acp","codex-app-server"]
  (修复 CI test/test-macos FAILURE:TestDerivedCaps_FromDefaultRegistry)。
- internal/node/caps.go: knownServerCaps 增 "codex-app-server",与 kiro 的
  "acp" 对齐,消除 codex 子节点注册时的 spurious "unknown capabilities" WARN。
- docs/rfc: 二次实测纠正 —— Bedrock + gpt-5.5(us-east-1/2,内置 amazon-bedrock
  provider)支持 codex 完整 shell agentic(codex exec + app-server 实测通过),
  是首选 Bedrock 部署路径;§7.1 约束 2(namespace 工具被拒)仅适用于 gpt-oss。

完整性审计(vs kiro 端到端接线):profile 注册 / detect / 节点 cap 派生+远程
门控 / config 校验 / doctor / dashboard chip+tag+feature+cost API / 本地 wrapper
全部经 backend.Profile 注册表泛型驱动,codex 零额外编辑生效。codexjsonl 历史
source 为 phase1+(当前 NoopHistorySource 优雅降级,不 panic)。

go build/vet/test ./internal/... ./cmd/... 全绿。
补齐 codex backend 唯一的结构性抽象缺口:历史面板回填。codex 之前是
NoopHistorySource(优雅降级,历史会话显示空),现与 claude/kiro 对等。

- internal/history/codexjsonl/source.go — history.Source 实现,读
  ~/.codex/sessions/YYYY/MM/DD/rollout-<iso>-<threadId>.jsonl。codex 的
  rollout 格式比 kiro 更干净:event_msg 行(user_message/agent_message)
  自带 ISO-8601 时间戳和已拼接纯文本,无需借时间戳。WalkDir 按
  `-<threadId>.jsonl` 后缀匹配日期分桶树;镜像 kirojsonl 的 path-traversal
  防护 / 16MiB 上限 / ctx 取消 / 逐行容错降级。init() 注册 codex factory。
- HistoryWiring.CodexSessionsDir 字段贯通:cli/history.go + RouterConfig +
  router_core/lifecycle(codexSessionsDir) + main.go(~/.codex/sessions) +
  wireup/history_backends.go blank-import。
- 测试:source_test.go(glob/event_msg 映射/ISO 时间戳/limit/before 过滤/
  path-traversal/缺文件/降级/坏行跳过/factory)+ 实测 codex 0.141 真实
  rollout 文件解析正确(user prompt + agent reply 时间戳准确)。

go build/vet/test ./internal/... ./cmd/... 全绿。

说明:askuser / passthrough / embedded_context 三个 Feature 标志仍为
phase1 false——codex 协议有对应原语(item/tool/requestUserInput /
turn/steer / mention UserInput),但需各自实现底层管线后再翻 true,否则
dashboard 会显示点了不工作的控件。属 RFC §10 Phase 2 范围。
check-router-fields lint 要求每个 Router 字段声明 `// 读写:` 访问集;
codexSessionsDir 与 kiroSessionsDir 同访问模式(core init / lifecycle
attachHistorySource / discovery)。修复 lint-router CI 失败。
@KevinZhao

Copy link
Copy Markdown
Owner Author

对抗性 review(独立 reviewer + 对抗验证,已在 PR 分支 HEAD 5b142a6d 实测核实):整体方向良好,发现 1 个 major(会话挂死)bug,建议合并前修。

🔴 [major] post-handshake codex turn/start RPC 错误被吞,turn 永不关闭、会话卡 state=running

位置internal/cli/process_readloop.go:391internal/cli/protocol_codex.go:364

链路(逐项核实):

  1. protocol_codex.go:55 定义独立 sentinel var ErrCodexRPC = errors.New("codex rpc error"),与 ErrACPRPCprotocol_acp.go)无关。
  2. CodexProtocol.ReadEventprotocol_codex.go:364)对主 readLoop 上观察到的 deferred turn/start ERROR 响应返回 (nil, true, fmt.Errorf("%w %d: %s", ErrCodexRPC, ...))。代码注释明确说这是为防止"post-handshake turn/start 失败被静默吞掉、会话卡 state=running"(对齐 ACP 的 rationale)。
  3. process_readloop.go:391 的错误分支只认 ACP sentinelif errors.Is(err, ErrACPRPC) { 合成 result 事件 },否则 :402 log.Warn("skip unparseable event"); return shimDispatchContinue
  4. errors.Is(codexErr, ErrACPRPC) == false(已实测),故 codex 错误落入 else 被丢弃;done 布尔在 :379 被丢弃,turn 关闭只靠 dispatch result 事件——于是永不关闭,恰是注释声称要防止的失败模式。

可触发性turn/startWriteMessage fire-and-forget 发送(非 sendAndWaitResponse),任何错误回复(-32001 过载 / gpt-oss namespace-tool 拒绝,见 validation report)都落到这条路径。

测试缺口TestCodexProtocol_ReadEvent_PostHandshakeErrorResponse 只断言 ReadEvent 返回值,无 readLoop/handleShimStdout 集成测试,故掩盖了此 swallow。

建议修复process_readloop.go:391 扩展为同时认 codex sentinel,并用 codex 标签合成 result:

if errors.Is(err, ErrACPRPC) || errors.Is(err, ErrCodexRPC) {
    tag := "[kiro] "
    if errors.Is(err, ErrCodexRPC) {
        tag = "[codex] "
    }
    events = []Event{{Type: "result", SubType: "error", Result: tag + err.Error()}}
    // fall through to normal turn-end dispatch
} else { ... }

并补一个 readLoop 集成测试(喂一条 codex turn/start error 响应,断言合成了 result/error 事件、turn 关闭)。

注:turn/completed status=failed 是另一条正常工作的路径(直接 emit result),只有 RPC-error-response 路径坏。严重度 major 非 blocker——happy path 与 status=failed 路径正常,且 codex 是新 opt-in backend;但这是真实的会话挂死且直接废掉代码声称的安全网。

其余(protocol 解析、codexjsonl history、backend 隔离 gating)未发现 claude 路径回归。

KevinZhao and others added 2 commits June 21, 2026 13:00
三个 claude 专属 Feature 在 codex backend 的可行性评估,含 codex 0.141
live 探针结论:
- embedded_context: EASY/MODERATE — @path 经 shell 工具读取(非原生内联),
  倾向结构化 mention UserInput;推荐第一做(~0.5-1d)。
- passthrough: MODERATE — 实测确认 clientUserMessageId 逐字 round-trip 为
  item.clientId + turn/steer mid-turn 成功,slot-matching 适配器可行(非重设计);
  第二做(~3-4d)。
- askuser: HARD — 阻塞式 RPC 重引入 pending/TTL 表,且 requestUserInput
  请求/响应 schema 未验证;推迟,先 live 捕获。

codex 与 kiro Feature map 逐位相同,三个 false 是所有非-replay 后端的
共同 Phase-2 空缺,非 codex 独有。
…lowed ErrCodexRPC) [#2216]

CodexProtocol.ReadEvent returns (nil, true, %w ErrCodexRPC) for a
deferred turn/start ERROR response on the main readLoop, intending to
close the turn — but handleShimStdout only recognised ErrACPRPC, so
errors.Is(codexErr, ErrACPRPC) was false and the error fell into the
'skip unparseable event' branch. No synthetic result event was
dispatched, the turn never closed, and the codex session hung in
state=running — exactly the failure mode the ReadEvent comment claims to
prevent. Triggerable because turn/start is sent fire-and-forget via
WriteMessage, so any error reply (-32001 overload, gpt-oss tool
rejection) lands on this path.

Fix: extract rpcErrorTurnEnd(err) which recognises BOTH ErrACPRPC and
ErrCodexRPC and returns the backend tag for the synthesized result. The
readLoop error branch calls it instead of an inline ACP-only errors.Is,
so a new backend only needs to register its sentinel in one place.

Regression test rpc_error_turn_end_test.go covers both sentinels, the
wrapped+bare forms, a non-RPC parse error (must NOT synthesize), and a
guard asserting ErrCodexRPC does not satisfy errors.Is(ErrACPRPC) — the
exact distinction the old code missed. Full internal/cli green.

Found via adversarial review of this PR.
@KevinZhao

Copy link
Copy Markdown
Owner Author

✅ 已修复并推送(commit 9af0fde6,rebase 到当前 PR HEAD 之上)。

修复:抽取包级纯函数 rpcErrorTurnEnd(err) (tag, ok),同时识别 ErrACPRPCErrCodexRPC 两个 sentinel 并返回对应 backend 标签;process_readloop.go 的错误分支改调它,取代原先内联的 ACP-only errors.Is。新 backend 只需在这一处注册 sentinel,否则其 post-handshake RPC 错误会被静默吞掉。

回归测试 rpc_error_turn_end_test.go:覆盖两个 sentinel 的 wrapped+bare 形式、非 RPC parse 错误(必须合成)、以及一个断言 errors.Is(ErrCodexRPC, ErrACPRPC)==false 的守卫——正是旧代码漏掉的区分点。

完整 internal/cli 测试通过、gofmt 干净。

注:你的本地 naozhi-codex worktree 现落后远程 1 个 commit(本修复),git pull --rebase 即可同步。

@KevinZhao KevinZhao merged commit bfae89f into master Jun 21, 2026
7 checks passed
@KevinZhao KevinZhao deleted the feat/codex-backend branch June 21, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant