外观
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,commite363b08c9175ac1cbe5893615dd2cb9ddf95043b - 当前 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-sol | 019fb1ba-6f46-7682-a110-8b1e116c3fac | V2 | Responses Lite | 密文 | input_text 路由头 + encrypted_content | RESULT=323 |
| gpt-5.6-terra | 019fb1bd-491a-7fb2-91c0-144a1c37ed5e | V2 | Responses Lite | 密文 | input_text 路由头 + encrypted_content | RESULT=323 |
| gpt-5.6-luna | 019fb1bd-491a-7ca0-9711-69e5a7f80b5f | V1 | Responses Lite | 明文 | 普通 user input_text | RESULT=323 |
| gpt-5.5 | 019fb1be-f1dc-7c33-a468-02454e7df7a0 | V1 | 常规 Responses | 明文 | 普通 user input_text | RESULT=323 |
| gpt-5.4 | 019fb1be-f1dc-7a63-8534-a4412fc6ebdf | V1 | 常规 Responses | 明文 | 普通 user input_text | RESULT=323 |
| gpt-5.2 | 019fb1c5-eb77-78e3-83b4-4d00c1e69aef | V1(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-sol | 019fb1c1-bcca-7443-9da5-a3ac23095c70 | V2 | 密文 | RESULT=323 |
| gpt-5.4 | 019fb1c1-bcca-7763-9248-bfdbbc1a4a6e | V1 | 明文 | 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-luna | 019fb1c2-ea5c-73f3-ba59-79f9e1b0f9eb | V1 | V2 | 密文 | input_text + encrypted_content |
| gpt-5.4 | 019fb1c2-ea50-7482-83da-7de8f75a270e | null/fallback V1 | V2 | 密文 | input_text + encrypted_content |
两个样本均返回 RESULT=323。这验证 multi_agent_v2=true 是高优先级 强制 V2 覆盖。
D. features.multi_agent=false
| 模型 | 根 thread ID | 有效版本 | 是否仍有子 agent |
|---|---|---|---|
| gpt-5.6-sol | 019fb1c3-e892-7ce2-8881-a9d157943598 | V2 | 是,且消息加密 |
| gpt-5.4 | 019fb1c3-e892-7980-a3a4-9feaf9e0b6ef | disabled | 否 |
这是本次最重要的配置陷阱之一: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-be82ad23bdd0 | gpt-5.6-sol | V1 | custom_tool_call(exec) 中明文 V1 spawn |
| 子 | 019fb217-a669-7573-a1f5-cd20ad3a8e82 | gpt-5.6-sol | V1 | 普通 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 turn | 019fb20d-eddc-7831-baca-8f7f5f35a3fd | gpt-5.6-luna | V1 | V1_SEEDED |
| resumed turn | 同上 | gpt-5.6-sol | V1 | 明文 V1 spawn |
| child | 019fb20e-c840-71c0-a438-5420ad500c5e | gpt-5.6-sol | V1 | 普通 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-f381bb8dbfb3 | 019fb232-eb7f-7482-8a02-3df5a4340068 | V2/V2 | function_call + encrypted message | agent_message + encrypted_content |
| 自定义目录 | 019fb234-08b4-79c2-922b-0bd625de8558 | 019fb234-3fd1-7e73-9499-8af193917288 | V1/V1 | custom_tool_call(exec) + plaintext message | user 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=truemarker 从 3 个降为 0;- 其余顶层生成参数保持不变。
首次原样重放命中了旧请求去重:返回的 response ID 与历史 resp1 相同。 为排除缓存影响,正式对照的 V2 和转换 V1 请求都删除了 prompt_cache_key 与 client_metadata。这两个字段只用于缓存、会话关联和 遥测,不改变用户输入或工具 schema。
| 重放 | response ID | 状态 | 首个 agent 调用输出 |
|---|---|---|---|
| 新鲜 V2 基线 | resp_09b55452a5da3ad4016a6b1ffa98c48198bf2ec6b3ec21073a | completed | function_call(namespace="collaboration", name="spawn_agent"),message 为密文 |
| 转换后的 V1 工具面 | resp_09b55452a5da3ad4016a6b20163748819898cb6ced9c52876e | completed | custom_tool_call(name="exec"),查询 ALL_TOOLS 中的 spawn_agent/wait_agent |
转换请求没有收到 4xx/5xx 或协议解析错误,SSE 正常产生 response.created、response.output_item.done 和 response.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/responsesAccept: text/event-streamContent-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.txt 至 query4.txt 和 resp1.txt 至 resp4.txt。这些文件包含完整运行上下文,只在本地研究 环境保留,不随公开网站或远端仓库发布。
其中 query1 是父会话首次请求,resp1 包含加密 spawn_agent tool call;query3 是子会话请求,包含 agent_message 的 input_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 格式”属于强格式推断; 仓库中没有该字段的加密/解密或密钥管理代码,不能仅凭格式断言服务端具体 库实现或密钥生命周期。