opencodex 提交卫生实战:Responses-only encrypted 标记剥离变更的安全 Commit 与 Push 全流程
opencodex 提交卫生实战:Responses-only encrypted 标记剥离变更的安全 Commit 与 Push 全流程
本文以 opencodex 开发日志中「WP3 — Commit hygiene + push」工作项为核心,完整还原一次面向生产仓库的变更落地流程:背景是 Codex 多智能体 v2 协作工具 schema 中携带的 Responses-only
encrypted: true标记引发多供应商 400 故障(issue #85),修复方案为在 Anthropic 适配器中防御性剥离该标记;本文既覆盖git add/git diff --cached/git commit/git push的提交卫生细节与回退策略,也结合仓库源码与测试,讲透stripResponsesOnlyEncryptedMarker的递归语义、名称袋(name-bag)与字面值保护规则,以及 Google、Kiro、OpenAI Responses 等相邻适配器的对照处理。
1. 事件背景:encrypted: true 标记从哪里来,又在哪里引爆 400
opencodex 作为 OpenAI Codex 与 Claude Code 的通用供应商代理,会把 Codex Desktop 的 v2 additional_tools 原样转发给下游供应商。风险源于上游 openai/codex 的一次提交 5f4d06ef(合并于 2026-06-05,PR #26210):v2 协作工具 schema(spawn_agent / send_message / followup_task → properties.message)被标注了一个非标准 JSON-Schema 关键字 encrypted: true——这是 ChatGPT 后端专用的「Responses-only marker」,对 Schema 校验本身没有意义。
opencodex 自提交 99b99bb8(2026-07-10,v2.7.3)起开始转发该 v2 工具集,于是问题浮出水面:Issue #85 报告 Gemini/Antigravity 直接以 400 拒绝整个请求——Google 的 sanitizer 白名单漏掉了 encrypted,而该关键字被逐字透传到所有 collaboration functionDeclarations 中。本地复现确认:parseRequest → google buildRequest 会在三个协作工具上全部输出 encrypted: true。
这次故障的核心教训是:非标准 JSON-Schema 关键字一旦被逐字透传,可能导致整个请求级别的 400,而不是局部降级。完整风险审计记录见 devlog/_fin/260710_encrypted_schema_provider_risk/000_plan.md。
2. 风险全景:哪些供应商暴露在同一类故障之下
审计团队(Tier-3 cxc-search 探查器,sol/medium,fork none)对本地适配器面逐一取证,结论如下表(依据 000_plan.md):
| 供应商 | 转换器 | 对未知嵌套关键字的处理 | 风险判定 |
|---|---|---|---|
| Google (AIS/Vertex/Antigravity) | google-tool-schema.ts 白名单 + deref |
未知关键字被过滤,但 encrypted 曾漏网 |
CONFIRMED broken(#85) |
| Kiro (CodeWhisperer/Q) | kiro-tools.ts sanitizeKiroSchema 黑名单 |
encrypted 曾不在 KIRO_REJECTED_SCHEMA_KEYS 中 → 透传 |
AT-RISK in practice |
| Anthropic | anthropic.ts normalizeAnthropicInputSchema |
仅根级归一化,嵌套关键字原样透传 | UNVERIFIED,防御性剥离 |
| OpenAI chat / Azure | openai-chat.ts 逐字透传 |
逐字透传 | non-strict SAFE / strict AT-RISK |
| Cursor | cursor/tool-definitions.ts 逐字透传 |
逐字透传 | UNVERIFIED |
| OpenAI Responses passthrough | 原生 | 标记在此合法 | SAFE(必须保留) |
关键实证(来自审计记录):
- Kiro/Bedrock:Kiro 对合法 JSON-Schema 也拒绝根级
oneOf/allOf/anyOf(Kiro#9638);一个坏 MCP 工具会让「Hi」提示也失败(Kiro#9653);网关实现因「Kiro 拒绝」而递归剥离additionalProperties(Kiro-Go#74)。Bedrock Converse 的toolSpec.inputSchema.json从未定义 draft/关键字子集,minimum/maximum、default/format均曾触发真实 400。虽然没有针对encrypted的公开报告,但剥离它副作用可忽略。 - Anthropic:服务端校验真实存在,根级
oneOf/allOf/anyOf报 "input_schema does not support ... at the top level"(claude-code#3383/#4886);强制 draft 2020-12,draft-07 直接 500(sdk-python#1001);property-key 模式有 400 案例;strict: true下不支持 minItems/minimum 等配置(vercel/ai#13355)。encrypted本身的接受度 UNVERIFIED,但防御性剥离被采纳。 - OpenAI Chat Completions non-strict:官方 OpenAPI 合约声明
FunctionParameters为type:object, additionalProperties:true,SAFE;strict: true仅支持 JSON-Schema 子集,AT-RISK;Fireworks、OpenRouter→DeepSeek、vLLM 等兼容网关对嵌套title/default/anyOf/$ref均有 400 报告,AT-RISK 且路由相关。
最终处置矩阵:Google 在 google-tool-schema.ts 白名单补 encrypted + 回归测试;Kiro 在 KIRO_REJECTED_SCHEMA_KEYS 补 encrypted;Anthropic 由 normalizeAnthropicInputSchema 递归剥离(本文主角);OpenAI Responses passthrough 严禁剥离(Responses-only 标记)。
3. WP1 实现:Anthropic 防御性剥离的递归语义与源码落地
3.1 定位:单线透传路径
在修复前,Anthropic 适配器的工具 schema 只有根级归一化。源码链为 src/adapters/anthropic.ts:
toolsToAnthropicFormat(anthropic.ts#L850)把每个工具映射为input_schema: normalizeAnthropicInputSchema(t.parameters)(anthropic.ts#L859);normalizeAnthropicInputSchema(anthropic.ts#L864)只做根级处理:确保type:"object"+properties,并展平根级oneOf/anyOf/allOf(anthropic.ts#L869-L923)。它从不递归,因此嵌套的properties.message.encrypted: true会被原样送上线路。
修复方式是在 normalizeAnthropicInputSchema 的第一行接入共享的 stripResponsesOnlyEncryptedMarker(anthropic.ts#L865),该函数从 src/adapters/responses-tool-schema.ts 导入(anthropic.ts#L23)。由于剥离发生在根归一化之前,无组合与有组合(composition)两条路径都自动继承剥离,且原有根级展平行为不变——strip 只移除这一个关键字。
3.2 stripResponsesOnlyEncryptedMarker 的四条语义规则
函数本体见 responses-tool-schema.ts#L32-L78。它需要区分「关键字 x」与「某人命名为 x 的属性」,为此在文件头部定义了两组集合(responses-tool-schema.ts#L1-L13):
const SCHEMA_NAME_BAG_KEYS = new Set([
"properties", "patternProperties", "$defs", "definitions",
"dependencies", "dependentSchemas", "dependentRequired",
]);
const SCHEMA_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
剥离逻辑按节点类型分派:
- 数组 → 逐项映射,子项视为独立 schema(
inNameBag=false); - 非对象 → 原样返回;
- name-bag 节点(
inNameBag=true,即properties/$defs等映射的值)→ 每个键是「名称」而非 schema 关键字,所以键名为encrypted的属性必须存活;仅对其值递归(inNameBag=false); - schema 节点(
inNameBag=false)→ 丢弃encrypted键;其余键按类别处理:- 键属于
SCHEMA_LITERAL_VALUE_KEYS(const/default/enum/examples)→ 值原样拷贝、不递归(这是审计修复 #1:default: { encrypted: true }是用户数据而非 schema 关键字); - 键属于 name-bag 集合 → 递归且
inNameBag=true; - 其余 → 递归且
inNameBag=false。
- 键属于
两个值得一提的实现细节:
- 显式栈而非原生递归(responses-tool-schema.ts#L28-L31):schema 由调用方提供,嵌套深度受攻击者影响,原生递归会在大深度 schema 下栈溢出打挂请求路径;显式栈把深度成本转化为有界、可恢复的堆内存。
- null-prototype 输出对象(responses-tool-schema.ts#L57-L58):schema 属性名可能是
__proto__,用Object.create(null)记录可确保其作为普通数据保留而非污染原型。 - 纯函数:总是构建新对象,绝不修改输入(
t.parameters是被多个适配器共享的已解析状态,如 src/responses/parser.ts 与 src/adapters/openai-chat.ts 均消费同一数据)。测试 anthropic-tool-schema.test.ts#L173-L183 用structuredClone快照断言输入 schema 未被改动。
3.3 相邻适配器对照
- Google:采用白名单策略(src/adapters/google-tool-schema.ts):只输出
type/nullable/description/format/enum/const/properties/items/required/anyOf等MERGED_SCHEMA_KEYS(google-tool-schema.ts#L43-L54),未知注解天然被丢弃,从根上防止新 MCP/JSON-Schema 注解引发 400;另有深度(MAX_SCHEMA_DEPTH=24)、节点数(MAX_SCHEMA_NODES=1024)等预算护栏(google-tool-schema.ts#L7-L11)。 - Kiro:采用黑名单
KIRO_REJECTED_SCHEMA_KEYS(src/adapters/kiro-tools.ts#L20-L56),其中明确包含"encrypted",并配套SCHEMA_MAP_KEYS = new Set(<a href="https://link.gitcode.com/i/ccf9c2de02382a69b12dfb8b9c9b09f8" target="_blank">"properties", "$defs", "definitions"])保护名称袋([kiro-tools.ts#L58-L61)。 - OpenAI Responses passthrough:标记在此合法,必须保留。边界测试见 tests/responses/plaintext-v2-agent-messages.test.ts#L454,断言 prepared body 中仍包含
"encrypted":true。
4. 验证体系:五类测试用例锁定行为边界
测试文件 tests/adapters/anthropic/anthropic-tool-schema.test.ts(计划文档中记为 tests/anthropic-tool-schema.test.ts,仓库实际位于 tests/adapters/anthropic/ 下)复用文件内置的两个辅助函数:toolsOf 驱动 buildRequest 并解析序列化 body(L7-L19),toolSchema 构造单工具 fixture(L21-L24)。五类核心用例:
| 用例 | 验证点 | 位置 |
|---|---|---|
| c1a 端到端剥离 | 真实 v2 协作形态 properties.message: { type:"string", encrypted:true } 序列化后不含 "encrypted",且 message.type === "string" 存活 |
L110-L122 |
| c1b 同名属性存活 | properties: { encrypted: { type:"boolean" } } 原样保留 |
L124-L129 |
| c1c 深层嵌套剥离 | items、嵌套对象属性、根 anyOf 分支(走组合展平路径)三处 marker 全部消失 |
L131-L150 |
| c1d 字面值存活 | default: { encrypted: true }、const/enum 内嵌对象、required:<a href="https://link.gitcode.com/i/954a2da4acdc7aa96a246fdfd675654c" target="_blank">"encrypted"]、$defs.encrypted 全部逐字节保留 |
[L152-L171 |
| 非变异 | 输入 schema 经 buildRequest 后与 structuredClone 快照一致 |
L173-L183 |
其中 c1c 与 c1d 是审计轮次 1 的强制要求:c1 若要达标,必须显式覆盖深路径与字面值存活,防止浅层实现蒙混过关。相邻适配器的对照回归同样齐备:Google 见 google-tool-schema.test.ts#L43 与 google-adapter.test.ts#L145,Kiro 见 kiro-adapter.test.ts#L833。
验收标准(goalplan c1/c2):序列化 input_schema 在所有被测路径(根属性、嵌套属性、items、anyOf 分支)均无 encrypted schema 关键字;同名属性与同名 $defs 条目存活;default/const/enum 中携带 encrypted 键的字面值逐字节保留;bun test tests/anthropic-tool-schema.test.ts 全绿且 bun x tsc --noEmit 退出码为 0。
5. WP3 核心:提交卫生与推送全流程(本工作项)
WP3 的目标计划(goalplan)为 ship-remaining-encrypted-marker-hardening-anthro,验收标准 c4/c5,完整操作序列见 020_wp3_commit_push.md。整个流程遵循「显式路径暂存 → 暂存核对 → 规范化提交信息 → 提交核对 → 快速前进推送」五步法,并严格划出 out-of-scope 边界。
5.1 只暂存两个文件:显式路径 git add
由于本会话开始时,用户 WIP 中存在大量未暂存修改(README*、docs-site/、src/web-search/、src/server/、src/lib/、其他适配器、catalog、types、未跟踪测试等),绝不能使用 git add -A 或 git add . 这类宽泛命令。必须用显式路径暂存,且只暂存包含 WP1 变更的两个文件——它们不在会话开始时的用户 WIP 修改集合中,因此不需要 hunk 拆分:
git add src/adapters/anthropic.ts tests/adapters/anthropic/anthropic-tool-schema.test.ts
配套约束(来自 010_wp1_anthropic_strip.md 的 scope boundary):worker 不得触碰任何用户 WIP 文件,也不得执行 git add/commit——提交与推送由主会话(即 WP3)独占负责。
5.2 暂存核对:--cached 与 --short 双保险
提交前必须验证暂存区内容精确等于预期,两道命令缺一不可:
# 1) 暂存区 diff 统计:必须恰好列出上述两个文件
git diff --cached --stat
# 2) 工作区全貌:所有用户 WIP 仍保持 unstaged / untracked 状态
git status --short
第一条确认「该暂存的只有这些」,第二条确认「不该暂存的都没进来」。若 git diff --cached --stat 出现任何第三个文件,立即回退暂存并排查,而不是继续提交。
5.3 规范化提交信息
提交信息沿用既有提交 e335e843(已随 Google/Kiro 防护一起发布)的 Conventional Commits 风格,type 为 fix,scope 为 anthropic,正文点明「剥离 Codex 的 Responses-only encrypted 标记」,并带上 issue 引用:
git commit -m "fix(anthropic): strip Codex's Responses-only encrypted marker from tool input_schema (#85)"
5.4 提交核对:git show --stat HEAD
提交完成后再次核对,本次提交(c4 标准)必须只列出两个文件:
git show --stat HEAD
这一步把「提交前核对」与「提交后核对」闭环:任何混入的文件都会在这里现形。
5.5 推送:fast-forward only,绝不 force-push
推送到 dev 分支,预期为快进推送(fast-forward),包含既有提交 e335e843 加本次新提交(c5 标准):
git push origin dev
若推送被拒绝(远端已分叉),处理策略严格分级:
git fetch拉取远端最新状态;- 检视分叉内容,判断是否可将自己的提交变基/重放到远端之上;
- 只允许重试 fast-forward;
- 任何情况下不得 force-push(防止覆盖他人提交、破坏共享历史);
- 若持续被拒绝 → 标记 BLOCKED,交由人工处理,而不是用危险手段硬推。
5.6 Out of scope 边界
devlog/与.codexclaw/均为 gitignore 目标(提交前应复核git check-ignore确认,若未忽略需排除在外);- 不做版本号 bump;
- 不触发任何 GitHub issue 动作;
- 不触碰除上述两个文件外的任何文件。
6. 审计加固过程:三个 blocker 如何被消解
WP1 方案经过一轮 terra 评审(GO-WITH-FIXES,blocker=3),三个问题全部被接受并修复(010_wp1_anthropic_strip.md 的 audit round 1 synthesis):
- #1 Medium(ACCEPTED):盲目递归会破坏 JSON 值型注解(
const/default/enum/examples)→ 引入SCHEMA_LITERAL_VALUE_KEYS,字面值键直接拷贝、不递归; - #2 Low(ACCEPTED):
$defs/definitions是名称→schema 映射 → 加入 name-bag 集合,名为encrypted的定义必须存活; - #3 Low(ACCEPTED):c1 必须显式包含深路径 + 字面值存活测试,浅层实现无法通过验收。
评审同时确认了三项关键前提:Anthropic 单线透传路径经由 normalizeAnthropicInputSchema(anthropic.ts:485 一带);marker 经 parser.ts:99-106,463 逐字到达各适配器;「先剥离后展平」的次序正确,且非变异是硬性要求。
7. 复现与自检清单
如需在本地复现本工作项的完整验证,可依次执行:
# 单元测试(全部通过,含 5 类 marker 用例)
bun test tests/adapters/anthropic/anthropic-tool-schema.test.ts
# 类型检查(退出码 0)
bun x tsc --noEmit
# 提交卫生核对(仅两个文件在暂存区)
git add src/adapters/anthropic.ts tests/adapters/anthropic/anthropic-tool-schema.test.ts
git diff --cached --stat
git status --short
git commit -m "fix(anthropic): strip Codex's Responses-only encrypted marker from tool input_schema (#85)"
git show --stat HEAD
git push origin dev
推送被拒时的铁律:fetch → 检视 → 仅重试 fast-forward → 永不 force-push → 持续失败即 BLOCKED 上报。
8. 总结
本次工作项完整展示了 opencodex 处理跨供应商 schema 兼容性问题的三层方法论:审计先行(000_plan 产出供应商×转换器×处置矩阵)、最小防御实现(WP1 的递归剥离,严格保护同名属性与字面值)、提交卫生落地(WP3 的显式暂存、双核对、规范化提交信息与 fast-forward-only 推送)。核心原则值得任何多供应商代理项目复用:非标准 schema 关键字只在其原生协议(OpenAI Responses)中合法,其余所有翻译路径都应防御性剥离,而剥离器必须能区分「关键字」与「碰巧同名的用户数据」,并在共享解析状态上保持纯函数语义。相关文档与代码证据均可在仓库中直接检索:000_plan.md、010_wp1_anthropic_strip.md、020_wp3_commit_push.md、responses-tool-schema.ts、anthropic.ts、anthropic-tool-schema.test.ts。