外观
Codex CLI 模型协议与 multi-agent 参数加密研究
摘要
本报告分析 Codex CLI 0.146.0 在六个模型、多个 multi-agent 配置下的 真实 HTTPS/SSE 通信、rollout 会话记录及对应 Rust 源码。核心结论如下。
- Codex CLI 对所有本次模型都调用
POST /v1/responses,服务端通过 Server-Sent Events(SSE)返回类型化事件。模型差异不是 Chat Completions 与 Responses 的切换,而是同一 endpoint 下的“常规 Responses”和“Responses Lite”两种序列化布局。 - 0.146.0 的内置模型目录指定 Sol、Terra 为 MultiAgent V2,Luna 为 V1;5.5、5.4、5.2 未指定版本,交给 feature fallback。
multi_agent_v2=false不等于强制 V1。它只表示“不强制 V2”; 之后模型目录仍可选择 V2。因此当前新会话中 Sol 和 Terra 都加密, Luna/5.5/5.4 为 V1 明文。multi_agent_v2=true是强制 V2 覆盖;实测可把 Luna 和 5.4 从 V1 切换到 V2。features.multi_agent=false也不是全局强制禁用。它只让没有模型目录 版本的模型回退为 disabled;Sol 仍可由模型目录启用 V2。真正的全局 禁用是[agents] enabled=false。- V2 只对
spawn_agent.message、send_message.message和followup_task.message标记应用层字段加密。工具名、task name、 sender/recipient、fork 参数、模型覆盖、流量长度以及父/子 thread metadata 仍是明文。 - 客户端没有实现该消息的加密或解密。它在 tool schema 中发送
"encrypted": true,Responses 服务返回 opaque 密文,客户端原样 封装为子会话的encrypted_content再发给服务端。这不是“本地 agent 之间的端到端加密”,而是服务端可解释、客户端不可解释的 opaque envelope。 - 实测密文严格符合 Fernet token 布局,但源码没有披露算法与密钥; 因此 Fernet 只能标为强格式推断,不能标为官方实现事实。
- 服务端判断“这个 tool argument 需要加密”的直接线上标记不是
multi_agent_v2,而是 tool parameter JSON Schema 中的parameters.properties.message.encrypted: true。multi_agent_v2仅在客户端本地决定是否生成带该标记的 V2 tool schema。 - 0.146.0 没有
force_v1=true之类的单字段开关,但可以用model_catalog_json提供一个把 Sol 标为 V1 的完整模型目录。实测这会让 全新的 Sol 根会话和子会话均使用 V1 明文。恢复既有 V1 thread 再 切换到 Sol 也有效,但会继承旧历史,隔离性较差。
完整脱敏实验矩阵见 ../experiments/2026-07-30-model-multi-agent-matrix.md。
1. 范围与证据等级
1.1 研究对象
- Codex CLI:
0.146.0 - 精确 tag:
rust-v0.146.0 - 精确 commit:
e363b08c9175ac1cbe5893615dd2cb9ddf95043b - 当前 main 对照 commit:
6219b7c40fc9c702c0aef9964e72b492558f60e4 - 模型:
gpt-5.6-sol、gpt-5.6-terra、gpt-5.6-luna、gpt-5.5、gpt-5.4、gpt-5.2 - Provider:
https://ai.feei.cn/v1,wire_api = "responses" - 初始配置:
toml
[features]
multi_agent = true
multi_agent_v2 = false1.2 证据等级
- 强证据:Burp 中的实际请求/响应、对应 rollout JSONL、0.146.0 精确源码三者相互印证。
- 中等证据:精确源码与 rollout 有效状态一致,但因上游错误未取得 完整成功响应,例如 5.2 的 V1 选择。
- 推断:仅由二进制格式或当前行为推导,源码/公开文档未正式披露, 例如 Fernet 的具体服务端实现。
1.3 限制
本次请求经过第三方自定义 provider,而不是直接请求 api.openai.com。这不影响“客户端生成了什么 HTTP/JSON”这一观察, 但服务可用性、模型目录的远端覆盖以及响应内容可能受 provider 影响。 5.2 连续返回 503,是本次无法完成该模型 tool-call 实证的直接原因。
Burp 已安装受信任 CA,因此本文所称“明文”是指 TLS 解密后 JSON 中仍为 可读文本;“密文”是指 TLS 内部还存在一层应用字段密文。未配置 MITM 时, 无论 V1/V2,公网链路都仍受 TLS 保护。
2. 总体通信架构
text
用户提示 / 历史 / AGENTS.md / tools
|
v
TurnContext + ModelInfo
|
+-- 选择 multi_agent_version
+-- 选择 use_responses_lite
|
v
ResponsesApiRequest 序列化
|
v
POST /v1/responses Accept: text/event-stream
|
v
SSE 类型化 ResponseEvent
|
+-------+--------+
| |
assistant text tool call
|
v
ToolRouter/handler
|
spawn child Session/Thread
|
v
新的 /v1/responses 请求CLI 不是把一个“子 agent HTTP API”直接暴露给模型。模型首先返回普通 Responses tool call,客户端在本地执行 spawn_agent handler,创建新的 Codex thread/session;子 session 再自行向同一个 Responses endpoint 发送模型请求。
3. 模型目录决定两项独立行为
0.146.0 内置目录位于 models.json。
| 模型 | multi_agent_version | use_responses_lite | 基线有效版本 |
|---|---|---|---|
| gpt-5.6-sol | V2 | true | V2 |
| gpt-5.6-terra | V2 | true | V2 |
| gpt-5.6-luna | V1 | true | V1 |
| gpt-5.5 | null | false | V1 fallback |
| gpt-5.4 | null | false | V1 fallback |
| gpt-5.2 | null | false | V1 fallback |
这里有两个互相独立的轴:
multi_agent_version决定工具面与消息是否使用 V2 opaque envelope。use_responses_lite决定整个 Responses request 的字段摆放方式。
所以不能简单说“5.6 都加密”或“Responses Lite 都加密”。Luna 是最清楚 的反例:它使用 Responses Lite,但 multi-agent 是 V1 明文。
当前 main 的这六项值与 0.146.0 相同,但报告仍以安装版本为结论边界。
4. multi-agent 版本选择算法
核心实现在 config/mod.rs:
rust
override =
if features.multi_agent_v2 { V2 }
else if !agents.enabled { Disabled }
else { None }
effective =
override
?? model_catalog.multi_agent_version
?? (features.multi_agent ? V1 : Disabled)优先级从高到低:
features.multi_agent_v2=true:强制 V2。[agents] enabled=false:强制 Disabled。- 已存入会话的 multi-agent version(恢复/继承场景)。
- 当前模型目录的
multi_agent_version。 features.multi_agent=true回退 V1,否则 Disabled。
4.1 为什么 multi_agent_v2=false 下 Sol 仍加密
false 只让第一条 override 不成立,不会产生 Some(V1)。随后 Sol 模型目录的 V2 被采用。这是观察到 Sol 加密的直接原因,不是 Sol 客户端有另一套隐藏加密代码。
4.2 为什么 Terra 的旧观察可能是明文
本次新会话的三条证据一致:
- 0.146.0 内置目录:Terra = V2。
- parent rollout:
multi_agent_version = "v2"。 - spawn
message与 childencrypted_content:均为同一 opaque token。
旧观察出现 Terra 明文,合理原因包括:
- 使用了旧 CLI 或旧模型目录;
- provider 的远端模型目录当时覆盖了内置值;
- 恢复了历史 V1 会话;
- 抓到的是最终结果回传,而不是初始任务消息;
multi_agent_v2=false被误解为强制 V1。
其中“历史会话粘性”有明确源码依据: session/mod.rs 会优先读取已记录版本;缺少运行时 metadata 的旧 resumed/forked 会话还会 兼容回退 V1。比较模型时必须每次创建新会话。
4.3 multi_agent=false 的陷阱
features.multi_agent 对应 Feature::Collab,其默认值本来就是 true: features/src/lib.rs。
把它设成 false 只改变最后一层 fallback。实测:
- Sol 仍从模型目录得到 V2,并成功 spawn。
- 5.4 模型目录为 null,最终得到 Disabled。
agents_enabled 来自独立的 [agents].enabled,默认 true: config/mod.rs。 要对所有模型强制禁用应使用:
toml
[agents]
enabled = false4.4 如何让 Sol 强制使用 V1
0.146.0 的普通 feature override 只有三种结果:
multi_agent_v2=true返回 V2;agents.enabled=false返回 Disabled;- 其余情况返回
None,继续采用会话版本或模型目录版本。
因此不存在下面这种可直接使用的配置:
toml
# 不存在;不要据此配置
force_v1 = true但源码支持顶层 model_catalog_json,可以加载一个完整的自定义模型目录: config/mod.rs。 将内置 models.json 复制到实验目录,只修改 Sol 条目:
json
{
"slug": "gpt-5.6-sol",
"multi_agent_version": "v1",
"use_responses_lite": true
}然后启动一个全新的 Sol thread:
bash
codex exec \
-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"' \
'<prompt>'注意文件必须是包含 models 数组的完整有效目录,不能只放上述三个 字段。2026-07-30 实测根 thread 019fb217-68a1-73c2-be0b-be82ad23bdd0 与子 thread 019fb217-a669-7573-a1f5-cd20ad3a8e82 均为 model=gpt-5.6-sol、 multi_agent_version=v1。父调用中出现明文:
javascript
const spawned = await tools.multi_agent_v1__spawn_agent({
message: "Independently calculate 31*37..."
});子会话收到普通 user input_text,没有 encrypted_content,结果为 RESULT=1147。这证明 model_catalog_json 不只是影响模型选择 UI,而是 实际进入 resolve_multi_agent_version_for_model()。它仍是研究用目录覆盖, 升级 CLI 后应重新核对目录 schema 和 rollout 的有效版本。
第二种已验证方法是会话继承:先用 Luna 建立 V1 thread,再以 Sol 恢复:
bash
codex exec -m gpt-5.6-luna \
-c features.multi_agent=true \
-c features.multi_agent_v2=false \
'<seed prompt>'
codex exec resume -m gpt-5.6-sol \
-c features.multi_agent=true \
-c features.multi_agent_v2=false \
<THREAD_ID> '<spawn prompt>'实测 thread 019fb20d-eddc-7831-baca-8f7f5f35a3fd 的 Luna 首轮和 Sol 恢复轮均为 V1,其 Sol 子 thread 019fb20e-c840-71c0-a438-5420ad500c5e 也为 V1,明文任务 23*29 得到 RESULT=667。原因是会话中已记录的 V1 在模型目录的 Sol V2 之前被采用;但是 multi_agent_v2=true 仍会覆盖为 V2。
继承法会触发模型不匹配警告,并携带原 thread 历史和可能由 Luna 选定的 base instructions。它适合比较协议,不适合作为“干净的新 Sol V1”方案。 不要手工篡改 rollout metadata;该做法不受支持,也容易造成会话状态不一致。
5. 两种请求正文布局
共同 endpoint:
http
POST /v1/responses HTTP/1.1
Accept: text/event-stream
Content-Type: application/json
Authorization: Bearer <redacted>共同语义字段包括:
modelinputtool_choice: "auto"reasoningstorestream: trueinclude: ["reasoning.encrypted_content"]prompt_cache_keytextclient_metadata
按 0.146.0 ResponsesApiRequest 结构,完整顶层字段语义为:
| 字段 | 类型/本次值 | 含义 |
|---|---|---|
model | string | 本 turn 请求的模型 slug;可与 thread 初建模型不同 |
instructions | string,可省略 | 常规 Responses 的主开发者指令;空字符串不序列化,Lite 改放 developer message |
input | ResponseItem[] | 按顺序回放的消息、reasoning、call、output、agent/compaction items |
tools | array,可省略 | 常规 Responses 的工具 schema;Lite 改放 additional_tools |
tool_choice | 本次 "auto" | 允许模型自行决定回答还是调用工具 |
parallel_tool_calls | boolean | 是否允许并行调用;Lite 分支强制 false |
reasoning | object/null | effort、summary,GPT-5.6 Lite 另含 context="all_turns" |
store | 本次 false | 不让 Responses 服务作为普通持久 response state 使用 |
stream | 本次 true | 使用 SSE 增量返回 |
stream_options | object,可省略 | 可请求特定 reasoning summary delivery 行为 |
include | string array | 本次请求 reasoning.encrypted_content,用于无状态 reasoning 回放 |
service_tier | string,可省略 | 可选服务等级覆盖 |
prompt_cache_key | string,可省略 | 把同 thread 请求归入稳定 cache key |
text | object,可省略 | 输出 verbosity 和可选 JSON Schema format |
client_metadata | string map,可省略 | session/turn/window/安装信息和 Code Mode 工具名映射等 |
HTTP/SSE 路径没有单独的顶层 multi_agent_v2、multi_agent_version 或 encrypt_subagent_message 字段。multi-agent 选择结果体现在工具集合、工具 参数 schema 和后续 item 类型里。
官方 Responses API 同样定义 input、instructions、tools、 parallel_tool_calls、reasoning、stream 和 typed SSE items。参考 Create a model response 与 Migrate to the Responses API。 Codex 的 additional_tools、tool-parameter encrypted、 agent_message 以及 x-codex-* metadata 是额外扩展,不能当作普通 Responses client 的通用格式。
5.1 Responses Lite:Sol/Terra/Luna
源码分支: client.rs。
脱敏结构:
json
{
"model": "gpt-5.6-sol",
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [
{"type": "custom", "name": "exec"},
{"type": "function", "name": "wait"},
{"type": "function", "name": "request_user_input"},
{"type": "namespace", "name": "collaboration", "tools": ["..."]}
]
},
{
"type": "message",
"role": "developer",
"content": [{"type": "input_text", "text": "<base instructions>"}]
},
"... history and user messages ..."
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"reasoning": {
"effort": "high",
"summary": "detailed",
"context": "all_turns"
},
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"client_metadata": {"...": "..."}
}特征:
- HTTP header:
X-Openai-Internal-Codex-Responses-Lite: true。 - tool definitions 被放进
input[0]的additional_tools。 - base instructions 被改写成 developer message。
- 顶层
instructions和tools被省略。 - 即便模型目录称支持并行 tool calls,客户端仍把
parallel_tool_calls强制为 false。 - GPT-5.6 的 reasoning request 增加
context: "all_turns"。
完整 query1.txt 中,input[0] 是 additional_tools,随后依次是多个 developer message、环境/用户 message 和历史 assistant/user message。 这解释了为什么抓包看起来不像常见的 {instructions, input, tools}。
5.2 常规 Responses:5.5/5.4/5.2
脱敏结构:
json
{
"model": "gpt-5.4",
"instructions": "<base instructions>",
"input": ["... messages and response items ..."],
"tools": ["... top-level tool definitions ..."],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {
"effort": "high",
"summary": "detailed"
},
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"client_metadata": {"...": "..."}
}特征:
- 没有 Responses Lite header。
- base instructions 位于顶层
instructions。 - tool definitions 位于顶层
tools。 parallel_tool_calls不会因布局被强制 false。- 非 Lite 模型省略
reasoning.context,让 Responses 使用其默认值。
5.3 input[] 到底是什么
input 不是 Chat Completions 风格的纯消息数组,而是一个有序的 ResponseItem 事件历史。它同时承载消息、模型 reasoning、工具调用、工具 结果、agent 路由消息和压缩状态。0.146.0 在 models.rs 用带 type discriminator 的 ResponseItem enum 表示这些项目。
最常见的消息形态是:
json
{
"type": "message",
"id": "msg_...",
"role": "user",
"content": [
{"type": "input_text", "text": "<prompt>"}
]
}role 在本次抓包中的含义如下。
role | 谁提供 | Codex 中的用途 | 常见 content |
|---|---|---|---|
developer | 客户端/产品 | base instructions、权限、模式、plugins、skills、子 agent 规则 | input_text |
user | 用户或 V1 子任务注入 | 用户问题、AGENTS.md、environment_context、V1 child prompt | input_text/input_image/input_audio |
assistant | 先前模型输出 | commentary 或 final answer 的历史回放 | output_text |
官方 Responses schema 还允许 system;本次 Lite 抓包没有使用它,Codex 把产品级指令序列化成一个或多个 developer message。常规 Responses 则把 主要 base instructions 放在顶层 instructions。additional_tools 上的 role="developer" 表示工具声明属于开发者上下文,不代表一个开发者说话 消息。
普通 message 的 content[] 支持:
input_text:输入给模型的文字;input_image:图片 URL/data URL,可带detail;input_audio:音频 data URL;output_text:先前 assistant 的文字输出。
agent_message.content[] 是另一套更窄的 union,只允许 input_text 和 encrypted_content。这正是 V2 child request 能把明文路由头与 opaque task token 放在同一个 item 内的原因。
0.146.0 客户端可序列化/解析的完整 ResponseItem 类型如下。后半部分 不一定出现在本次简单计算中,但属于同一 wire union。
type | 关键字段 | 含义 |
|---|---|---|
additional_tools | role, tools | Lite 专用的内联工具声明前缀 |
message | role, content, phase | developer/user/assistant 消息;phase 可为 commentary 或 final_answer |
agent_message | author, recipient, content | V2 父子 agent 私有路由 envelope |
reasoning | summary, content, encrypted_content | 可回放的 reasoning item;密文与子任务加密是不同机制 |
function_call | name, namespace, arguments, call_id | JSON-schema function 工具调用;arguments 在线上是“包含 JSON 的字符串” |
function_call_output | call_id, output | 本地 handler 结果,通过 call_id 对应调用 |
custom_tool_call | name, input, call_id | freeform/custom 工具调用,例如 Lite 的 exec JavaScript |
custom_tool_call_output | call_id, output | custom 工具结果 |
local_shell_call | call_id, status, action | provider 原生 local-shell 调用表示 |
tool_search_call | call_id, execution, arguments | 延迟工具搜索/选择调用 |
tool_search_output | call_id, status, tools | 工具搜索结果 |
web_search_call | status, action | 服务端 web search item |
image_generation_call | status, revised_prompt, result | 图像生成 item |
compaction | encrypted_content | 兼容旧名称 compaction_summary 的加密压缩结果 |
compaction_trigger | 无 | 请求服务端执行压缩的控制 item,不是持久结果 |
context_compaction | encrypted_content | 新的上下文压缩 item |
未知 type 会被反序列化为内部 Other 兜底,而不是让整个历史解析失败。 工具 output 在线上既可为普通 string,也可为结构化 content items;结构化 结果支持 input_text、input_image、input_audio 和 encrypted_content。
query1 到 query4 的 input 增长
旧 Sol V2 样本展示了完整的 stateless replay:
| 请求 | 在前一请求基础上新增的关键 item | 客户端目的 |
|---|---|---|
query1 | 工具声明、全部 developer/user/history message | 让父模型决定首次动作 |
query2 | reasoning、assistant commentary、加密 function_call、匹配的 function_call_output | 告诉父模型 spawn 已由本地完成 |
query3 | fork 的历史、child developer instructions、V2 agent_message | 作为新子 thread 的首次模型请求 |
query4 | 父历史、spawn/wait 调用及输出、child 到 parent 的 agent_message | 让父模型消费子结果并生成最终回答 |
这里没有使用 previous_response_id。因为 store=false,Codex 把需要保留的 项目显式重新放回后续请求的 input[];include 中的 reasoning.encrypted_content 使 opaque reasoning 也能在 stateless 请求中 回放。
子 agent 首次请求实际发送什么
严格说不是“子 agent 先自行发内容”,而是父模型产生 spawn tool call 后, CLI 创建 child session,再代表它构造第一次 /v1/responses 请求。该请求 通常包含:
- child 可用工具的定义(Lite 为
additional_tools); - base/developer instructions、权限、collaboration mode、plugins/skills;
AGENTS.md与 child 的 environment context;- 按
fork_turns/V1fork_context选择继承的父历史; - child 专属开发者指令与父子 agent 路由规则;
- 最后才是这次任务本身。
任务本身在 V1/V2 的差别最清楚:
jsonc
// V1
{"type":"message","role":"user",
"content":[{"type":"input_text","text":"<plaintext task>"}]}
// V2
{"type":"agent_message","author":"/root","recipient":"/root/task",
"content":[
{"type":"input_text","text":"Message Type: NEW_TASK\n...\nPayload:\n"},
{"type":"encrypted_content","encrypted_content":"<opaque task>"}
]}所以 V2 只隐藏 task payload,不隐藏 child 使用的工具、developer 指令、 路由头、继承的明文历史或父子 thread 关系。fork_turns="all" 尤其会让 父历史占据 child request 的大部分内容;none 才能显著减少这部分复制。
5.4 同一用户输入:Sol V2 与 Sol V1 逐报文对比
为避免把不同 prompt 导致的模型输出差异误认为协议差异,2026-07-30 使用 完全相同的用户输入做了一组配对实验:
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.除 model_catalog_json 外,模型和 feature 参数相同:
text
共同:-m gpt-5.6-sol
共同:-c features.multi_agent=true
共同:-c features.multi_agent_v2=false
V2:不设置 model_catalog_json,Sol 目录值为 v2
V1:设置 model_catalog_json,且只把 Sol 的目录值改为 v1| 组 | 根 thread | 子 thread | 根/子有效版本 | 结果 |
|---|---|---|---|---|
| 默认 Sol | 019fb232-d524-77e0-9854-f381bb8dbfb3 | 019fb232-eb7f-7482-8a02-3df5a4340068 | V2/V2 | PAIR_RESULT=1763 |
| 自定义目录 Sol | 019fb234-08b4-79c2-922b-0bd625de8558 | 019fb234-3fd1-7e73-9499-8af193917288 | V1/V1 | PAIR_RESULT=1763 |
阶段 1:父会话首次 Query
两边请求的 endpoint、model、用户 message 和 Lite 布局相同:
jsonc
{
"model": "gpt-5.6-sol", // 相同
"input": [
{
"type": "additional_tools", // 相同
"role": "developer", // 相同
"tools": [/* 这里不同 */]
},
/* 相同类别的 developer/environment items */,
{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"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."
}]
}
],
"parallel_tool_calls": false, // 相同
"store": false, // 相同
"stream": true, // 相同
"include": ["reasoning.encrypted_content"] // 相同且与子任务加密无关
}决定性区别只在客户端生成的工具面。默认 V2 的 additional_tools.tools 直接包含:
json
{
"type": "namespace",
"name": "collaboration",
"tools": [{
"type": "function",
"name": "spawn_agent",
"parameters": {
"type": "object",
"properties": {
"task_name": {"type": "string"},
"fork_turns": {"type": "string"},
"message": {
"type": "string",
"encrypted": true
}
},
"required": ["task_name", "message"]
}
}]
}本次 V1 Sol Query 中没有直接的 collaboration namespace,也没有任何 message.encrypted=true。因为 Sol 是 Code Mode/Responses Lite,V1 agent 工具作为本地 exec 的 deferred/nested tools 提供;模型可通过 ALL_TOOLS 发现 multi_agent_v1__spawn_agent,但服务端收到的顶层可调用工具是 freeform exec:
json
{
"type": "custom",
"name": "exec",
"description": "Run JavaScript code to orchestrate/compose tool calls..."
}首次 Query 的最小结构 diff 因而是:
diff
model = "gpt-5.6-sol" # 相同
X-Openai-Internal-Codex-Responses-Lite = true # 相同
input.user.content = 同一条 41*43 prompt # 相同
- V2: additional_tools 包含 collaboration.spawn_agent
- V2: collaboration.spawn_agent.message.encrypted = true
+ V1: 没有直接 collaboration namespace
+ V1: 通过 custom exec 调用本地 multi_agent_v1__* nested tools
+ V1: 不存在 encrypted=true markermodel_catalog_json 路径、文件内容以及本地 TurnContext.multi_agent_version 都没有出现在这个 HTTP body 中。
阶段 2:父模型的首次 spawn 返回
对于同一个用户目标,V2 返回标准/namespaced function call。密文在 SSE 到达客户端时已经生成:
json
{
"type": "function_call",
"name": "spawn_agent",
"namespace": "collaboration",
"arguments": "{\"task_name\":\"calculate_pair\",\"fork_turns\":\"all\",\"message\":\"gAAAAA...<redacted>\"}",
"call_id": "call_<redacted>"
}V1 返回 freeform/custom call,任务明文位于 JavaScript input:
json
{
"type": "custom_tool_call",
"name": "exec",
"input": "const spawned = await tools.multi_agent_v1__spawn_agent({message: \"Independently calculate 41*43. Return only the numeric result. Do not use shell commands or modify files.\"});\ntext(spawned);",
"call_id": "call_<redacted>"
}这是最直观的响应差异:
diff
- V2: function_call.arguments.message = "gAAAAA..."
+ V1: custom_tool_call.input = "...spawn_agent({message: \"明文\"})..."两次生成的子任务措辞略有不同,属于模型生成的非确定性,不是协议字段差异; 协议差异是 V2 的 function_call + encrypted argument 与 V1 的 custom_tool_call + plaintext freeform input。
阶段 3:父会话续 Query
工具由本地执行完毕后,Codex 将 call 和 output 回放进下一次 input[]。 V2 回放:
jsonc
[
{
"type": "function_call",
"name": "spawn_agent",
"namespace": "collaboration",
"arguments": "{...\"message\":\"gAAAAA...<redacted>\"}",
"call_id": "call_1"
},
{
"type": "function_call_output",
"call_id": "call_1",
"output": "{\"task_name\":\"/root/calculate_pair\"}"
}
]V1 回放:
jsonc
[
{
"type": "custom_tool_call",
"name": "exec",
"input": "const spawned = await tools.multi_agent_v1__spawn_agent({message: \"Independently calculate 41*43...\"});",
"call_id": "call_1"
},
{
"type": "custom_tool_call_output",
"call_id": "call_1",
"output": [{
"type": "input_text",
"text": "{\"agent_id\":\"<redacted>\",\"nickname\":\"<redacted>\"}"
}]
}
]因此 V1 task 不仅在父 SSE response 中是明文,还会在父续 Query 的历史 回放中再次以明文出现。V2 回放的是同一个 opaque token。
阶段 4:子会话首次 Query
V2 child 的任务 item 是:
json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/calculate_pair",
"content": [
{
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: /root/calculate_pair\nSender: /root\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "gAAAAA...<same redacted token>"
}
]
}V1 child 的任务 item 是:
json
{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "Independently calculate 41*43. Return only the numeric result. Do not use shell commands or modify files."
}]
}本次 V2 模型另外选择了 fork_turns="all",而 V1 调用省略 fork_context,默认不 fork。这会让 V2 child 额外带上父历史,属于模型 选择/两个工具接口默认值的差异,不能归因于加密本身。只比较任务 item 时, 协议差异仍是:
diff
- V2: type = agent_message
- V2: content = 明文路由头 + encrypted_content
+ V1: type = message, role = user
+ V1: content = plaintext input_text服务端究竟识别了什么
服务端并未识别一个显式“V1 标志”。它看到的是两套不同的可调用接口:
text
V2 Query
collaboration.spawn_agent.message.encrypted = true
|
v
服务端将该 argument 封装为 opaque token
|
v
V2 child 用 type=encrypted_content 消费 token
V1 Query
没有 encrypted=true;调用路径是普通 freeform exec
|
v
服务端按普通模型输出返回 JavaScript 明文
|
v
V1 child 用 role=user + input_text 消费明文因此最准确的说法是:服务端识别的是参数级加密标记和后续 encrypted_content 类型,而不是 model_catalog_json 或一个线上 V1/V2 协商字段。
5.5 不同模型的实际 tool-call 报文差异
| 模型/有效版本 | 请求布局 | 父模型返回的调用 item | task message |
|---|---|---|---|
| Sol/Terra 默认 V2 | Lite | namespaced function_call:collaboration.spawn_agent | arguments 中为密文 |
| Luna V1 | Lite | custom_tool_call(name="exec"),JavaScript 调用 multi_agent_v1__spawn_agent | input 中为明文 |
| Sol + 自定义目录 V1 | Lite | 与 Luna V1 相同的 exec/V1 deferred tool 路径 | input 中为明文 |
| 5.5/5.4 V1 | 常规 | 直接 function_call(name="spawn_agent") | arguments 中为明文 |
| 5.2 V1 | 常规 | 预计同 5.5/5.4;本次 503 未取得输出 | 源码预测明文 |
| 5.4 + 强制 V2 | 常规 | 直接 V2 function_call(name="spawn_agent") | arguments 中为密文 |
所以“请求格式不同”至少包含两个独立选择:Lite/常规决定工具定义放在哪里, V1/V2 决定暴露哪组 agent 工具以及 message 是否带 encrypted marker。 模型还可能选择是否先输出 commentary/reasoning,这属于生成行为,不是另一 套 endpoint。
5.6 为什么请求会很大
每个请求不是只发送用户最后一句话。CLI 会组合:
- 模型 base instructions;
- system/developer 指令;
AGENTS.md;- environment context;
- skills/plugin descriptions;
- MCP 与本地工具 schema;
- 之前的 message、reasoning、function call、function output;
- multi-agent 路由与 thread metadata。
本地 query1 的 JSON body 为约 73 KB,其中仅第一个 base-instruction developer message 就约 17.7 KB;另一个 developer message 由多个 content part 构成,总计约 19 KB。工具说明与 schema 是请求膨胀的主要 来源之一。
6. HTTP header 与 metadata
本次父会话观察到的 Codex 特有 header 包括:
text
X-Codex-Beta-Features: remote_compaction_v2
X-Codex-Window-Id: <thread>:<window>
X-Codex-Turn-Metadata: <bounded JSON>
X-Client-Request-Id: <redacted>
Session-Id: <redacted>
Thread-Id: <redacted>
Originator: Codex Desktop
User-Agent: Codex Desktop/0.146.0 ... (codex_exec; 0.146.0)Lite 模型另有:
text
X-Openai-Internal-Codex-Responses-Lite: true子会话另有:
text
X-Codex-Parent-Thread-Id: <redacted>
X-Openai-Subagent: collab_spawn同类信息还会进入 body 的 client_metadata。实现见 responses_metadata.rs: compatibility headers 与 client metadata 有意并存;大的 Code Mode tool-name mapping 只放 body,避免无界 header。
这意味着即使 task payload 加密,网络观察者仍可关联父子 thread、判断 spawn 时间、子 agent 类型和请求数量。
7. SSE 返回格式与客户端处理
典型事件序列:
text
response.created
response.in_progress
response.output_item.added
response.reasoning_summary_part.added
response.reasoning_summary_text.delta
response.reasoning_summary_text.done
response.output_item.done
response.output_item.added
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done
response.completedResponses 不应按 Chat Completions 的 choices[0].delta 解析。reasoning、 message、function call 都是独立 item,function call 通过 call_id 与下一次请求中的 function_call_output 关联。 官方 Streaming API responses 也明确将 Responses streaming 定义为具有预定义 schema 的 semantic events; additional_tools、agent_message 和 encrypted tool parameter 仍是 Codex 扩展,不在该通用文档中。
每个 SSE frame 是一个 event: 行和一个 JSON data: 行,例如:
text
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":1,
"item":{"type":"function_call","name":"spawn_agent",
"arguments":"{\"message\":\"...\"}",
"call_id":"call_..."}}外层 type 描述流事件,item.type 描述产生的持久 ResponseItem。两层 type 不能混为一谈。常见字段含义如下:
sequence_number:本次 response 内事件顺序;output_index:response.output[]中的 item 位置;content_index/summary_index:item 内 content/summary part 的位置;item_id:正在流式生成的 ResponseItem ID;call_id:工具调用与下一请求工具输出的关联键;delta:本 frame 新增的文字或 freeform tool input;item:added/done 时的部分或完整 item;response:created/completed/failed 时的 response 对象,completed 中含 usage、status、response ID 等。
0.146.0 的 SSE parser 实际处理表位于 responses.rs:
| SSE event | 客户端动作 |
|---|---|
response.created | 产生内部 Created |
response.output_item.added | 尝试解析部分 ResponseItem,用于开始展示/跟踪 |
response.output_text.delta | 向 UI 增量发送 assistant 文字 |
response.reasoning_summary_part.added | 建立 reasoning summary part |
response.reasoning_summary_text.delta/done | 增量/完成 reasoning 摘要 |
response.reasoning_text.delta | 处理可见 reasoning content(若 provider 提供) |
response.custom_tool_call_input.delta | 累积 freeform/custom tool input,例如 exec JavaScript |
response.output_item.done | 解析完整 ResponseItem;这是持久化和工具执行的权威 item |
response.completed | 读取 response ID、token usage、可选 end_turn 并结束流 |
response.failed | 映射 context/quota/cyber/invalid/retryable 等错误 |
response.incomplete | 读取 incomplete_details.reason 后作为流错误结束 |
response.in_progress、content-part 的 added/done、 response.function_call_arguments.done 等事件虽然存在于真实流中,但本版本 parser 没有为它们产生单独的内部事件;最终完整 arguments 来自 response.output_item.done.item。因此不能笼统地说客户端总是靠 function_call_arguments.delta 拼接函数参数。custom/freeform tool input 才明确走 delta 累积路径。
三类典型输出 item 的完成形态是:
json
{"type":"message","role":"assistant",
"content":[{"type":"output_text","text":"..."}],
"phase":"commentary"}
{"type":"reasoning","summary":[{"type":"summary_text","text":"..."}],
"encrypted_content":"<opaque reasoning>"}
{"type":"function_call","name":"spawn_agent","namespace":"collaboration",
"arguments":"{\"task_name\":\"calc\",\"message\":\"...\"}",
"call_id":"call_..."}Lite V1 的 freeform 调用则是:
json
{"type":"custom_tool_call","name":"exec",
"input":"const x = await tools.multi_agent_v1__spawn_agent({...});",
"call_id":"call_..."}客户端处理逻辑可概括为:
- SSE parser 发出文字/reasoning 增量供 UI 展示。
output_item.done给出完整 item,reasoning/message/call 被写入 rollout。- 对
function_call,客户端把arguments字符串解析成 JSON;对custom_tool_call,保留其 freeforminput。 - ToolRouter 按 name/namespace 找到本地 handler。
- handler 执行后生成带同一
call_id的function_call_output或custom_tool_call_output。 - 客户端把历史 item + call + output 重新发送到
/responses。 - 重复直到服务端产生最终 assistant message 或错误。
phase=final_answer与end_turn都可能缺失,客户端为旧 provider 保留兼容判断。
旧 Sol 四请求样本正好展示这个循环:
query1:父请求;resp1:reasoning + 加密spawn_agent;query2:父历史 + spawn call + spawn output;query3:新子会话请求,含加密agent_message;query4:父历史继续推进并接收子结果。
父会话和子会话各自有独立的 /responses SSE 流;spawn_agent handler 不会把 child token stream 直接复用为 parent stream。它创建 child session, 等待/消息工具再把 child 的完成状态或最终文本作为本地 tool output/agent message 注入父历史。
8. V1:子任务为什么是明文
V1 tool surface 由 multi_agents.rs 及 multi_agents_spec.rs 定义:
spawn_agentsend_inputresume_agentwait_agentclose_agent
V1 的 message schema 是普通 string,没有 encrypted: true。handler 调用 parse_collab_input,将明文转换成普通 UserInput,然后创建子 session: multi_agents/spawn.rs。
实际表现分两种外观:
- Luna 以及自定义目录强制 V1 的 Sol 都使用 Responses Lite。multi-agent V1 工具被延迟暴露,模型先调用
exec,其 JavaScript input 中出现tools.multi_agent_v1__spawn_agent({message: "..."})。Burp 与 rollout 都能直接看到 message。 - 5.5/5.4 使用常规 Responses。模型直接返回名为
spawn_agent的function_call,arguments 中是明文 message。
子会话最终收到普通 user message:
json
{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "Independently calculate 17*19..."
}]
}因此 V1 的三个位置都可能留存明文:
- 父请求的 tool call output stream;
- 父 rollout;
- 子请求与子 rollout。
9. V2:应用层字段加密的完整链路
9.1 请求 tool schema
V2 tool surface:
spawn_agentsend_messagefollowup_task- 可选
wait_agent interrupt_agentlist_agents
V2 的三个 message 字段调用 .with_encrypted():
- spawn:
multi_agents_spec.rs - send_message:同文件约 L186
- followup_task:同文件约 L218
with_encrypted() 只是在 JSON Schema 中序列化:
json
{
"type": "string",
"description": "...",
"encrypted": true
}实现见 json_schema.rs。
服务端识别加密要求的精确参数
父会话请求中,服务端据此加密模型产生的 message argument:
json
{
"type": "function",
"name": "spawn_agent",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Initial plain-text task for the new agent.",
"encrypted": true
}
},
"required": ["task_name", "message"]
}
}在 Sol 的 Responses Lite 实际抓包 query1.txt 中,完整 wire path 是:
text
input[]
.type = "additional_tools"
.tools[namespace = "collaboration"]
.tools[name = "spawn_agent"]
.parameters.properties.message.encrypted = true同一个 collaboration namespace 中还有:
text
send_message.parameters.properties.message.encrypted = true
followup_task.parameters.properties.message.encrypted = true强制 V2 的常规 Responses 模型(例如本次 gpt-5.4 + multi_agent_v2=true)语义相同,只是 tool definition 位于顶层 tools[],所以路径变为:
text
tools[name = "spawn_agent"]
.parameters.properties.message.encrypted = true当有效版本为 V1 时,message 仍是普通 {"type":"string"} schema, 没有 encrypted key;服务端因而返回明文 function arguments。V1 与 V2 不是通过一个单独的顶层 multi_agent_v2 参数协商,而是通过客户端发送 的不同 tool surface 和每个敏感参数上的 encrypted: true 表达。
以下字段都不是子 agent task message 的首次加密触发器:
include: ["reasoning.encrypted_content"]:只要求返回可回放的加密 reasoning,与 tool argument 加密是两套机制。X-Openai-Internal-Codex-Responses-Lite: true:只选择 Lite 请求布局。X-Openai-Subagent: collab_spawn:标识请求来自已创建的子 agent。multi_agent_v2=true/false:本地配置,不直接出现在 Responses body。
首次加密和后续消费是两个阶段:
- 父请求的
message.encrypted: true告诉 Responses 服务将模型产生的 message argument 作为 opaque 密文返回。 - 子请求的
content[].type = "encrypted_content"告诉服务端该 input part 已是 opaque 密文,需要在子模型上下文中解释。它承载的是第一阶段 返回的同一 token,不会要求客户端重新加密。
9.2 模型/服务端返回
父模型内部需要理解原始任务,但发给客户端的 SSE tool arguments 是:
json
{
"task_name": "calculate",
"fork_turns": "all",
"message": "gAAAAA...<opaque>"
}同一次 response 的 function_call_arguments.done 已经是密文。也就是说 加密发生在客户端收到 SSE 之前。
9.3 客户端不解密
V2 handler 把 arguments 解析为普通 Rust String,但把它当 opaque message:
multi_agents_v2/spawn.rs 调用 communication_from_tool_message,后者直接调用 InterAgentCommunication::new_encrypted:
仓库中没有这条消息的本地 encrypt/decrypt 或 key lookup。客户端只是:
- 收到 opaque string;
- 保存到
encrypted_content; - 创建子 thread;
- 将同一 string 放入子请求。
9.4 子请求封装
InterAgentCommunication::to_model_input_item 生成:
json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/calculate",
"content": [
{
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: ...\nSender: ...\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "gAAAAA...<same opaque value>"
}
]
}实现见 protocol.rs 和 models.rs。
服务端在子模型上下文中恢复 task payload;本地 CLI 从未获得明文。
9.5 子结果回父
本次简单计算中,子 agent 的 final answer 323 以普通 input_text 回到父 thread,并非 encrypted_content。所以不能把 V2 描述为“所有 agent 间消息全加密”。准确描述是:
- 指定的 V2 message 输入参数被应用层加密;
- 路由 envelope 明文;
- 本次 final result 明文;
- reasoning 另有独立的
reasoning.encrypted_content机制。
10. 密文格式与能否解密
Sol 样本的 opaque token:
- URL-safe Base64;
- 首字节
0x80; - 8-byte 大端 Unix timestamp;
- 16-byte IV;
- 密文长度为 16 的倍数;
- 末尾 32 bytes;
- timestamp 精确对应 tool call 产生时间。
这与标准 Fernet token 布局完全一致。证据强度为“强格式推断”,理由是:
- 格式与时间戳全部匹配;
- token 以典型
gAAAAA开头; - 但 Codex 开源客户端没有算法或 key;
- 公开 Responses 文档只解释
reasoning.encrypted_content的 stateless/ZDR 回放用途,没有公开说明 Codex multi-agent tool parameter 的密码套件或密钥生命周期。
没有服务端 key,Burp、CLI 与本地 rollout 都不能恢复 message。不要尝试 把 Base64 decode 误认为解密;decode 只能看到 envelope 元数据。
10.1 泄漏面
即使 message 加密,仍泄漏:
- token 生成时间;
- padded ciphertext 长度,从而近似泄漏明文长度区间;
- tool 名是 spawn/send/followup 中哪一种;
- task name;
- sender、recipient;
- fork_turns;
- 模型、reasoning effort、service tier override;
- parent/child thread 关联;
- 请求/响应时间、次数和 token usage。
10.2 信任边界
这是“客户端 opaque、服务端可解释”的 envelope,不是由父 CLI 与子 CLI 持有独立密钥的端到端加密。它主要减少以下明文暴露:
- 本地 tool arguments;
- 本地 rollout 中的 task payload;
- TLS 解密代理看到的 task 字段;
- 不需要理解 payload 的中间转发层。
它不防:
- 执行模型推理和解密的服务;
- 父模型本身;
- full-history fork 中原本就存在的明文对话;
- 子 agent 输出的明文结果;
- task name 与 metadata 侧信道。
特别是 fork_turns="all":子请求会携带被 fork 的历史。即使新的 message 是密文,旧历史中的用户内容仍可能以普通 message 发送。
11. 5.2 失败与重试机制
5.2 两次独立会话都在首次模型输出前失败。第二次观察到:
- 初始请求;
- 5 次
Reconnecting... n/5; - 最终第 6 个 503 使 turn failed。
源码: responses_retry.rs 与 util.rs。
默认 backoff 基数为 200 ms、因子 2,并乘 0.9..1.1 jitter;若服务端 错误携带 retry delay 则优先使用服务端值。达到最大次数后,如果当前使用 WebSocket 且可切换,客户端还可以回退 HTTPS;本次 provider 本来就是 HTTPS/SSE,因此最终直接报错。
5.2 在发请求前生成的 turn context 已记录 multi_agent_version="v1", Burp 也确认它使用常规 Responses layout。但服务端没有返回 spawn call, 所以本报告把“5.2 会明文 spawn”列为源码/版本选择预测,而非成功流量证据。
12. 安全结论
12.1 对“参数是否加密”的精确回答
| 情况 | task message | 其他 spawn 参数 | 子结果 |
|---|---|---|---|
| V1 | 明文 | 明文 | 明文 |
| V2 | message 为 opaque 密文 | task_name/fork/model 等明文 | 本次为明文 |
| TLS 外部观察 | 整个 HTTP 均受 TLS 保护 | 整个 HTTP 均受 TLS 保护 | 整个 HTTP 均受 TLS 保护 |
| Burp MITM 后 | 能看到 V1 message;V2 只见密文 | 可见 | 可见 |
12.2 配置建议
以下配置不能稳定固定为 V1:
toml
[features]
multi_agent = true
multi_agent_v2 = false比较 V1/V2 时应:
- 每次使用新 thread;
- 记录 rollout 的有效
multi_agent_version; - 同时记录模型目录;
- 若必须让新 Sol thread 使用 V1,使用完整自定义
model_catalog_json并把 Sol 条目标为 V1; - 继承既有 V1 thread 可用于协议对照,但不要把它当成隔离的新会话;
- 用
multi_agent_v2=true强制 V2; - 若要全局禁用,使用
[agents] enabled=false; - 不要只依据模型名称或 UI 中的 feature checkbox 推断有效版本。
若目标是减少本地/代理日志中的子任务明文,应强制 V2,但仍需理解它不是 全对话 E2E 加密,也不隐藏 full-history fork。
13. 可复核证据索引
抓包
query1.txt:Sol 父会话初始 Lite requestresp1.txt:reasoning + encrypted spawn callquery2.txt:父会话带 function call/output 的续请求query3.txt:子会话agent_messagerequestquery4.txt:父会话继续请求resp2.txt至resp4.txt:对应 SSE
原始抓包包含完整运行上下文,只在本地研究环境保留,不随公开网站或远端 仓库发布。可公开复核的字段、脱敏结果和源码证据已写入正文。
源码
- 模型目录:
models.json - 版本选择:
config/mod.rs - 会话版本粘性:
session/mod.rs - Responses / Lite request:
client.rs - tool surface 选择:
spec_plan.rs - V1/V2 schema:
multi_agents_spec.rs - encrypted JSON Schema marker:
json_schema.rs - V2 spawn:
multi_agents_v2/spawn.rs - agent_message envelope:
protocol.rs - header/client metadata:
responses_metadata.rs - retry:
responses_retry.rs
本地 rollout
2026-07-30 的新测试位于:
text
$HOME/.codex/sessions/2026/07/30/“配置缺失”样本位于临时目录:
text
/private/tmp/codex-protocol-absent-20260730/sessions/2026/07/30/Sol V1 的两个新增验证组:
text
# Luna V1 thread -> resume 为 Sol;随后创建 Sol child
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-04-45-019fb20d-eddc-7831-baca-8f7f5f35a3fd.jsonl
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-05-41-019fb20e-c840-71c0-a438-5420ad500c5e.jsonl
# model_catalog_json 强制新 Sol 根 thread 为 V1;随后创建 Sol child
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-15-06-019fb217-68a1-73c2-be0b-be82ad23bdd0.jsonl
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-15-22-019fb217-a669-7573-a1f5-cd20ad3a8e82.jsonl
# 同一 41*43 用户输入:默认 Sol V2 根/子
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-45-03-019fb232-d524-77e0-9854-f381bb8dbfb3.jsonl
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-45-09-019fb232-eb7f-7482-8a02-3df5a4340068.jsonl
# 同一 41*43 用户输入:model_catalog_json Sol V1 根/子
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-46-22-019fb234-08b4-79c2-922b-0bd625de8558.jsonl
$HOME/.codex/sessions/2026/07/30/rollout-2026-07-30T16-46-36-019fb234-3fd1-7e73-9499-8af193917288.jsonl详细根 thread ID 与结果映射见实验记录。报告没有复制 rollout 原文,以 避免把本地完整 developer instructions、环境信息或 metadata 写入研究库。
14. 后续可扩展实验
当前最有价值的后续工作不是重复简单 spawn,而是分别触发并验证:
- V2
send_message.message与followup_task.message是否使用相同 token envelope,并确认send_message的trigger_turn=false路由差异。 - V2 子结果在什么条件下也会加密;本次 final answer 是明文。
fork_turns=none、all、数字窗口对 child request 明文历史量的影响。- 直接 OpenAI endpoint 与
ai.feei.cnprovider 的 header/body 差异。 - provider 远端
/models目录变化是否会覆盖内置multi_agent_version。 - WebSocket-capable provider 下 Responses WS 与 HTTPS/SSE fallback。
- 不同
multi_agent_v2.tool_namespace配置对 wire tool name 的影响。
这些实验应继续遵守:新会话、定向 Burp filter、只保存脱敏结构、不要导出 无关 Proxy History、不要提交认证信息。