opencodex 提交卫生实战:Responses-only encrypted 标记剥离变更的安全 Commit 与 Push 全流程

原创2026-09-22 16:32:06396 阅读
文章标签:人工智能LLM 网关API网关后端开发工具

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"]);

剥离逻辑按节点类型分派:

  1. 数组 → 逐项映射,子项视为独立 schema(inNameBag=false);
  2. 非对象 → 原样返回;
  3. name-bag 节点(inNameBag=true,即 properties/$defs 等映射的值)→ 每个键是「名称」而非 schema 关键字,所以键名为 encrypted 的属性必须存活;仅对其值递归(inNameBag=false);
  4. 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

若推送被拒绝(远端已分叉),处理策略严格分级:

  1. git fetch 拉取远端最新状态;
  2. 检视分叉内容,判断是否可将自己的提交变基/重放到远端之上;
  3. 只允许重试 fast-forward;
  4. 任何情况下不得 force-push(防止覆盖他人提交、破坏共享历史);
  5. 若持续被拒绝 → 标记 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. #1 Medium(ACCEPTED):盲目递归会破坏 JSON 值型注解(const/default/enum/examples)→ 引入 SCHEMA_LITERAL_VALUE_KEYS,字面值键直接拷贝、不递归;
  2. #2 Low(ACCEPTED):$defs/definitions 是名称→schema 映射 → 加入 name-bag 集合,名为 encrypted 的定义必须存活;
  3. #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。

登录后查看全文
opencodex