阅读契约。跟随一条测试命令,区分策略结果、审批决定、执行环境和失败重试。 本文核实于 2026-09-20,依据公开源码 固定公开源码快照;不推断未公开的服务实现。
先从一个很普通的动作开始:模型想跑测试。它返回了一个 shell tool call,命令看起来也没问题。 但真正执行之前,Codex 还要知道这轮的工作目录是什么、当前允许读写哪些路径、网络是否受限、 用户配置的是遇到风险再问,还是永远不问,以及有没有 hook 想在工具执行前改写或拦住这次请求。
这些判断合起来,就是这一篇要讲的“权限”。这里的权限不是 UI 上的一个按钮,也不是 prompt
里一句“请谨慎执行”。它是一条运行时路径:turn 带入策略,registry 先跑 hook,
orchestrator 做审批和 sandbox 选择,具体 runtime 只在某个 SandboxAttempt
里执行动作。
本文里的“副作用”指会碰外部环境的动作:启动进程、写文件、扩大可读写范围、访问网络,
或者把某个失败的 sandboxed attempt 升级为更宽的执行方式。读权限代码时,先问这个动作会不会产生副作用,
再看它是否进入 ToolOrchestrator。
取材范围。
本文只描述 openai/codex 公开源码里能看到的策略字段、hook 调用、审批事件、sandbox 选择和 retry 逻辑。
关键判断分别对应源码里的 AskForApproval、
PermissionProfile、ExecApprovalRequirement、
ToolOrchestrator 和 SandboxAttempt 等结构。
不推断私有 guardian 模型的内部判断,也不推断操作系统 sandbox 的未公开实现细节。
这一篇按六个问题往下拆:
- 一次 turn 从入口带进来哪些权限和 sandbox 参数?
- 这些参数进入运行时后,会落到哪些对象上?
- 工具真正执行前,pre/post hooks 和 permission hooks 分别在什么位置?
ToolOrchestrator怎样决定跳过、拒绝,还是请求审批?- 初次 sandbox attempt 和失败后的 retry 是怎样选择的?
- 读源码时怎样区分“模型请求了动作”和“运行时允许动作发生”?
一、turn 启动时就确定权限参数
Codex 并不是等到 shell handler 运行时才检查权限。app-server v2 的
TurnStartParams
里已经能看到入口参数:approval_policy 可以覆盖本轮审批策略,
sandbox_policy 可以覆盖本轮 sandbox,permissions 可以选择一个命名权限 profile,
而且注释明确写着 permissions 不能和 sandboxPolicy 同时使用。
这一步很重要。权限从这里就进入 turn runtime,和本轮的工作目录、workspace roots、 环境选择、模型参数放在同一层配置里;后面的工具会读取这些明确的配置值。
TurnContext 保留 turn 的配置入口;真正执行当前请求时,orchestrator 从 StepContext.settings 读取审批策略,从工具请求选定的 TurnEnvironment 读取权限 profile、workspace roots 和 sandbox 配置。这样已排队请求使用自己所属步骤的环境,而不是下一步修改后的设置。
| 入口字段 | 运行时对象 | 读源码时的含义 |
|---|---|---|
approval_policy |
AskForApproval |
决定默认什么时候请求审批,什么时候直接返回失败。 |
sandbox_policy |
SandboxPolicy |
旧的 sandbox 形状:full access、read-only、workspace-write 等。 |
permissions |
PermissionProfile |
更细的权限 profile,会投影成文件系统和网络运行时策略。 |
runtime_workspace_roots |
workspace roots | 决定 workspace-write、额外可写根等策略的解释范围。 |
权限更新会进入 session 的配置状态,供后续步骤捕获;权限 profile 与 legacy sandbox policy 是两种输入形状,最终都要得到文件系统和网络策略。读具体命令时,应继续追到 tool.turn_environment(req),确认权限应用于哪一个执行环境。
二、approval、permissions、sandbox 和 hooks 各管什么
读到这里,容易把 approval、permissions、sandbox、hooks 全混在一起。它们都在限制工具能做什么, 但源码里的位置不同。
| 词 | 对应源码对象 | 负责的问题 | 不负责的问题 |
|---|---|---|---|
| 审批策略 | AskForApproval |
这次动作要不要向用户、guardian 或 hook 要一个决定。 | 具体怎样隔离文件系统和网络。 |
| 权限 profile | PermissionProfile |
当前 turn 允许哪些文件系统、网络和额外权限形状。 | 模型是否会选择某个工具。 |
| 执行审批要求 | ExecApprovalRequirement |
某一次 tool request 是 skip、needs approval,还是 forbidden。 | 工具结果怎样回到模型。 |
| sandbox attempt | SandboxAttempt |
一次实际执行使用的 sandbox 类型、权限 profile、cwd、workspace roots 和网络限制。 | 审批 UI 怎么展示。 |
| hook | pre/post/permission hooks | 在工具执行前后或审批前插入外部规则。 | 替代 orchestrator 的 sandbox 选择。 |
对照源码看会更清楚。
AskForApproval
包含 UnlessTrusted、OnRequest、Granular 和
Never。其中 GranularApprovalConfig 还把 sandbox approval、execpolicy rules、
skill approval、request_permissions 和 MCP elicitation 分开控制。
旁边的
SandboxPolicy
则描述执行限制:danger-full-access、read-only、external-sandbox、
workspace-write,以及各自的网络和可写目录配置。审批策略回答“要不要问”,sandbox policy
回答“在什么限制下跑”。
配置兼容性还需单独看:当前枚举没有独立的 OnFailure,但 OnRequest 保留 on-failure 的 serde alias。旧字符串仍可解析,不过不能据此推断仍有单独的“失败后审批”运行模式。
三、registry 先给工具一次前置检查机会
第四篇已经讲过 ToolRegistry 是工具派发壳。放到权限主题里,它的第一个作用是:
在 handler 真正执行前,先跑 pre-tool-use hooks。
这段逻辑
会从工具拿 pre_tool_use_payload,调用 run_pre_tool_use_hooks。
hook 可以直接阻止这次工具调用,也可以返回 updated input,让 registry 用改写后的 invocation 继续往下走。
这不是审批弹窗。它发生在 handler 之前,更像一个组织或扩展层面的“工具入口规则”: 有些命令可以在这里被拒绝,有些输入可以在这里被规范化。真正的用户审批和 sandbox selection 还在后面的 orchestrator。
handler 成功后,registry 还有 post-tool-use hooks。 post hook 可以追加上下文,也可以用 feedback message 替换模型可见输出。也就是说,hook 不只管“能不能执行”, 还可能影响模型下一步看到什么。
这一层要和后面的 permission request hooks 区分开:pre/post hooks 包住工具 handler; permission request hooks 则包住审批请求,在 guardian 或用户审批之前抢先给出 allow / deny。
四、有副作用的工具进入 orchestrator
以执行命令为例,exec_command 进入 unified-exec 管理逻辑,构造 UnifiedExecRequest,由 ToolOrchestrator 驱动 UnifiedExecRuntime;apply_patch 则使用自己的 request 和 runtime。二者共用审批、sandbox 选择和 retry 的控制流程。
这也补全了第四篇的执行路径。模型发出的只是 call;有副作用的 call 会被翻译成 runtime request,
然后交给统一的 orchestrator。审批、sandbox 选择和失败重试这三个决定集中在
orchestrator.rs 文件头
已经写明的三件事:approval、sandbox selection、retry semantics。
工具 runtime 通过 Approvable 提供 sandbox permissions、自定义审批要求和 approval_action,通过 Sandboxable 声明 sandbox 偏好与是否允许失败升级,最后实现 ToolRuntime::run。审批 action 描述待审查动作,session 负责选择 hook、Guardian 或用户处理它。
继续固定一条请求:模型想在当前 workspace 里跑 npm test。进入 orchestrator 之后,
读源码可以按下面这条 record 追,而不是把审批、sandbox 和 retry 混成一个概念:
{
"tool": "exec_command",
"input": {"cmd": "npm test", "cwd": "workspace"},
"approval_policy": "on-request",
"sandbox_attempt": {
"sandbox_requested": true,
"file_system": "workspace-write",
"permission_profile": "project-write",
"workspace_roots": ["workspace"]
},
"retry_reason": "sandbox_denied"
}
这条记录能把几个决定拆开:approval policy 决定先问不问,sandbox attempt 决定第一次在哪个权限 profile 下跑,tool runtime 负责把 attempt 变成实际执行环境; 失败如果像 sandbox denial,orchestrator 才按策略走升级或重试。
五、第一个决定:这次需要审批吗
ToolOrchestrator::run 先取请求环境的权限 profile,再读取工具自定义 exec_approval_requirement(req);没有自定义时使用 默认规则:Never 返回 Skip,OnRequest 与 Granular 在文件系统受限时需要审批,UnlessTrusted 需要审批。在本来需要 sandbox 审批时,Granular 禁止该类审批才会返回 Forbidden;文件系统不受限时默认仍为 Skip。
这只是策略阶段的结果。Skip 不能无条件读成“绝不审核”。 strict auto-review 分支 即使看到 Skip,也会构造 ApprovalAction 与 ApprovalContext,调用 Session::request_approval。普通 Skip 才直接继续;Forbidden 返回拒绝,NeedsApproval 进入相同的统一审批入口。
policy requirement: Skip / Forbidden / NeedsApproval
Skip + strict_auto_review -> request_approval(action, context)
Skip + ordinary mode -> select sandbox
Forbidden -> reject
NeedsApproval -> request_approval(action, context)
六、审批请求前,permission hooks 可以先回答
审批现在由 Session::request_approval 统一处理。ApprovalAction 区分命令、stdin 写入、patch、MCP 调用、网络和权限请求;它把具体动作转换成 permission_request_payload。先运行 PermissionRequest hooks:Allow 批准,Deny 拒绝,没有 verdict 时再进入 Guardian 或用户审批。
命令 action 生成 bash hook payload,patch action 生成 apply_patch payload;重试时 hook run id 带 :retry。这个入口与 registry 的 PreToolUse 不同:前者回答一次审批,后者在 handler 前检查或改写输入。审批通过仍然只允许接下来的尝试,不会取消请求环境的 sandbox 限制。
七、审批完成后选择初次 sandbox attempt
审批阶段放行后,orchestrator 根据权限 profile、override 与 managed network 决定是否需要 sandbox。对本地执行,SandboxManager::select_initial 选择平台实现;对执行器自行管理进程 sandbox 的环境,本地 SandboxType::None 可以与 sandbox_requested = true 同时出现。此时限制交给执行器落实,不能把本地 None 解读为权限完全开放。
SandboxAttempt 携带 sandbox_requested、权限、执行器权限、cwd、workspace roots 和网络状态;env_for 与 env_for_exec_server 分别服务对应执行路径。UnifiedExecRuntime 用这些字段准备实际命令执行,patch runtime 则准备文件系统操作。
shell runtime 的 run 里就能看到这一步的落点:
它从 attempt 取 runtime permissions,构造 sandbox command,再调用 attempt.env_for(...) 执行。
apply_patch runtime 也会从 attempt 生成文件系统 sandbox context;如果失败看起来像 sandbox denied,
会返回 SandboxErr::Denied。
所以 sandbox 不是“工具执行失败后才补上的安全网”。它是每一次 attempt 的执行环境。
工具 runtime 拿到的是 SandboxAttempt,再把自己的命令或 patch 放进这个 attempt。
八、失败后能不能 retry,要再过一遍规则
如果初次 attempt 成功,orchestrator 直接返回 output。真正复杂的是 sandbox denied。
retry 分支
会先看 denial 里有没有网络策略信息,再看工具是否允许 escalate_on_failure(),
当前文件系统策略是否允许无 sandbox 执行,以及 approval policy 是否允许为了 no-sandbox retry 再问一次。
这段逻辑里有几个细节很能体现 Codex 的谨慎:
如果 denial 带有网络策略,但无法构造 network approval context,就直接返回 denied;
如果工具不允许失败升级,也直接返回 denied;
如果文件系统策略里有 denied-read restrictions,unsandboxed_execution_allowed 会阻止直接去掉 sandbox,
因为那些 denied reads 只有在 sandbox 里才能被执行。
需要 retry 时,orchestrator 构造网络拒绝或 sandbox 拒绝原因,并可能再次请求审批。strict_auto_review 下,初次审批只覆盖当时的 sandboxed attempt,不能靠缓存直接跳过更宽重试的审核。没有 denied-read 限制时才可能取消文件系统 sandbox;执行器管理隔离的路径仍分别传递 sandbox 意图和权限。
sandbox denied 后:
denial output + network policy
↓
tool 是否允许升级
↓
当前策略是否允许 no-sandbox 或网络审批
↓
可能再次 request_approval(call_id:retry)
↓
retry attempt 或把 denied 返回给模型
九、审批事件怎样交给客户端
审批向客户端发送结构化事件:ExecApprovalRequestEvent 描述命令、cwd、原因、网络上下文、额外权限和可选 decision;ApplyPatchApprovalRequestEvent 描述文件修改与 grant root。统一审批模块 将 action 路由给 reviewer,用户路径再调用 session 的命令或 patch 审批接口,最终收回 ReviewDecision。
因此要分清三个时刻:请求已经发给 reviewer,reviewer 已批准尝试,以及工具实际完成。审批事件或 Approved decision 都不能代替工具退出状态与结果记录;持续运行进程还可能在之后写 stdin 或访问网络时触发新的审批。
十、读这段执行流程时,带走三条规则
| 看到现象 | 源码里先问 | 关键位置 |
|---|---|---|
| 模型请求执行命令。 | handler 是否把它翻成有副作用 runtime request? | unified_exec/apply_patch handler 调用 ToolOrchestrator。 |
| 工具被问审批。 | 审批来自默认 policy,还是工具自定义 requirement? | exec_approval_requirement 与 default_exec_approval_requirement。 |
| 审批没有弹给用户。 | 是不是 permission request hook 已经 allow / deny? | request_approval 里的 run_permission_request_hooks。 |
| 命令进了 sandbox。 | 本次 attempt 的 sandbox、permissions、cwd 和 network 是什么? | SandboxAttempt。 |
| sandbox denied 后重试。 | 它是网络审批、no-sandbox retry,还是直接把 denied 返回? | ToolOrchestrator::run retry 分支。 |
到这里,权限和 sandbox 的主线可以连起来了:turn 入口带入审批和权限策略;
TurnContext 保留 turn 配置,StepContext 捕获当前步骤的设置,选定的 TurnEnvironment 提供执行权限;registry 在工具入口前后提供 hook;
有副作用的 handler 把请求交给 ToolOrchestrator;
orchestrator 先判定审批,再选择 sandbox attempt,失败后按 policy 决定能不能 retry。
这也是读 Codex agent 时最容易错过的一点:模型提出动作,只是流程开始。 真正决定动作能不能碰系统的,是这条运行时路径。下一篇把这条路径落到 Windows:用户身份、ACL、受限 token 与网络规则怎样让操作系统执行这些限制。
参考源码
- openai/codex · 5c5308fc9a9e
- TurnStartParams
- StepContext
- TurnEnvironment / permissions
- ApprovalAction / Session::request_approval
- hook / Guardian / user routing
- ToolOrchestrator
- default requirements / denied reads
- Approvable / SandboxAttempt
- UnifiedExecRequest / runtime
- ApplyPatch runtime
- PreToolUse / PostToolUse