diff --git a/README.md b/README.md index e7ed041..ec3d3fa 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,9 @@ agent 循环 + 沙箱 + 工具 + MCP + skills + 线程持久化 agent 循环、沙箱、工具执行、上下文压缩、线程存储,全部是官方 codex 二进制里的,不是我复刻的简化版。 -实测版本:`codex-cli 0.164.0-alpha.9`,协议定义就是从它 -导出的,见 `protocol/`。 +实测版本:`codex-cli 0.166.0`,协议定义就是从它 +导出的,见 `protocol/`(2026-11-05 由 0.166.0 二进制重导,见 +`docs/upstream-166-alignment.md`)。 ## 目录 diff --git a/config.example.toml b/config.example.toml index fae8c8e..9cad6d5 100644 --- a/config.example.toml +++ b/config.example.toml @@ -36,6 +36,12 @@ daemon_auto_start = false # Skills / 插件:放 ~/.codex/skills 或用 `codex plugin` 管理, # app-server 会在 thread 里按需加载,不用在代码里硬编码。 +# 想停用某个技能又不想删文件时,用 [[skills.config]](只写一条 +# 选择器:path 匹配单个 SKILL.md,name 匹配同名全部技能;多条 +# 匹配时最后一条胜出): +# [[skills.config]] +# name = "software-engineering" +# enabled = false # 项目指令:每个仓库根放 AGENTS.md,thread 启动时自动合并读取, # 依次是 ~/.codex/AGENTS.md、仓库根、当前子目录,越往下优先级越高。 diff --git a/docs/upstream-166-alignment.md b/docs/upstream-166-alignment.md new file mode 100644 index 0000000..b86fd15 --- /dev/null +++ b/docs/upstream-166-alignment.md @@ -0,0 +1,130 @@ +# 上游对齐:openai/codex 稳定版 0.166.0(核查日 2026-11-05) + +结论先说:**已用 0.166.0 官方二进制重导 schema 并验证通过;本批只改 +宿主侧 pin、文档、schema 与映射测试,不动任何生产二进制。** + +生产二进制更换(/usr/local/bin/codex、172、170、Docker 镜像内 +二进制)是部署动作,需要用户另外点头 + 窗口,不在本批;本批的 +验证边界是本地 scratch 验证(见下文“验证”),不是生产换装实证。 + +## 基线与上游位置 + +- 本项目此前锚定:`codex-cli 0.164.0-alpha.9`(2026-10-25 实测, + `protocol/` 的 schema 就是从它用 + `codex app-server generate-json-schema` 导出的;见 + `docs/upstream-164-alignment.md`)。 +- 上游稳定版:`0.165.0`(2026-10-27)、`0.166.0`(2026-10-29, + 当前最新稳定版)。本批一次跨两个稳定版锚定到 0.166.0,不 + 在 0.165.0 停留(0.165.0 的内容已全含在 0.166.0 里)。 +- 上游 alpha 线(不作为目标):0.167.0-alpha.5 与 0.168.0-alpha.1 + (均 2026-11-03),无 0.167/0.168 稳定版。按 164 文档立过的 + 原则只记录、不跟进;alpha 跟踪办法见批总结,不进本文档。 +- 上游来源:https://github.com/openai/codex/releases(npm 官方包 + `@openai/codex@0.166.0` + 平台包 `@openai/codex@0.166.0-linux-x64`, + npm integrity sha512 已验)。 + +## 稳定版 0.166.0 改动 → 本项目状态 + +| 上游改动 | 对宿主层的影响 | 结论 | +|---|---|---| +| app-server 线程启动/关闭竞态修复、已完成 runtime 及时释放(#57787、#57788) | 宿主 host.py 是长循环、一个 app-server 进程托管多 thread,直接受益 | 升级收益,无需改代码;重测 thread 生命周期一项(已做,见验证) | +| resume/fork 时保留已确认的 approval 与权限设置(#57227) | 宿主 resume/fork 流程不再被重置坑绊 | 升级收益,无需改代码 | +| 独立 executor 随规范 CLI 与 app-server 发布包一起发(#57837) | 实查:不是新文件,是二进制内 `exec-server` 子命令;三件套文件形态不变,宿主只驱动 `app-server` | 不影响安装形态,无需改代码 | +| GPT-6.1 Terra 进入 bundled、Vertex AI、Bedrock 三个目录(#57293);Vertex AI provider(0.165.0 #56265) | 宿主模型走中转路由,引擎版本不卡 Terra 可用性;内置列表回退路径 0.165+ 才含 Terra | 不相关,无需改代码 | +| `codex queue --resume`(0.165.0 #57232) | 宿主任务队列自建(host.py 循环 + worker_sweep admin CLI),不依赖上游 queue | 不相关,功能等价但实现无关 | +| `codex doctor --network-only`(#57606) | 运维诊断工具,不是宿主运行路径 | 观察项,不接 | +| 键盘 transcript 选择、Markdown 渲染一致化、安全拦截说明 | 都是 TUI 交互面,app-server 协议与宿主层不依赖它们 | 不相关,无需改代码 | +| memory v2 对新 profile 默认开启 + 外部记忆导入(0.165.0) | 宿主 auto_memory 为自建租户记忆,未对齐上游 memory v2 格式/导入 | 不对齐,见对齐表 A10;本批不动 | +| 实验性 attached thread forks(0.165.0 #56351) | 只在 `--experimental` schema 里出现(`ThreadForkParams.attachToSource`),默认 schema 无;且 attached fork 在普通 thread/list 里不可见,与宿主“fork 即新建 worktree 的可见线程”模型冲突 | 不接,评估见批总结;本批不写代码 | +| 构建基线 Node.js 24.22.1、Rust 1.95.0 | 与本机 Node 24.22.1 一致;宿主是 Python,不编译 Rust | 不相关 | + +## schema 重导结果(0.166.0 二进制实导,非树对比) + +导出方式:`codex app-server generate-json-schema --out `, +原样取其中两 bundle 替换 `protocol/`(v1 + v2,与旧形态一致, +不引入 per-type 散文件)。 + +| 对比项 | alpha.9 时代 bundle | 0.166.0 重导 bundle | 结论 | +|---|---|---|---| +| v1 bundle 定义数 | 83 | 83 | 仅 ClientRequest 变(新增 feedback/save 一个方法) | +| v2 bundle 定义数 | 665 | 677 | +12 定义、0 删除、16 个定义有改动 | +| v2 新增定义 | — | AccountSession、AccountSessionWorkspace(Kind)、ApplicationStorageRequirements、FeedbackSaveParams/Response、ModelProviderError、OtelExporter、OtelHttpProtocol、OtelRequirements、OtelTlsConfig、OtelToolResultLogConfig | 均为新能力/新遥测,无 host 用项 | +| v2 改动定义 | — | Account、AdditionalContextEntry、ApplicationRequirements、ClientRequest、CodexErrorInfo、ConfigRequirements、HookMetadata、HookRunSummary、HooksListParams、NetworkRequirements、ProcessExitedNotification、ReviewDelivery、ReviewStartParams、ReviewStartResponse、ThreadItem、ThreadUsage | 除 ReviewDelivery 外全是新增可选字段或描述改动(见下) | +| 方法集 | ClientRequest 105 | ClientRequest 106(+feedback/save) | 0 删除;ServerRequest 10→10、通知 85→85 全不变 | +| host 方法 params 引用 | — | 21 个 host 方法的 params 引用全部未变 | 零漂移 | + +唯一语义变化:`ReviewDelivery` 枚举从 `["inline", "detached"]` +变为 `["inline"]`(detached 已移除),`ReviewStartParams.delivery` +与 `ReviewStartResponse.reviewThreadId` 的描述同步改写。宿主早已 +不发 detached(`host.py start_review` 注释“不发 detached(已废弃)”、 +`cloud/review.py` 同款说明),故无影响;其余改动如 +`ThreadItem` 新增 `completedAtMs`/`startedAtMs`(可空)、 +`ThreadUsage` 新增 `nativeUsageUsdMicros`(可空)、 +`CodexErrorInfo` 新增 `modelNotFound`/`policyCapacityUnavailable` +均为向后兼容加法。 + +方法数勘误:2026-11-05 对齐表写“17 个方法/通知”并不准确。 +实查 host.py 触及 21 个方法字符串:18 个发出请求(含 +initialize 握手)+ 3 个入站通知;对齐表列出的 18 项漏了 +initialize、app/list、mcpServerStatus/list 三个。 +`tests/test_host_protocol_mapping.py` 以实查 21 项为准钉死。 + +## 三件套核对(0.166.0 linux-x64 平台包实查) + +- `bin/codex`(319,346,888 字节,`codex-cli 0.166.0`)、 + `bin/codex-code-mode-host`(77,997,328 字节)、 + `codex-resources/bwrap`(529,776 字节)三件齐全,成套。 +- 包内另有 voice bundle(`codex-voice-host` 及 gstreamer 库, + 语音为刻意不做项,不理会)与 `codex-path/rg`,均不影响三件套。 +- 0.166.0 发布包不改变安装形态:宿主的 `bin/codex` 优先、 + `CODEX_BIN` 环境变量、`PATH` 回退三档逻辑(`host.py + default_codex_bin`、`start.sh`)无需改动。 + +## 验证(本地 scratch 验证,无模型调用) + +- 沙箱 smoke:scratch 三件套布局下 `codex sandbox echo` + 成功返回,exit 0(bwrap 配对工作)。 +- app-server smoke:仓库自带 `client.py --list-threads` 对 + scratch 0.166.0 二进制跑通:initialize 成功(codexHome 为 + scratch 目录、platform=linux)、thread/list 返回空数据,exit 0。 +- 技能发现复核:0.166.0 下 `/skills/`(我方播种 + 位置)与 `$HOME/.agents/skills/`(文档 USER 位置)放的测试 + 技能均被 `skills/list` 发现(scope=user、enabled=true), + 故播种机制无需改动;系统技能(skill-creator 等 5 个)照常出现。 +- 方法映射重测:`tests/test_host_protocol_mapping.py`(21 项 + 钉死)+ 完整三套件(数见批总结)。 + +## 本批实际改动(只改宿主侧,不动官方二进制) + +1. `protocol/`:两 bundle 替换为 0.166.0 二进制原样导出 + (749,825 + 646,070 字节)。 +2. `README.md`:实测版本 pin 从 `0.164.0-alpha.9` 改为 + `0.166.0`(验证通过后才改,见批总结;不是提前改 pin 充数)。 +3. `start.sh`:三件套注释版本号同步为 0.166.0(仅注释)。 +4. `config.example.toml`:补 `[[skills.config]]` 禁用语义的 + 注释示例(仅注释,不改任何生效配置)。 +5. `tests/test_host_protocol_mapping.py`:新增,21 项钉死。 +6. 本文件:作为 0.166.0 的对齐记录,后续每次上游稳定版发布时 + 按同一张表复核。 + +## 明确不做的事(诚实边界) + +- **不换任何生产/服务器二进制。** `/usr/local/bin/codex`、 + 172、170、Docker 镜像内二进制一律不动;本批二进制只活在 + scratch 目录,批后清理。 +- **不追 alpha。** 0.167/0.168 alpha 只记录、不采用。 +- **不改播种机制。** 0.166.0 下 `/skills/` 仍被 + 发现,无需改 `skill_seed.py`。 +- **不接 attached forks。** 实验性、仅 `--experimental` + schema、与宿主可观测模型冲突;重估触发条件见批总结。 +- **不改阈值、不改 #238 auto_apply、不改 #247 语义、不改 + v1.1 契约、不改 SKILL.md 第五项。** +- **不合并、不部署、不推送。** 本批留在隔离 worktree 的 + `codex-opensource-align2` 分支上;live 仍是 aba06d9,不碰。 + +## 下一步(需要用户定) + +1. 是否把生产二进制从 alpha.9 换到 0.166.0(主机部署动作, + 需窗口 + 重跑 resume / goal / 成本实弹)?那一步与控制面 + 部署是两件分开的决策,不可悄悄混进一次部署。 +2. gapfill2 线是否合并(B1)仍待用户;本批不替用户定。 diff --git a/protocol/codex_app_server_protocol.schemas.json b/protocol/codex_app_server_protocol.schemas.json index 9381414..afeb276 100644 --- a/protocol/codex_app_server_protocol.schemas.json +++ b/protocol/codex_app_server_protocol.schemas.json @@ -2394,6 +2394,30 @@ "title": "Feedback/uploadRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "feedback/save" + ], + "title": "Feedback/saveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FeedbackSaveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Feedback/saveRequest", + "type": "object" + }, { "description": "Execute a standalone command (argv vector) under the server's sandbox.", "properties": { @@ -6781,6 +6805,13 @@ ], "title": "ChatgptAccountType", "type": "string" + }, + "workspaceName": { + "description": "Display name of the selected ChatGPT workspace, when available.", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -6810,6 +6841,23 @@ ], "title": "AmazonBedrockAccount", "type": "object" + }, + { + "description": "Google Cloud Vertex AI with externally managed credentials.", + "properties": { + "type": { + "enum": [ + "gcp" + ], + "title": "GcpAccountType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "GcpAccount", + "type": "object" } ] }, @@ -6871,6 +6919,103 @@ ], "type": "string" }, + "AccountSession": { + "properties": { + "displayName": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "imageUrl": { + "type": [ + "string", + "null" + ] + }, + "isActive": { + "type": "boolean" + }, + "lastUsedAt": { + "format": "int64", + "type": "integer" + }, + "selectedWorkspaceAccountId": { + "type": [ + "string", + "null" + ] + }, + "sessionId": { + "type": "string" + }, + "userId": { + "type": [ + "string", + "null" + ] + }, + "workspaces": { + "items": { + "$ref": "#/definitions/v2/AccountSessionWorkspace" + }, + "type": "array" + } + }, + "required": [ + "isActive", + "lastUsedAt", + "sessionId", + "workspaces" + ], + "type": "object" + }, + "AccountSessionWorkspace": { + "properties": { + "accountId": { + "type": "string" + }, + "imageUrl": { + "type": [ + "string", + "null" + ] + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AccountSessionWorkspaceKind" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "accountId" + ], + "type": "object" + }, + "AccountSessionWorkspaceKind": { + "enum": [ + "personal", + "workspace" + ], + "type": "string" + }, "AccountTokenUsageDailyBucket": { "properties": { "startDate": { @@ -6993,6 +7138,13 @@ "kind": { "$ref": "#/definitions/v2/AdditionalContextKind" }, + "name": { + "description": "Optional name for untrusted context, used to label its tool output.", + "type": [ + "string", + "null" + ] + }, "value": { "type": "string" } @@ -7756,6 +7908,42 @@ "type": "null" } ] + }, + "storage": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ApplicationStorageRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ApplicationStorageRequirements": { + "description": "Managed storage settings for new Desktop workspaces, not filesystem access restrictions.", + "properties": { + "defaultProjectParent": { + "anyOf": [ + { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + { + "type": "null" + } + ] + }, + "projectlessWorkspaceRoot": { + "anyOf": [ + { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -8502,6 +8690,8 @@ "rateLimitExceeded", "flexUnavailable", "serverOverloaded", + "modelNotFound", + "policyCapacityUnavailable", "cyberPolicy", "misalignmentPolicyViolation", "tooManyDenials", @@ -10216,6 +10406,16 @@ } ] }, + "otel": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelRequirements" + }, + { + "type": "null" + } + ] + }, "sqliteHome": { "type": [ "string", @@ -11807,6 +12007,85 @@ }, "type": "object" }, + "FeedbackSaveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "classification": { + "type": "string" + }, + "extraLogFiles": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeLogs": { + "type": "boolean" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "savePath": { + "allOf": [ + { + "$ref": "#/definitions/v2/LegacyAppPathString" + } + ], + "description": "Absolute, previously unused ZIP path on the app-server host. The caller owns this file, including after a disconnected RPC." + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "classification", + "savePath" + ], + "title": "FeedbackSaveParams", + "type": "object" + }, + "FeedbackSaveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "promptHash": { + "description": "Same prompt hash as feedback/upload; null when prompt metadata is absent.", + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "savedPath", + "threadId" + ], + "title": "FeedbackSaveResponse", + "type": "object" + }, "FeedbackUploadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -13455,6 +13734,13 @@ "enabled": { "type": "boolean" }, + "environmentId": { + "description": "The owning execution environment, if this hook came from its requirements.", + "type": [ + "string", + "null" + ] + }, "eventName": { "$ref": "#/definitions/v2/HookEventName" }, @@ -13622,7 +13908,7 @@ "default": "unknown" }, "sourcePath": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "$ref": "#/definitions/v2/LegacyAppPathString" }, "startedAt": { "format": "int64", @@ -13743,11 +14029,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "cwds": { - "description": "When empty, defaults to the current session working directory.", + "description": "Working directories to inspect when no thread is specified. An empty list uses the session cwd.", "items": { "type": "string" }, "type": "array" + }, + "threadId": { + "description": "List a loaded thread's hooks, including its selected environments. Omit `cwds` in this mode.", + "type": [ + "string", + "null" + ] } }, "title": "HooksListParams", @@ -15818,6 +16111,81 @@ "title": "ModelProviderCapabilitiesReadResponse", "type": "object" }, + "ModelProviderError": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Structured JSON-RPC error data. Unknown errors must retain the client's normal fallback.", + "oneOf": [ + { + "properties": { + "currentModelProvider": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderMismatch" + ], + "title": "ModelProviderMismatchv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "currentModelProvider", + "modelProvider", + "type" + ], + "title": "ModelProviderMismatchv2::ModelProviderError", + "type": "object" + }, + { + "properties": { + "modelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderNotConfigured" + ], + "title": "ModelProviderNotConfiguredv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "modelProvider", + "type" + ], + "title": "ModelProviderNotConfiguredv2::ModelProviderError", + "type": "object" + }, + { + "properties": { + "modelProvider": { + "type": "string" + }, + "requiredModelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderPolicyConflict" + ], + "title": "ModelProviderPolicyConflictv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "modelProvider", + "requiredModelProvider", + "type" + ], + "title": "ModelProviderPolicyConflictv2::ModelProviderError", + "type": "object" + } + ], + "title": "ModelProviderError" + }, "ModelRerouteReason": { "enum": [ "highRiskCyberActivity" @@ -16116,7 +16484,7 @@ "additionalProperties": { "$ref": "#/definitions/v2/NetworkDomainPermission" }, - "description": "Canonical network permission map for `experimental_network`.", + "description": "Canonical managed network permission map.", "type": [ "object", "null" @@ -16143,6 +16511,16 @@ "null" ] }, + "reviewDomains": { + "description": "Host patterns routed to network approval before ordinary allow rules.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "socksPort": { "format": "uint16", "minimum": 0.0, @@ -16155,7 +16533,7 @@ "additionalProperties": { "$ref": "#/definitions/v2/NetworkUnixSocketPermission" }, - "description": "Canonical unix socket permission map for `experimental_network`.", + "description": "Canonical managed unix socket permission map.", "type": [ "object", "null" @@ -16229,6 +16607,232 @@ ], "title": "Nullable_GetAccountTokenUsageParams" }, + "OtelExporter": { + "oneOf": [ + { + "enum": [ + "none", + "statsig" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "otlp-http": { + "properties": { + "endpoint": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Header names are retained; values are redacted by the server.", + "type": "object" + }, + "protocol": { + "$ref": "#/definitions/v2/OtelHttpProtocol" + }, + "tls": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelTlsConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "endpoint", + "headers", + "protocol" + ], + "type": "object" + } + }, + "required": [ + "otlp-http" + ], + "title": "OtlpHttpOtelExporter", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "otlp-grpc": { + "properties": { + "endpoint": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Header names are retained; values are redacted by the server.", + "type": "object" + }, + "tls": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelTlsConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "endpoint", + "headers" + ], + "type": "object" + } + }, + "required": [ + "otlp-grpc" + ], + "title": "OtlpGrpcOtelExporter", + "type": "object" + } + ] + }, + "OtelHttpProtocol": { + "enum": [ + "binary", + "json" + ], + "type": "string" + }, + "OtelRequirements": { + "properties": { + "environment": { + "type": [ + "string", + "null" + ] + }, + "exporter": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "log_agent_responses": { + "type": [ + "boolean", + "null" + ] + }, + "log_guardian_assessments": { + "type": [ + "boolean", + "null" + ] + }, + "log_user_prompt": { + "type": [ + "boolean", + "null" + ] + }, + "metrics_exporter": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "span_attributes": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "tool_result": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelToolResultLogConfig" + }, + { + "type": "null" + } + ] + }, + "trace_exporter": { + "anyOf": [ + { + "$ref": "#/definitions/v2/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "tracestate": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "OtelTlsConfig": { + "properties": { + "ca-certificate": { + "type": [ + "string", + "null" + ] + }, + "client-certificate": { + "type": [ + "string", + "null" + ] + }, + "client-private-key": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "OtelToolResultLogConfig": { + "properties": { + "max_bytes": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "max_bytes" + ], + "type": "object" + }, "OverriddenMetadata": { "properties": { "effectiveValue": true, @@ -17824,6 +18428,13 @@ "format": "int32", "type": "integer" }, + "outputDrainTimedOut": { + "description": "Whether output collection hit its post-exit deadline before seeing EOF or reaching the requested byte cap on either stream. Older servers omit this field.", + "type": [ + "boolean", + "null" + ] + }, "processHandle": { "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", "type": "string" @@ -19801,8 +20412,7 @@ }, "ReviewDelivery": { "enum": [ - "inline", - "detached" + "inline" ], "type": "string" }, @@ -19819,7 +20429,7 @@ } ], "default": null, - "description": "Where to run the review: inline (default) on the current thread or detached on a new thread (returned in `reviewThreadId`). Detached delivery is deprecated and emits `deprecationNotice`. Use `thread/start` followed by an inline review for a separate review thread." + "description": "Reviews run inline on the supplied thread. Omit this field or pass `inline`. To review on a separate thread, start a thread first and use its id." }, "target": { "$ref": "#/definitions/v2/ReviewTarget" @@ -19839,7 +20449,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "reviewThreadId": { - "description": "Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", + "description": "The thread id supplied to `review/start`.", "type": "string" }, "turn": { @@ -22135,6 +22745,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -22917,6 +23528,14 @@ ], "default": null }, + "generationId": { + "default": null, + "description": "Image generation identifier for the generated image, when provided by the image backend.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, @@ -23037,7 +23656,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadItemEntry": { "properties": { @@ -25689,6 +26329,14 @@ }, "type": "array" }, + "nativeUsageUsdMicros": { + "description": "Native dollar usage, independent of credit estimates. None means unknown, not zero.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, "threadId": { "type": "string" } diff --git a/protocol/codex_app_server_protocol.v2.schemas.json b/protocol/codex_app_server_protocol.v2.schemas.json index cf6063c..b6a7622 100644 --- a/protocol/codex_app_server_protocol.v2.schemas.json +++ b/protocol/codex_app_server_protocol.v2.schemas.json @@ -40,6 +40,13 @@ ], "title": "ChatgptAccountType", "type": "string" + }, + "workspaceName": { + "description": "Display name of the selected ChatGPT workspace, when available.", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -69,6 +76,23 @@ ], "title": "AmazonBedrockAccount", "type": "object" + }, + { + "description": "Google Cloud Vertex AI with externally managed credentials.", + "properties": { + "type": { + "enum": [ + "gcp" + ], + "title": "GcpAccountType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "GcpAccount", + "type": "object" } ] }, @@ -130,6 +154,103 @@ ], "type": "string" }, + "AccountSession": { + "properties": { + "displayName": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "imageUrl": { + "type": [ + "string", + "null" + ] + }, + "isActive": { + "type": "boolean" + }, + "lastUsedAt": { + "format": "int64", + "type": "integer" + }, + "selectedWorkspaceAccountId": { + "type": [ + "string", + "null" + ] + }, + "sessionId": { + "type": "string" + }, + "userId": { + "type": [ + "string", + "null" + ] + }, + "workspaces": { + "items": { + "$ref": "#/definitions/AccountSessionWorkspace" + }, + "type": "array" + } + }, + "required": [ + "isActive", + "lastUsedAt", + "sessionId", + "workspaces" + ], + "type": "object" + }, + "AccountSessionWorkspace": { + "properties": { + "accountId": { + "type": "string" + }, + "imageUrl": { + "type": [ + "string", + "null" + ] + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/AccountSessionWorkspaceKind" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "accountId" + ], + "type": "object" + }, + "AccountSessionWorkspaceKind": { + "enum": [ + "personal", + "workspace" + ], + "type": "string" + }, "AccountTokenUsageDailyBucket": { "properties": { "startDate": { @@ -252,6 +373,13 @@ "kind": { "$ref": "#/definitions/AdditionalContextKind" }, + "name": { + "description": "Optional name for untrusted context, used to label its tool output.", + "type": [ + "string", + "null" + ] + }, "value": { "type": "string" } @@ -1015,6 +1143,42 @@ "type": "null" } ] + }, + "storage": { + "anyOf": [ + { + "$ref": "#/definitions/ApplicationStorageRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ApplicationStorageRequirements": { + "description": "Managed storage settings for new Desktop workspaces, not filesystem access restrictions.", + "properties": { + "defaultProjectParent": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + }, + "projectlessWorkspaceRoot": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -3967,6 +4131,30 @@ "title": "Feedback/uploadRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "feedback/save" + ], + "title": "Feedback/saveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FeedbackSaveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Feedback/saveRequest", + "type": "object" + }, { "description": "Execute a standalone command (argv vector) under the server's sandbox.", "properties": { @@ -4318,6 +4506,8 @@ "rateLimitExceeded", "flexUnavailable", "serverOverloaded", + "modelNotFound", + "policyCapacityUnavailable", "cyberPolicy", "misalignmentPolicyViolation", "tooManyDenials", @@ -6032,6 +6222,16 @@ } ] }, + "otel": { + "anyOf": [ + { + "$ref": "#/definitions/OtelRequirements" + }, + { + "type": "null" + } + ] + }, "sqliteHome": { "type": [ "string", @@ -7623,6 +7823,85 @@ }, "type": "object" }, + "FeedbackSaveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "classification": { + "type": "string" + }, + "extraLogFiles": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeLogs": { + "type": "boolean" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "savePath": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "Absolute, previously unused ZIP path on the app-server host. The caller owns this file, including after a disconnected RPC." + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "classification", + "savePath" + ], + "title": "FeedbackSaveParams", + "type": "object" + }, + "FeedbackSaveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "promptHash": { + "description": "Same prompt hash as feedback/upload; null when prompt metadata is absent.", + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "savedPath", + "threadId" + ], + "title": "FeedbackSaveResponse", + "type": "object" + }, "FeedbackUploadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -9382,6 +9661,13 @@ "enabled": { "type": "boolean" }, + "environmentId": { + "description": "The owning execution environment, if this hook came from its requirements.", + "type": [ + "string", + "null" + ] + }, "eventName": { "$ref": "#/definitions/HookEventName" }, @@ -9549,7 +9835,7 @@ "default": "unknown" }, "sourcePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "$ref": "#/definitions/LegacyAppPathString" }, "startedAt": { "format": "int64", @@ -9670,11 +9956,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "cwds": { - "description": "When empty, defaults to the current session working directory.", + "description": "Working directories to inspect when no thread is specified. An empty list uses the session cwd.", "items": { "type": "string" }, "type": "array" + }, + "threadId": { + "description": "List a loaded thread's hooks, including its selected environments. Omit `cwds` in this mode.", + "type": [ + "string", + "null" + ] } }, "title": "HooksListParams", @@ -11810,6 +12103,81 @@ "title": "ModelProviderCapabilitiesReadResponse", "type": "object" }, + "ModelProviderError": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Structured JSON-RPC error data. Unknown errors must retain the client's normal fallback.", + "oneOf": [ + { + "properties": { + "currentModelProvider": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderMismatch" + ], + "title": "ModelProviderMismatchv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "currentModelProvider", + "modelProvider", + "type" + ], + "title": "ModelProviderMismatchv2::ModelProviderError", + "type": "object" + }, + { + "properties": { + "modelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderNotConfigured" + ], + "title": "ModelProviderNotConfiguredv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "modelProvider", + "type" + ], + "title": "ModelProviderNotConfiguredv2::ModelProviderError", + "type": "object" + }, + { + "properties": { + "modelProvider": { + "type": "string" + }, + "requiredModelProvider": { + "type": "string" + }, + "type": { + "enum": [ + "modelProviderPolicyConflict" + ], + "title": "ModelProviderPolicyConflictv2::ModelProviderErrorType", + "type": "string" + } + }, + "required": [ + "modelProvider", + "requiredModelProvider", + "type" + ], + "title": "ModelProviderPolicyConflictv2::ModelProviderError", + "type": "object" + } + ], + "title": "ModelProviderError" + }, "ModelRerouteReason": { "enum": [ "highRiskCyberActivity" @@ -12108,7 +12476,7 @@ "additionalProperties": { "$ref": "#/definitions/NetworkDomainPermission" }, - "description": "Canonical network permission map for `experimental_network`.", + "description": "Canonical managed network permission map.", "type": [ "object", "null" @@ -12135,6 +12503,16 @@ "null" ] }, + "reviewDomains": { + "description": "Host patterns routed to network approval before ordinary allow rules.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "socksPort": { "format": "uint16", "minimum": 0.0, @@ -12147,7 +12525,7 @@ "additionalProperties": { "$ref": "#/definitions/NetworkUnixSocketPermission" }, - "description": "Canonical unix socket permission map for `experimental_network`.", + "description": "Canonical managed unix socket permission map.", "type": [ "object", "null" @@ -12221,6 +12599,232 @@ ], "title": "Nullable_GetAccountTokenUsageParams" }, + "OtelExporter": { + "oneOf": [ + { + "enum": [ + "none", + "statsig" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "otlp-http": { + "properties": { + "endpoint": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Header names are retained; values are redacted by the server.", + "type": "object" + }, + "protocol": { + "$ref": "#/definitions/OtelHttpProtocol" + }, + "tls": { + "anyOf": [ + { + "$ref": "#/definitions/OtelTlsConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "endpoint", + "headers", + "protocol" + ], + "type": "object" + } + }, + "required": [ + "otlp-http" + ], + "title": "OtlpHttpOtelExporter", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "otlp-grpc": { + "properties": { + "endpoint": { + "type": "string" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Header names are retained; values are redacted by the server.", + "type": "object" + }, + "tls": { + "anyOf": [ + { + "$ref": "#/definitions/OtelTlsConfig" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "endpoint", + "headers" + ], + "type": "object" + } + }, + "required": [ + "otlp-grpc" + ], + "title": "OtlpGrpcOtelExporter", + "type": "object" + } + ] + }, + "OtelHttpProtocol": { + "enum": [ + "binary", + "json" + ], + "type": "string" + }, + "OtelRequirements": { + "properties": { + "environment": { + "type": [ + "string", + "null" + ] + }, + "exporter": { + "anyOf": [ + { + "$ref": "#/definitions/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "log_agent_responses": { + "type": [ + "boolean", + "null" + ] + }, + "log_guardian_assessments": { + "type": [ + "boolean", + "null" + ] + }, + "log_user_prompt": { + "type": [ + "boolean", + "null" + ] + }, + "metrics_exporter": { + "anyOf": [ + { + "$ref": "#/definitions/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "span_attributes": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "tool_result": { + "anyOf": [ + { + "$ref": "#/definitions/OtelToolResultLogConfig" + }, + { + "type": "null" + } + ] + }, + "trace_exporter": { + "anyOf": [ + { + "$ref": "#/definitions/OtelExporter" + }, + { + "type": "null" + } + ] + }, + "tracestate": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "OtelTlsConfig": { + "properties": { + "ca-certificate": { + "type": [ + "string", + "null" + ] + }, + "client-certificate": { + "type": [ + "string", + "null" + ] + }, + "client-private-key": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "OtelToolResultLogConfig": { + "properties": { + "max_bytes": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "max_bytes" + ], + "type": "object" + }, "OverriddenMetadata": { "properties": { "effectiveValue": true, @@ -13816,6 +14420,13 @@ "format": "int32", "type": "integer" }, + "outputDrainTimedOut": { + "description": "Whether output collection hit its post-exit deadline before seeing EOF or reaching the requested byte cap on either stream. Older servers omit this field.", + "type": [ + "boolean", + "null" + ] + }, "processHandle": { "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", "type": "string" @@ -15793,8 +16404,7 @@ }, "ReviewDelivery": { "enum": [ - "inline", - "detached" + "inline" ], "type": "string" }, @@ -15811,7 +16421,7 @@ } ], "default": null, - "description": "Where to run the review: inline (default) on the current thread or detached on a new thread (returned in `reviewThreadId`). Detached delivery is deprecated and emits `deprecationNotice`. Use `thread/start` followed by an inline review for a separate review thread." + "description": "Reviews run inline on the supplied thread. Omit this field or pass `inline`. To review on a separate thread, start a thread first and use its id." }, "target": { "$ref": "#/definitions/ReviewTarget" @@ -15831,7 +16441,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "reviewThreadId": { - "description": "Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", + "description": "The thread id supplied to `review/start`.", "type": "string" }, "turn": { @@ -19849,6 +20459,7 @@ "type": "object" }, "ThreadItem": { + "description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.", "oneOf": [ { "properties": { @@ -20631,6 +21242,14 @@ ], "default": null }, + "generationId": { + "default": null, + "description": "Image generation identifier for the generated image, when provided by the image backend.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, @@ -20751,7 +21370,28 @@ "title": "ContextCompactionThreadItem", "type": "object" } - ] + ], + "properties": { + "completedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item completed, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "startedAtMs": { + "default": null, + "description": "Unix timestamp in milliseconds when the item started, if recorded.", + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" }, "ThreadItemEntry": { "properties": { @@ -23403,6 +24043,14 @@ }, "type": "array" }, + "nativeUsageUsdMicros": { + "description": "Native dollar usage, independent of credit estimates. None means unknown, not zero.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, "threadId": { "type": "string" } diff --git a/start.sh b/start.sh index 67205b6..51d9dd7 100755 --- a/start.sh +++ b/start.sh @@ -21,7 +21,7 @@ cd "$(dirname "$0")" # 本机 VM 修复:系统里的 codex 是裸二进制,旁边没有 # codex-code-mode-host 和 codex-resources/bwrap,工具会 fail closed。 -# 本项目 bin/ 里是配对好的官方 0.164.0-alpha.9 三件套 +# 本项目 bin/ 里是配对好的官方 0.166.0 三件套 # (codex + codex-code-mode-host + codex-resources/bwrap), # 优先用它;Mac 上用 npm 正常安装的 codex 自带这三件套, # 不需要 bin/,直接用 PATH 里的 codex 即可。 diff --git a/tests/test_host_protocol_mapping.py b/tests/test_host_protocol_mapping.py new file mode 100644 index 0000000..335afbe --- /dev/null +++ b/tests/test_host_protocol_mapping.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""host.py 方法 ↔ protocol/ schema 映射钉死测试(引擎升级 0.166.0 批)。 + +钉死两件事: +1. host.py 实际触及的方法集合(发出的请求 + 处理的入站通知)必须 + 等于下表;host.py 新增方法时本测试先红,逼升级者同步更新映射, + 而不是悄悄带一个 schema 里没有的方法上线。 +2. 下表每个方法在 protocol/ 两 bundle 里存在,且 params 引用与 + 0.166.0 重导结果一致;上游删方法或改 params 引用时本测试先红。 + +方法数勘误:2026-11-05 对齐表写“17 个方法/通知”并不准确—— +实查 host.py 触及 21 个方法字符串:18 个发出请求(含 initialize +握手)+ 3 个入站通知;对齐表列出的 18 项漏了 initialize、 +app/list、mcpServerStatus/list 三个。本测试以实查 21 项为准。 +其中 17 个应用请求(不含 initialize)与对齐表原意一致,只是 +对齐表把其中 app/list、mcpServerStatus/list 两项漏列了。 + +本测试只读 host.py 文本与 protocol/ 两 bundle,不 import host, +不启动任何 app-server 进程,不做模型调用。 +""" + +import json +import os +import re +import unittest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# method -> v2 ClientRequest 的 params 引用;None 表示该请求的 +# params 在 schema 里是 {"type": "null"}(config/mcpServer/reload)。 +EXPECTED_REQUESTS = { + "initialize": "InitializeParams", + "thread/start": "ThreadStartParams", + "thread/resume": "ThreadResumeParams", + "thread/fork": "ThreadForkParams", + "thread/list": "ThreadListParams", + "thread/read": "ThreadReadParams", + "thread/name/set": "ThreadSetNameParams", + "thread/archive": "ThreadArchiveParams", + "thread/unarchive": "ThreadUnarchiveParams", + "thread/compact/start": "ThreadCompactStartParams", + "turn/start": "TurnStartParams", + "turn/interrupt": "TurnInterruptParams", + "review/start": "ReviewStartParams", + "skills/list": "SkillsListParams", + "app/list": "AppsListParams", + "mcpServerStatus/list": "ListMcpServerStatusParams", + "mcpServer/oauth/login": "McpServerOauthLoginParams", + "config/mcpServer/reload": None, +} + +# method -> (bundle, container, params 引用)。turn/completed 与 +# thread/tokenUsage/updated 是服务端发来的通知(v2 ServerNotification); +# mcpServer/elicitation/request 是服务端发来的请求,活在 v1 bundle +# 的 ServerRequest 里(v2 bundle 没有 ServerRequest 定义)。 +EXPECTED_INBOUND = { + "turn/completed": ("v2", "ServerNotification", "TurnCompletedNotification"), + "thread/tokenUsage/updated": ( + "v2", "ServerNotification", "ThreadTokenUsageUpdatedNotification"), + "mcpServer/elicitation/request": ( + "v1", "ServerRequest", "McpServerElicitationRequestParams"), +} + + +def _load_bundle(name): + path = os.path.join(ROOT, "protocol", name) + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _method_map(bundle, container): + out = {} + for entry in bundle["definitions"].get(container, {}).get("oneOf", []): + props = entry.get("properties", {}) + method = (props.get("method", {}).get("enum") or [None])[0] + if not method: + continue + params = props.get("params", {}) + ref = params.get("$ref", "") + out[method] = ref.split("/")[-1] if ref else params + return out + + +class TestHostProtocolMapping(unittest.TestCase): + def test_host_method_set_matches_pin(self): + with open(os.path.join(ROOT, "host.py"), encoding="utf-8") as f: + src = f.read() + requests = set(re.findall(r'\.request\(\s*"([^"]+)"', src)) + inbound = set( + re.findall(r'\.get\("method"\)\s*[=!]=\s*"([^"]+)"', src)) + self.assertEqual(requests, set(EXPECTED_REQUESTS), + "host.py 发出的请求集合变了,先更新映射表再合") + self.assertEqual(inbound, set(EXPECTED_INBOUND), + "host.py 处理的入站通知集合变了,先更新映射表再合") + + def test_requests_in_v2_schema(self): + bundle = _load_bundle("codex_app_server_protocol.v2.schemas.json") + methods = _method_map(bundle, "ClientRequest") + for method, ref in EXPECTED_REQUESTS.items(): + self.assertIn(method, methods, f"v2 schema 缺少 host 请求 {method}") + if ref is None: + self.assertEqual(methods[method], {"type": "null"}, + f"{method} 的 params 应为 null 类型") + else: + self.assertEqual(methods[method], ref, + f"{method} 的 params 引用变了") + + def test_inbound_in_schema(self): + bundles = { + "v1": _load_bundle("codex_app_server_protocol.schemas.json"), + "v2": _load_bundle("codex_app_server_protocol.v2.schemas.json"), + } + for method, (bundle_name, container, ref) in EXPECTED_INBOUND.items(): + methods = _method_map(bundles[bundle_name], container) + self.assertIn(method, methods, + f"{bundle_name} schema {container} 缺少 {method}") + self.assertEqual(methods[method], ref, + f"{method} 的 params 引用变了") + + def test_bundles_parse(self): + for name in ("codex_app_server_protocol.schemas.json", + "codex_app_server_protocol.v2.schemas.json"): + bundle = _load_bundle(name) + self.assertIn("$schema", bundle) + self.assertIn("definitions", bundle) + self.assertGreater(len(bundle["definitions"]), 0) + + +if __name__ == "__main__": + unittest.main()