审批流程 · 什么时候停下来问你

一次命令或补丁要不要问人,要过三道关:执行策略先判定“跳过、需要批准、禁止”,ToolOrchestrator 据此审批、选沙箱,并在沙箱拒绝后视策略请求提权重试,审批请求再按“钩子、自动审查、用户”的顺序分派。发给用户的请求经 app-server 变成 item/commandExecution/requestApproval,回复沿 Op::ExecApproval 唤醒挂起的 oneshot。默认的 on-request 下,命令失败不会自动弹“去掉沙箱重试”,提权由模型主动申请。

作者 David更新于 第 28 篇(共 57 篇)

审批流程 · 什么时候停下来问你

上一篇定义了两个值:配置档划出边界,审批策略决定越界前问不问。这一篇跟着一次工具调用,看这两个值在运行时怎样变成“弹窗还是不弹窗”。

用户看到的样子

TUI 的审批框只列出服务端允许的选项(请求里的 available_decisions)。普通命令默认两项:Yes, proceed 和 No, and tell Codex what to do differently,后者不只是拒绝,还会中断本轮,等你输入新的指示;有可建议的前缀规则时(模型给了 prefix_rule,或由启发式推出),中间再多一项 Yes, and don't ask again for commands that start with ...,选它会把规则写进 default.rules。补丁审批另有 Yes, and don't ask again for these files,网络审批的选项留到网络代理一篇。策略组合与 rules 的写法分别见手册的沙箱、审批与安全和命令规则。

第一关:先判定要不要问

工具在进编排器之前,先得到一个 ExecApprovalRequirement(codex-rs/core/src/tools/sandboxing.rs:153),三种结果:Skip(不问,附带“第一次就不套沙箱”的 bypass_sandbox 标志)、NeedsApproval(要问,附带理由和建议的规则前缀)、Forbidden(直接拒绝)。

来源管什么怎么判
执行策略(第 33 篇)命令规则命中取最严格的决定;没命中走启发式,见下
assess_patch_safety(codex-rs/core/src/safety.rs:67)补丁改动全落在可写路径、平台沙箱可用就自动批准,否则问;never 或关掉 sandbox_approval 时直接拒绝
default_exec_approval_requirement兜底受限文件系统下 on-request 与 granular 要问,never 不问

没命中规则的命令由 render_decision_for_unmatched_command_for_platform(codex-rs/core/src/exec_policy.rs:770)判定。在 on-request、受限文件系统下,它只看两件事:命令是否被识别为危险(危险就问,never 下改为禁止),以及模型有没有申请越出沙箱(sandbox_permissions 不是默认值就问)。其余命令直接放行,交给沙箱兜底。所以 on-request 的“由模型决定何时请求”是字面意思:模型要在工具参数里主动申请。权限说明片段里的模板这样教它:

- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.

(codex-rs/prompts/templates/permissions/approval_policy/on_request.md:28)

在 never,或关掉了 sandbox_approval 的 granular 下,这类申请在 exec_command 处理器里就被驳回(codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs:343),根本到不了审批环节。

第二关:ToolOrchestrator 的三步

ToolOrchestrator::run(codex-rs/core/src/tools/orchestrator.rs:122)按“审批、选沙箱、尝试、必要时提权重试”驱动任何实现了 ToolRuntime 的执行后端,目前是 unified exec 与 apply_patch 两个:

图表加载中…
  1. 审批:Forbidden 直接变成 ToolError::Rejected;NeedsApproval 调 request_approval;Skip 本来不问,但本轮开了“严格自动审查”时仍要过一遍审查(客户端回复本轮范围的 request_permissions 授权时带上 strictAutoReview,之后本轮每条命令都先审再跑)。
  2. 第一次尝试:规则明确放行了每一段命令(Skip 且 bypass_sandbox),或者模型申请了 require_escalated 并已获批,第一次就不套沙箱;否则由 SandboxManager 选本平台的沙箱。唯一的例外是读拒绝:策略里有 deny_read 时 unsandboxed_execution_allowed 为 false,无论怎样批准都在沙箱里跑。
  3. 失败后:只有 SandboxErr::Denied 才考虑重试。这个判断是启发式的:非零退出码,且输出里出现 “operation not permitted”“permission denied”“read-only file system”“sandbox”“landlock” 这类关键词,或者 Linux 上以 SIGSYS 退出(codex-rs/sandboxing/src/denial.rs)。能不能“去掉沙箱再来一次”,由工具的 wants_no_sandbox_approval 决定:
    /// Decide we can request an approval for no-sandbox execution.
    fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool {
        match policy {
            AskForApproval::UnlessTrusted => true,
            AskForApproval::Never => false,
            AskForApproval::OnRequest => false,
            AskForApproval::Granular(granular_config) => granular_config.sandbox_approval,
        }
    }

(codex-rs/core/src/tools/sandboxing.rs:330)

默认的 on-request 下,命令被沙箱拦住后不会弹“去掉沙箱重试”,编排器把带原始输出的拒绝直接交还模型(codex-rs/core/src/tools/orchestrator.rs:367 的注释就是这么写的),由模型按模板带上 require_escalated 重新申请。例外是 apply_patch:它覆盖了这个方法,on-request 下补丁写到沙箱外会问你。untrusted 与打开 sandbox_approval 的 granular 才会对普通命令弹重试审批,理由固定是 “command failed; retry without sandbox?”。编排器里还留着一条“网络被拦后请求批准再重试”的分支,但 0.158.0 里产生 SandboxErr::Denied 的各处都不填 network_policy_decision,它实际走不到;联网审批改在代理拦截的当下内联发起(见网络代理)。

第一次已经批准过、且没开严格审查时,重试不再重复询问(should_bypass_approval)。

第三关:谁来批

Session::request_approval(codex-rs/core/src/tools/approvals.rs:479)把一次审批分派出去,顺序写在注释里:

        // Approval precedence is:
        // 1. Hooks
        // 2. If StrictAutoReview || Guardian enabled, then Guardian. Else, user.
        let resolution = match run_permission_request_hooks(
            self,
            &ctx.review_context,
            &permission_request_run_id,
            action.permission_request_payload(),
        )
        .await
        {
            // ...
            Some(PermissionRequestDecision::Deny { message }) => ApprovalResolution {
                decision: ReviewDecision::denied(message),
                source: ApprovalResolutionSource::Hook,
            },
            None => self.request_reviewer_approval(action, &ctx).await,
        };

(codex-rs/core/src/tools/approvals.rs:505)

先问 PermissionRequest 钩子(见 Hooks),钩子没表态再交给审查者:request_guardian_approval 认领了就由另一个模型裁决(见自动审查),否则才弹给用户。结果统一折算成工具结果:Denied 变成带理由的 ToolError::Rejected,作为工具输出喂回模型,模型可以换个做法;Abort 变成 TurnAborted,本轮就此中断。用户选过 ApprovedForSession 的请求记在会话级的 ApprovalStore 里(with_cached_approval,codex-rs/core/src/tools/sandboxing.rs:71),键是序列化后的请求要素:命令会先规范化,再连同 cwd、tty、sandbox_permissions、额外权限和执行策略指纹一起算键;补丁按文件逐个记。下次同样的请求直接放行。

从内核到客户端再回来

图表加载中…

发给用户的那一支落在 request_command_approval:先在本轮的 TurnState 里登记一个 oneshot,再发事件,然后原地等:

        // Add the tx_approve callback to the map before sending the request.
        let (tx_approve, rx_approve) = oneshot::channel();
        let prev_entry = {
            let mut active = self.active_turn.lock().await;
            match active.as_mut() {
                Some(at) => {
                    let mut ts = at.turn_state.lock().await;
                    ts.insert_pending_approval(effective_approval_id.clone(), tx_approve)
                }
                None => None,
        // ...
        self.send_event(turn_context, event).await;
        rx_approve.await.unwrap_or(ReviewDecision::Abort)

(codex-rs/core/src/session/mod.rs:2813)

事件里带着命令、cwd、理由、建议的规则前缀和 available_decisions;缺省时由 ExecApprovalRequestEvent::default_available_decisions(codex-rs/protocol/src/approvals.rs:365)算出上面那几个选项。app-server 在 bespoke_event_handling.rs 里把它翻成服务端请求 item/commandExecution/requestApproval,客户端回复的 decision 有六种:accept、acceptForSession、acceptWithExecpolicyAmendment、applyNetworkPolicyAmendment、decline、cancel,由 on_command_execution_request_approval_response(codex-rs/app-server/src/bespoke_event_handling.rs:1960)映射回 ReviewDecision:decline 变成理由为 “rejected by user” 的 Denied,cancel 变成 Abort,回复解析失败或客户端报错一律按拒绝处理。随后提交 Op::ExecApproval。内核的 exec_approval 处理器(codex-rs/core/src/session/handlers.rs:174)先处理附带动作:ApprovedExecpolicyAmendment 会把前缀规则追加进 default.rules,失败只发一条警告;Abort 不走通知,而是直接 interrupt_task 中断本轮;反过来,待审的 oneshot 若在回复到达前被清掉,request_command_approval 也按 Abort 处理,被中断的轮次不会拿着一个假的拒绝接着跑。其余决定经 notify_approval 从 TurnState 取出 oneshot 发回。补丁审批走同样的套路,对应 item/fileChange/requestApproval 与 Op::PatchApproval;request_permissions 工具的请求是 item/permissions/requestApproval。旧的 v1 接口还保留着 execCommandApproval 与 applyPatchApproval 两个已弃用的请求。

往终端里打字也要审

后台终端可以接着 write_stdin。write_stdin_approval 特性(默认开启)在 codex-rs/core/src/unified_exec/stdin_approval.rs 里比较终端启动时的权限快照与当前策略:终端当初是提权启动的、带着额外授权,或者策略已经变了,往里写字就要重新审批,选项只有批准与中断。策略有读拒绝、而终端在沙箱外启动或沙箱与当前不一致时,写入直接被拒,提示新开终端:批准也没法给正在运行的进程补上读拒绝。

和《从 LLM 到 Coding Agent》对照

那本书的权限系统强调“拒绝要喂回模型”,Codex 正是这样处理 Denied:理由作为工具结果交还,本轮继续;而 Abort 对应中断,直接停下本轮等人。挂起等待的做法与 OpenCode 的权限系统如出一辙:那边用 Deferred.await 把工具停在 ctx.ask 上,这边用存进 TurnState 的 oneshot。区别在分派层:Codex 在用户之前多了钩子和审查模型两级,同一个审批请求可能根本不会出现在你面前。


上一篇:权限模型 · 沙箱策略与审批策略 · 下一篇:macOS 沙箱 · Seatbelt 策略生成

本页目录