改审批策略之前,先把影响面追完
从 AskForApproval::Granular 的真实协议、配置和运行时分支出发,推演 network_approval 能力位需要怎样保持兼容并守住安全边界。
上一章的 GuardianWarning 是一次 transient notification。客户端如果当时没有收到,重启后也不会补发。审批策略的性质不同:它在请求发生前就决定一条路径能不能进入 hook、Guardian 或用户界面,还会出现在配置、wire payload、turn context 和生成 schema 中。
这类改动最容易被低估成“给 struct 加一个 bool”。Rust 编译器能帮你找到一部分 struct literal,却找不到旧 JSON 应该默认成什么,也不会提醒模型 prompt 仍在描述旧能力,更不会替你决定已经 session-approved 的 host 要不要撤销。
这一章先还原 rust-v0.144.6 中真实存在的 AskForApproval::Granular,再推演一个固定 tag 中不存在的字段:network_approval。推演的目标不是提交补丁,而是把语义、兼容边界、影响文件和测试合同写到足够明确,下一步实现时不需要猜。
可复现实验:先锁住现有 Granular 基线
我在固定 tag 的 detached checkout 中运行了十组命令:
just test -p codex-protocol \
granular_approval_config_defaults_missing_optional_flags_to_false
just test -p codex-app-server-protocol \
ask_for_approval_granular_round_trips_request_permissions_flag
just test -p codex-app-server-protocol \
ask_for_approval_granular_defaults_missing_optional_flags_to_false
just test -p codex-app-server-protocol \
ask_for_approval_granular_is_marked_experimental
just test -p codex-config deserialize_allowed_approval_policies
just test -p codex-app-server-protocol \
config_requirements_granular_allowed_approval_policy_is_marked_experimental
just test -p codex-core only_never_policy_disables_network_approval_flow
just test -p codex-app-server --test all \
thread_start_granular_approval_policy_requires_experimental_api_capability
just test -p codex-core config_schema_matches_fixture
just test -p codex-app-server-protocol --test schema_fixtures
前九组各选中一条测试,最后一组 schema fixtures 选中两条。结果合计 11 passed, 0 failed。
这 11 条是现有行为基线,不是 network_approval 的实现证明。core 与 app-server 的 default 测试只确认 skill_approval、request_permissions 缺省为 false;round-trip 只覆盖现有字段;requirements 解析测试只用了字符串策略;network runtime 测试也没有构造 Granular。这些测试下一步都需要扩充,不能因为名字里出现了 granular 或 network 就当作新合同已经存在。
固定 tag 中的 Granular 到底控制什么
core protocol 把审批策略建模成四种值:UnlessTrusted、OnRequest、Granular(...) 和 Never。GranularApprovalConfig 有五个能力位:
| 字段 | 当前语义 | 缺省行为 |
|---|---|---|
sandbox_approval | shell 的 sandbox escape、inline additional permissions 与 require_escalated 是否可询问 | 必填 |
rules | execpolicy prompt rule 是否可询问 | 必填 |
skill_approval | skill script execution 是否可询问 | 缺省 false |
request_permissions | 内置 request_permissions 工具是否可询问 | 缺省 false |
mcp_elicitations | MCP elicitation 是否可询问 | 必填 |
这里的 bool 不是“是否自动批准”。true 只让该类别有资格进入审批流,后面仍可能被 hook、Guardian 或用户拒绝;false 则在对应 runtime 分支直接拒绝,不弹出请求。
这已经说明新增字段不能只改一个 match。AskForApproval 派生了 Serde、JsonSchema 和 TypeScript;它也是 hash/equality 的一部分,会作为完整值进入 managed constraints。任何默认值错误,都会同时改变配置解析、旧 rollout 恢复和 requirements 比较。
同一个策略至少有两份公开表示
ConfigToml.approval_policy 直接使用 core AskForApproval。所以配置入口不需要再定义一份 enum,但生成的 core/config.schema.json 会把字段必填性与默认值公开给编辑器和配置校验器。
app-server-protocol 另有一份 v2 AskForApproval。它把 Granular 写成 struct-like enum variant,保留自己的 Serde/JsonSchema/TS 派生,并通过 to_core 与 From<CoreAskForApproval> 手工复制每一个字段。该 variant 还标着 askForApproval.granular experimental capability。
这个标记会递归穿过所有携带 approval policy 的公开输入与投影。固定 tag 的协议测试覆盖 thread/start、thread/resume、thread/fork、turn/start、thread/settings/update,以及 config/read 与 configRequirements/read 对应类型。app-server 的 live capability test 只选了 thread/start 做进程级验证;新增字段后,其他入口的 struct literal、递归 marker 和 schema 仍要一起更新,不能用这一条 live test 代替。
flowchart TD
accTitle: Granular approval policy 的解析、投影、执行与恢复影响面
accDescr: config TOML 和 app-server request 分别进入 core AskForApproval;完整策略受 requirements 约束,写入 turn context,并同时影响 runtime network decision、模型权限提示、TUI 状态与生成 schema。
A["config.toml\ncore AskForApproval"] --> C["Constrained<AskForApproval>"]
B["app-server v2 AskForApproval\nexperimental conversion"] --> C
R["managed requirements\n完整值 equality"] --> C
C --> D["TurnContext\n当前 runtime policy"]
D --> E["network approval decision"]
D --> F["permissions prompt"]
D --> G["SessionConfigured / TUI projection"]
D --> H["TurnContext rollout baseline\nresume / fork"]
A --> S["config JSON schema"]
B --> T["app-server JSON / TS schema"]
Requirements 比较的是整个值
managed allowed_approval_policies 不是“允许 granular 这个 variant 就行”。Constrained 最终用 policies.contains(candidate) 比较完整 AskForApproval 值。
新增 network_approval 后,下面两份策略应当被视为不同值:
Granular { network_approval: true, ... }
Granular { network_approval: false, ... }
这使默认值变得更敏感。旧 requirements payload 省略新字段时,如果被解析成 false,原本允许网络询问的管理策略会在升级后静默收紧;若反过来把一个原本应收紧的新 payload 误解析成 true,又会放宽安全边界。兼容默认必须在设计阶段定死,并由旧载荷测试证明。
命令行又是另一条边界。-a/--ask-for-approval 只接受 untrusted、on-request、never 三个无负载值,无法表达 Granular 对象。这个提案可以继续只通过 config 或 -c 传入;如果产品要求一级 CLI,就要设计对象解析或单独 flag,不能把 granular 塞进现有 value enum 后丢掉六个字段。
本章提案:只控制 allowlist miss 的审批资格
下面这个字段是设计草案,rust-v0.144.6 中不存在:
#[serde(default = "default_true")]
pub network_approval: bool;
还需要新增并测试 default_true(),core protocol 与 app-server v2 的两份表示都要采用相同缺省。生成的 config JSON schema 应显示 default: true 且不把字段列入 required;新版 TypeScript 可以要求调用者显式填写,但服务端必须继续接受旧客户端省略字段的 payload。
语义只覆盖一个场景:受管网络代理发现目标 host 不在 allowed_domains 时,是否允许这次 miss 进入审批流。
| 策略 | 新 allowlist miss | 其他网络行为 |
|---|---|---|
Never | 继续直接拒绝 | 与固定 tag 相同 |
Granular { network_approval: false } | 在 hook、Guardian、用户 UI 前直接拒绝 | 已允许 domain 继续工作;不改变 sandbox 网络能力 |
Granular { network_approval: true } | 保持当前询问路径 | 仍需通过 hook、Guardian 或用户决策 |
OnRequest / UnlessTrusted | 保持当前询问路径 | 与固定 tag 相同 |
它不是“禁网”。allowlist 已命中的请求不会进入这段逻辑;PermissionProfile 是否提供受管网络、proxy 如何执行规则,也不由这个 bool 重写。
它也不接管另外两类扩权:
request_permissions工具主动请求network权限,仍由现有request_permissions字段控制;- shell inline additional permissions 与
require_escalated,仍由sandbox_approval控制。
把三者合成一个 network 开关看似简单,实际会改变已经发布的 Granular 分类法。
拒绝检查应该落在哪里
固定 tag 的 allows_network_approval_flow 只有一个判断:策略不是 Never 就返回 true。handle_inline_policy_request 的关键顺序是:
解析 request attribution / environment
-> 命中 session_denied_hosts 则拒绝
-> 命中 session_approved_hosts 则允许
-> 合并同 host 的 pending request
-> 检查 Managed permission profile
-> 检查 AskForApproval 是否允许 network approval flow
-> permission request hooks
-> Guardian 或用户 approval UI
-> 缓存 ApprovedForSession / deny,或持久化 policy amendment
最小改动只扩展 allows_network_approval_flow:Never 和 Granular(false) 返回 false,其余返回 true。这样直接拒绝仍在 hook、Guardian 和 UI 之前,现有 outcome 记录与 pending cleanup 也继续复用。
这个位置还固定了一个刻意保留的边界:session_approved_hosts 查询发生在 policy check 之前。把字段从 true 改成 false,不会自动撤销当前 session 已批准的 host;已经持久化进 network policy 的 allow rule 也会在更早的 allowlist 判断中生效,不会再次形成 miss。
如果产品需要“切换为 false 立即撤销一切已批准网络访问”,那是另一项功能:要清 session cache、回滚 policy amendment,并处理正在等待的 request。不能借这个字段的名字悄悄附带完成。
模型提示必须和 runtime 一起变
Granular 的 permissions prompt 会列出允许询问和自动拒绝的类别。固定 tag 只列五个现有字段,对 managed allowlist miss 的审批资格保持沉默。若 runtime 已因 network_approval: false 直接拒绝,prompt 仍无法表达这个差异,模型就可能反复走一条必败路径。
新增字段后,granular_instructions 的 categories 必须加入 `network_approval`,并测试 true/false 分别出现在正确分组。文字还要说明它只控制 managed allowlist miss,避免与 request_permissions 混淆。
TUI 不需要新请求类型,但仍有显示边界
network miss 当前复用 command approval,请求中带 network_approval_context。TUI overlay 已据此把标题改成 “Do you want to approve network access to …?”。network_approval: false 时,这个 request 根本不应到达 overlay,因此最小实现不需要新增一种 UI event。
但 TUI 的状态面只对整个 policy 调 to_string(),Granular 最终显示为 granular,看不到任何子字段;permissions menu 也只选择预设 profile/approval policy,不编辑 Granular object。若用户需要在界面里理解或修改 network_approval,必须另做子字段详情与编辑流程。仅支持配置文件时,则要明确这是当前 UI 边界,并测试 false 时不会弹网络审批。
持久化兼容不能只测 config.toml
当前 turn 的 TurnContextItem 会把完整 approval_policy 写入 rollout,供 resume/fork 重建最新 durable baseline。ThreadSettingsApplied 也保存设置变更。旧 rollout 中的 Granular object 没有新字段,因此 core Serde default 必须同样覆盖恢复路径。
app-server 的 SessionConfiguredEvent 和 TUI thread session state 又会投影当前策略。新增字段虽然不要求新 notification method,但现有 payload、缓存和 fork config 都会随完整 enum 变化。只测试 config TOML 能解析,无法证明一条旧 thread 还能 resume。
完整改动清单
| 层 | 必改内容 | 新增或扩展的测试 |
|---|---|---|
| core protocol | 字段、default_true、accessor、所有 struct literal | old JSON 缺字段为 true;显式 false 保真 |
| config schema | 重新生成 core/config.schema.json | property default true、非 required、fixture 一致 |
| managed requirements | 完整值继续区分 true/false | 旧 requirements payload 兼容;两种候选约束 |
| CLI | 最小方案保持 config/-c,不伪造无负载 granular | help/value parser 仍拒绝无法表达的对象 |
| app-server protocol | v2 字段、default、双向转换、experimental 标记 | true/false round-trip、old payload、capability gate |
| network runtime | allows_network_approval_flow 读取新 accessor | false 直接拒绝;true 当前路径;approved host 不撤销 |
| prompt | 增加精确定义的 Granular 类别 | true/false 分组,不与 request_permissions 混淆 |
| TUI | overlay 无新增类型;决定是否展示/编辑子字段 | false 不出现 request;true 仍显示 network target |
| rollout/resume | 旧 TurnContextItem 缺字段仍解析为 true | old rollout resume、false round-trip、fork 保真 |
| generated artifacts | config 与 app-server JSON/TS 全部重生成 | config_schema_matches_fixture、schema fixtures |
逐文件审计清单
上表用于理解职责,不能替代文件清单。固定 tag 中,最先需要做语义修改或显式确认“无需修改”的入口是:
codex-rs/protocol/src/protocol.rs
codex-rs/config/src/config_toml.rs
codex-rs/config/src/config_requirements.rs
codex-rs/core/src/tools/network_approval.rs
codex-rs/core/src/tools/network_approval_tests.rs
codex-rs/prompts/src/permissions_instructions.rs
codex-rs/prompts/src/permissions_instructions_tests.rs
codex-rs/utils/cli/src/approval_mode_cli_arg.rs
codex-rs/app-server-protocol/src/protocol/v2/shared.rs
codex-rs/app-server-protocol/src/protocol/v2/tests.rs
codex-rs/app-server/tests/suite/v2/experimental_api.rs
codex-rs/tui/src/bottom_pane/approval_overlay.rs
codex-rs/tui/src/chatwidget/status_surfaces.rs
codex-rs/tui/src/chatwidget/permissions_menu.rs
codex-rs/tui/src/app/thread_session_state.rs
其中 CLI 和现有 overlay 在最小方案里可以不改行为,但必须留下审计结论:CLI 继续不表达带载荷的 Granular;overlay 继续复用 network command approval,false 时不会收到请求。
新增字段还会让所有 Granular struct literal 和 match 成为编译或语义检查点。完整集合保留在这里,正文不逐个展开职责。
展开 31 个非生成 Rust 文件
codex-rs/protocol/src/protocol.rs
codex-rs/app-server-protocol/src/protocol/v2/shared.rs
codex-rs/app-server-protocol/src/protocol/v2/tests.rs
codex-rs/app-server/tests/suite/v2/experimental_api.rs
codex-rs/codex-mcp/src/connection_manager_tests.rs
codex-rs/codex-mcp/src/elicitation.rs
codex-rs/codex-mcp/src/mcp/mod_tests.rs
codex-rs/core/src/exec_policy.rs
codex-rs/core/src/exec_policy_tests.rs
codex-rs/core/src/guardian/review.rs
codex-rs/core/src/guardian/tests.rs
codex-rs/core/src/hook_runtime.rs
codex-rs/core/src/mcp_tool_call.rs
codex-rs/core/src/mcp_tool_call_tests.rs
codex-rs/core/src/safety.rs
codex-rs/core/src/safety_tests.rs
codex-rs/core/src/session/mod.rs
codex-rs/core/src/session/tests.rs
codex-rs/core/src/tools/handlers/mod.rs
codex-rs/core/src/tools/runtimes/apply_patch.rs
codex-rs/core/src/tools/runtimes/apply_patch_tests.rs
codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs
codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs
codex-rs/core/src/tools/sandboxing.rs
codex-rs/core/src/tools/sandboxing_tests.rs
codex-rs/core/tests/suite/approvals.rs
codex-rs/core/tests/suite/exec_policy.rs
codex-rs/core/tests/suite/request_permissions.rs
codex-rs/core/tests/suite/skill_approval.rs
codex-rs/prompts/src/permissions_instructions.rs
codex-rs/prompts/src/permissions_instructions_tests.rs这不表示 31 个文件都要新增 network 分支。很多测试只是必须补上 network_approval 字段,并重新确认原类别行为没有变化;exec_policy、MCP、skill、apply-patch 与 sandboxing 尤其不能因为补默认值就被误接到新语义。
app-server generator 当前会把 AskForApproval 扩散到 26 个 JSON/TypeScript 产物。更新后应由生成命令决定实际 diff,再逐个确认没有遗漏或手工编辑。
展开 26 个 app-server 生成物与 core config schema
codex-rs/app-server-protocol/schema/json/ClientRequest.json
codex-rs/app-server-protocol/schema/json/ServerNotification.json
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json
codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json
codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json
codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json
codex-rs/app-server-protocol/schema/json/v2/ThreadSettingsUpdatedNotification.json
codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json
codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json
codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json
codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts
codex-rs/app-server-protocol/schema/typescript/v2/Config.ts
codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadSettings.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts
codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts
codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts
codex-rs/app-server-protocol/schema/typescript/v2/index.tscore 配置生成物另有:
codex-rs/core/config.schema.json必须扩展的测试族
文件列全以后,再按合同分组,避免只修编译:
- core protocol:旧 Granular JSON 缺字段默认 true、显式 true/false、accessor;
- app-server protocol:旧 payload、双向 conversion、TypeScript/JSON shape;
- recursive experimental:config、config requirements、thread start/resume/fork、turn start、thread settings update;
- live capability:至少保留
thread/start的进程级拒绝,确认新增字段没有绕过experimentalApi; - managed requirements:Granular(true/false) 完整值约束和旧对象默认;
- network runtime:直接拒绝、current allow path、session-approved host、policy amendment、pending request;
- downstream isolation:false 时 hook、Guardian、user sender 都未调用;
- adjacent categories:execpolicy、sandbox、skill、request_permissions、MCP、apply-patch 行为不变;
- prompt/TUI:类别分组、false 无 overlay、true 显示 network target、状态面边界;
- rollout:旧 TurnContext resume、新 false round-trip、fork 保真;
- generators:config schema fixture、app-server JSON 与 TypeScript fixtures。
生成文件要通过仓库命令更新:
just write-config-schema
just write-app-server-schema --experimental
手改某个 JSON 文件不算完成。源类型、转换、fixtures 和生成器输出必须在同一提交里收敛。
源码依据
本章对固定 tag 的描述只覆盖现有五字段 Granular 与当前 network approval flow。network_approval、default_true 和所有预期测试都是明确推演,不能从源码链接中误读为仓库已经实现。
现场运行的 11 条测试证明当前默认值、转换、experimental marker 与 thread/start capability gate、requirements 的字符串解析和 marker、network helper,以及 schema fixtures 在固定 tag 通过。它们没有验证 Granular 完整值约束、旧 requirements 对象,也没有编译本章提案;真正实现后,旧测试要扩展,新测试也要先失败一次,才能证明影响面没有停留在清单上。
失败边界
- core 默认 true、app-server 默认 false:config 可以恢复,旧客户端 payload 却被静默收紧。
- 只改 core enum:v2 conversion 丢字段,client 看见的策略与 runtime 不同。
- schema 标成 required:服务端可能接受旧 payload,生成客户端却无法表达兼容形状。
- 把字段解释为禁网:allowlisted domain、PermissionProfile 和 request_permissions 的职责被意外覆盖。
- policy check 放在 hook 之后:false 仍触发外部 hook 或 Guardian,违反直接拒绝语义。
- 切换 false 时清空 approved host:超出提案边界,并可能让 in-flight request 出现竞态。
- prompt 不更新:模型持续发起 runtime 必拒的网络询问。
- TUI 只显示
granular:用户无法从状态面判断 network approval 是否关闭。 - 只跑 unit test:旧 rollout、managed requirements 和 generated schema 仍可能在发布后失败。
动手改一个地方
不要先补 UI。先在 scratch branch 只完成“默认值 + runtime 直接拒绝 + 协议保真”这条最窄纵切:
- 给 core 与 app-server 两份类型增加
network_approval,旧 payload 缺省为 true; - 扩展双向 conversion 与所有 struct literal;
- 让
allows_network_approval_flow在 Granular(false) 时返回 false; - 更新
granular_instructions及 true/false 分类测试,让模型看到相同能力边界; - 添加 spy,证明 false 路径没有调用 permission hook、Guardian 或 user approval sender;
- 预置一个
session_approved_hosts条目,证明它仍按既有边界允许; - 序列化旧
TurnContextItemfixture 并 resume,证明缺字段恢复为 true; - 重生成 config/app-server schema,跑完本章列出的 11 条基线和新增测试。
这条纵切已经包含发布前必需的 prompt 合同,但不增加 TUI 子字段编辑入口。后者可以等核心语义稳定后再决定。若第一步就同时做菜单、CLI 和新 request type,失败时很难判断是安全语义错了,还是某个投影漏字段。
这一章建立了什么
这个提案最终锁定五个决定:旧载荷缺字段时默认为 true;false 在 hook、Guardian 和用户审批之前直接拒绝;当前 session 已批准的 host 与已落盘 policy amendment 不被撤销;core、app-server、rollout resume 与生成 schema 必须保留同一个值;模型 prompt 也要暴露同一类别。少掉任何一项,network_approval 就会在重启、跨协议或真实执行时变成另一种策略。