阅读契约。跟随一条测试命令,区分策略结果、审批决定、执行环境和失败重试。 本文核实于 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 的未公开实现细节。

这一篇按六个问题往下拆:

  1. 一次 turn 从入口带进来哪些权限和 sandbox 参数?
  2. 这些参数进入运行时后,会落到哪些对象上?
  3. 工具真正执行前,pre/post hooks 和 permission hooks 分别在什么位置?
  4. ToolOrchestrator 怎样决定跳过、拒绝,还是请求审批?
  5. 初次 sandbox attempt 和失败后的 retry 是怎样选择的?
  6. 读源码时怎样区分“模型请求了动作”和“运行时允许动作发生”?

一、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 与网络规则怎样让操作系统执行这些限制。

参考源码