Skip to content

Codex CLI 模型与 multi-agent 通信实验记录

实验元数据

  • 日期:2026-07-30(Asia/Shanghai)
  • Codex CLI:codex-cli 0.146.0
  • CLI 二进制:/opt/homebrew/bin/codex
  • 包装脚本:$HOME/.local/bin/codex
  • Provider:自定义 Responses provider,https://ai.feei.cn/v1
  • 抓包:Burp Proxy 127.0.0.1:8080
  • 精确版本源码:rust-v0.146.0,commit e363b08c9175ac1cbe5893615dd2cb9ddf95043b
  • 当前 main 对照:commit 6219b7c40fc9c702c0aef9964e72b492558f60e4
  • 工作目录:/private/tmp

本记录不包含 Authorization、Cookie、API key、安装 ID、完整 turn metadata 或完整密文。Burp Intercept 在自动测试前关闭,测试后已恢复开启; Proxy History 全程保留。

固定测试提示

text
You must create exactly one subagent to independently calculate 17*19.
Wait for it, then reply with exactly: RESULT=<number>.
Do not run shell commands or modify files.

通用命令:

bash
$HOME/.local/bin/codex exec \
  --json \
  --skip-git-repo-check \
  -C /private/tmp \
  -m <MODEL> \
  -c features.multi_agent=<BOOL> \
  -c features.multi_agent_v2=<BOOL> \
  '<PROMPT>'

除专门测试版本继承的 E.2 外,每个矩阵单元均使用新会话。不要用 resume 做普通模型比较,因为 multi_agent_version 会写入会话并在恢复时 优先复用。

A. multi_agent=true, multi_agent_v2=false

模型根 thread ID有效版本请求族spawn.message子会话任务体结果
gpt-5.6-sol019fb1ba-6f46-7682-a110-8b1e116c3facV2Responses Lite密文input_text 路由头 + encrypted_contentRESULT=323
gpt-5.6-terra019fb1bd-491a-7fb2-91c0-144a1c37ed5eV2Responses Lite密文input_text 路由头 + encrypted_contentRESULT=323
gpt-5.6-luna019fb1bd-491a-7ca0-9711-69e5a7f80b5fV1Responses Lite明文普通 user input_textRESULT=323
gpt-5.5019fb1be-f1dc-7c33-a468-02454e7df7a0V1常规 Responses明文普通 user input_textRESULT=323
gpt-5.4019fb1be-f1dc-7a63-8534-a4412fc6ebdfV1常规 Responses明文普通 user input_textRESULT=323
gpt-5.2019fb1c5-eb77-78e3-83b4-4d00c1e69aefV1(turn context)常规 Responses未取得未创建连续 503 后失败

模型行为存在两项无害偏差:5.5 额外调用了 exec_command({"cmd":"true"}); 5.4 额外调用了 exec_command({"cmd":"echo invalid"})。两者都违背固定提示 中的“不要运行 shell”,但不改变 spawn message 的明文/密文判定。实验工作 目录是 /private/tmp,这些命令没有修改文件。

5.2 首次会话 019fb1be-f1dc-7b43-a81b-470386545d7f 也失败; 第二次重试经历初始请求加 5 次自动重连后,最终仍收到 503。因服务端没有 返回 tool call,不能把 5.2 的“明文”标为本次流量实证;只能由有效版本和 源码预测它会走 V1 明文路径。

B. multi_agent=true, multi_agent_v2 缺失

使用临时 CODEX_HOME=/private/tmp/codex-protocol-absent-20260730, 仅从 profile 中机械删除 multi_agent_v2 = ...;认证文件只通过临时 符号链接复用,没有复制进研究目录。测试完成后已删除该认证符号链接, 临时 rollout 仍保留。

模型根 thread ID有效版本spawn.message结果
gpt-5.6-sol019fb1c1-bcca-7443-9da5-a3ac23095c70V2密文RESULT=323
gpt-5.4019fb1c1-bcca-7763-9248-bfdbbc1a4a6eV1明文RESULT=323

这两个样本覆盖了“模型目录显式指定 V2”和“模型目录为 null、feature 回退 V1”两条分支。结果证明,在 0.146.0 中“缺失”和“显式 false”都不 构成 V1 强制覆盖。

C. multi_agent=true, multi_agent_v2=true

模型根 thread ID原模型默认有效版本spawn.message子会话任务体
gpt-5.6-luna019fb1c2-ea5c-73f3-ba59-79f9e1b0f9ebV1V2密文input_text + encrypted_content
gpt-5.4019fb1c2-ea50-7482-83da-7de8f75a270enull/fallback V1V2密文input_text + encrypted_content

两个样本均返回 RESULT=323。这验证 multi_agent_v2=true 是高优先级 强制 V2 覆盖。

D. features.multi_agent=false

模型根 thread ID有效版本是否仍有子 agent
gpt-5.6-sol019fb1c3-e892-7ce2-8881-a9d157943598V2是,且消息加密
gpt-5.4019fb1c3-e892-7980-a3a4-9feaf9e0b6efdisabled

这是本次最重要的配置陷阱之一:features.multi_agent=false 关闭的是 V1 feature fallback,不能压过模型目录里 Sol 的显式 V2。

补测:

toml
[agents]
enabled = false

对应 CLI override -c agents.enabled=false。Sol 根 thread 019fb1c5-9b15-7ee2-a8b9-942c0dcf5f96 的有效版本为 disabled, 模型返回 NO_SUBAGENT_TOOL。这才是全局强制禁用。

E. 强制 Sol 使用 V1

E.1 model_catalog_json:全新 Sol V1 会话

0.146.0 支持顶层 model_catalog_json。测试时临时把完整内置目录中 Sol 的 字段从:

json
"multi_agent_version": "v2"

改为:

json
"multi_agent_version": "v1"

并执行:

bash
$HOME/.local/bin/codex exec \
  --json --skip-git-repo-check -C /private/tmp \
  -m gpt-5.6-sol \
  -c features.multi_agent=true \
  -c features.multi_agent_v2=false \
  -c 'model_catalog_json="/absolute/path/models-v1.json"' \
  'You must create exactly one subagent to independently calculate 31*37...'
角色thread ID模型有效版本任务表示
019fb217-68a1-73c2-be0b-be82ad23bdd0gpt-5.6-solV1custom_tool_call(exec) 中明文 V1 spawn
019fb217-a669-7573-a1f5-cd20ad3a8e82gpt-5.6-solV1普通 user input_text

结果为 RESULT=1147。测试后内置目录副本已恢复 Sol=V2;Burp Intercept 已恢复开启。这个结果证明无需重编译即可为新 Sol thread提供 V1 模型 目录,但升级版本后必须重新验证目录 schema。

E.2 V1 thread 继承:Luna seed 后切换 Sol

先用 Luna 建立 V1 thread,再恢复同一 thread 并指定 Sol:

bash
codex exec -m gpt-5.6-luna \
  -c features.multi_agent=true \
  -c features.multi_agent_v2=false \
  'Reply exactly: V1_SEEDED'

codex exec resume -m gpt-5.6-sol \
  -c features.multi_agent=true \
  -c features.multi_agent_v2=false \
  019fb20d-eddc-7831-baca-8f7f5f35a3fd \
  'You must create exactly one subagent to independently calculate 23*29...'
阶段thread ID模型有效版本结果
seed turn019fb20d-eddc-7831-baca-8f7f5f35a3fdgpt-5.6-lunaV1V1_SEEDED
resumed turn同上gpt-5.6-solV1明文 V1 spawn
child019fb20e-c840-71c0-a438-5420ad500c5egpt-5.6-solV1普通 user input_text

结果为 RESULT=667。CLI 发出模型不匹配警告;恢复轮还携带 seed thread 的 历史和 base instructions,因此这只适合作协议对照。若设置 multi_agent_v2=true,配置 override 会压过已存 V1,使其回到 V2。

E.3 同一输入的 Sol V2/V1 配对实验

两组均使用完全相同的用户输入:

text
You must create exactly one subagent to independently calculate 41*43.
Wait for it, then reply with exactly: PAIR_RESULT=<number>.
Do not run shell commands or modify files.
根 thread子 thread根/子版本父 spawn 形态子任务形态
默认目录019fb232-d524-77e0-9854-f381bb8dbfb3019fb232-eb7f-7482-8a02-3df5a4340068V2/V2function_call + encrypted messageagent_message + encrypted_content
自定义目录019fb234-08b4-79c2-922b-0bd625de8558019fb234-3fd1-7e73-9499-8af193917288V1/V1custom_tool_call(exec) + plaintext messageuser message + plaintext input_text

两组都返回 PAIR_RESULT=1763。默认 V2 调用选择了 fork_turns="all";V1 调用省略 fork_context,默认不 fork。这个历史量 差异是模型选择/工具默认值造成的,不是加密机制本身。完整逐阶段脱敏报文 对比见主报告 5.4。

E.4 Proxy 重放:把已采集 Sol V2 Query 转为 V1 工具面

本实验不修改 Codex 配置,也不启动本地 tool handler;直接把已采集的 query1.txt 通过 Burp 127.0.0.1:8080 重放到同一 POST /v1/responses endpoint。转换器 v2-lite-to-v1-lite.jq 只对 input[type="additional_tools"].tools 做如下修改:

diff
- collaboration namespace(含 spawn_agent/send_message/followup_task)
  exec / wait / request_user_input 保持不变

这是本次真实 V1 Sol/Responses Lite 样本的工具面差异:V1 multi-agent 工具作为 exec 的 deferred/nested tools 使用,不直接暴露 collaboration namespace。转换后检查结果为:

  • model 仍为 gpt-5.6-sol
  • input 仍为 11 项;
  • 用户最后输入仍为“使用 subagent 计算一下,我要结果”;
  • encrypted=true marker 从 3 个降为 0;
  • 其余顶层生成参数保持不变。

首次原样重放命中了旧请求去重:返回的 response ID 与历史 resp1 相同。 为排除缓存影响,正式对照的 V2 和转换 V1 请求都删除了 prompt_cache_keyclient_metadata。这两个字段只用于缓存、会话关联和 遥测,不改变用户输入或工具 schema。

重放response ID状态首个 agent 调用输出
新鲜 V2 基线resp_09b55452a5da3ad4016a6b1ffa98c48198bf2ec6b3ec21073acompletedfunction_call(namespace="collaboration", name="spawn_agent")message 为密文
转换后的 V1 工具面resp_09b55452a5da3ad4016a6b20163748819898cb6ced9c52876ecompletedcustom_tool_call(name="exec"),查询 ALL_TOOLS 中的 spawn_agent/wait_agent

转换请求没有收到 4xx/5xx 或协议解析错误,SSE 正常产生 response.createdresponse.output_item.doneresponse.completed。 这直接证明:在网络层,服务端接受从 V2 Query 转换得到的 V1/Code Mode 工具面,并相应切换了模型的 tool-call 形态。

本实验尚不证明“未改配置的 V2 CLI 可以端到端执行该响应”。模型下一步要 通过 exec 查询/调用 multi_agent_v1__spawn_agent,但本地 V2 会话构建的 ToolRegistry 并未注册 V1 handler。要验证完整 CLI 流程,Proxy 还必须把 V1 custom call/输出双向翻译为本地 V2 handler 能识别的调用,并继续转换 child request。SSE 摘要由 summarize-responses-sse.jq 生成,密文未写入 实验记录。

抓包结构证据

对六个根 thread 定向查询 Burp Proxy History,均看到:

  • POST /v1/responses
  • Accept: text/event-stream
  • Content-Type: application/json
  • SSE 响应

Sol、Terra、Luna:

  • 存在 X-Openai-Internal-Codex-Responses-Lite: true
  • 请求前缀包含 input[0].type = "additional_tools"
  • 没有顶层 instructions

5.5、5.4、5.2:

  • 不存在 Responses Lite header
  • 不存在 additional_tools
  • 存在非空顶层 instructions
  • 源码构造非空顶层 tools

旧的四组完整 Sol 抓包正文为本地证据文件 query1.txtquery4.txtresp1.txtresp4.txt。这些文件包含完整运行上下文,只在本地研究 环境保留,不随公开网站或远端仓库发布。

其中 query1 是父会话首次请求,resp1 包含加密 spawn_agent tool call;query3 是子会话请求,包含 agent_messageinput_text + encrypted_content

加密触发字段

query1 的实际 tool schema 中存在三个参数标记:

text
collaboration.spawn_agent.parameters.properties.message.encrypted = true
collaboration.send_message.parameters.properties.message.encrypted = true
collaboration.followup_task.parameters.properties.message.encrypted = true

这是服务端识别“对应 argument 需要应用层加密”的直接 wire marker。 features.multi_agent_v2 本身没有出现在请求 body;它只在客户端选择 V2 tool schema。V1 schema 的 message 没有 encrypted key。

不要混淆:

  • include=["reasoning.encrypted_content"] 只控制 reasoning envelope;
  • Responses Lite header 只控制请求布局;
  • 子请求 type="encrypted_content" 表示消费/回放既有密文。

rollout 证据提取

根会话中 V2 spawn 的脱敏形态:

json
{
  "type": "function_call",
  "name": "spawn_agent",
  "arguments": {
    "task_name": "calculate",
    "fork_turns": "all",
    "message": "gAAAAA...<redacted>"
  }
}

子会话中 V2 任务输入的脱敏形态:

json
{
  "type": "agent_message",
  "author": "/root",
  "recipient": "/root/calculate",
  "content": [
    {
      "type": "input_text",
      "text": "Message Type: NEW_TASK\nTask name: /root/calculate\nSender: /root\nPayload:\n"
    },
    {
      "type": "encrypted_content",
      "encrypted_content": "gAAAAA...<redacted>"
    }
  ]
}

Luna V1 的实际模型输出包含:

javascript
const spawned = await tools.multi_agent_v1__spawn_agent({
  message: "Independently calculate 17*19. Reply with only the numeric result."
});

5.5/5.4 V1 则在 rollout 中记录为直接 function_call,arguments 的 message 为明文。

密文格式检查

对 Sol 样本的 token 只做结构解码,不保存完整值:

  • URL-safe Base64 解码长度:201 bytes
  • byte 0:0x80
  • bytes 1..8:大端时间戳 1785393220
  • 时间戳:2026-07-30T06:33:40Z
  • 后续布局:16-byte IV + 144-byte 块对齐密文 + 32-byte MAC

这与 Fernet token 布局精确一致。因此“Fernet 格式”属于强格式推断; 仓库中没有该字段的加密/解密或密钥管理代码,不能仅凭格式断言服务端具体 库实现或密钥生命周期。