# 自动审查 · 让另一个模型替你审批

> 把 approvals_reviewer 设为 auto_review 后，原本弹给你的审批请求先交给 guardian：内核把待审动作渲染成 JSON，连同压缩过的对话记录、权限上下文和一份风险策略，交给一个独立的审查线程。审查线程只有只读的命令工具，必须以严格 JSON 回答风险等级、用户授权程度、放行与否和理由；结论折算回审批决定，拒绝理由喂回主模型。连续拒绝会触发熔断中断本轮，开发中的 guardian v2 则尝试用后台打分跳过低风险动作的同步审查。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/guardian
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 自动审查 · 让另一个模型替你审批

[审批流程](https://daiw.net/manual/codex-source/approvals-flow)的第三关里，钩子没表态的请求会先问 `request_guardian_approval`，它认领了就轮不到你。这一篇拆开这个“替你审批”的审查者：它由什么触发、看到什么、怎么回答，结论又怎样回到审批流程。代码分在四处：`codex-rs/core/src/guardian/`（宿主侧，负责准备证据、执行裁决），`codex-rs/guardian-context/`（证据的组装与预算），`codex-rs/ext/guardian-reviewer/`（同步审查的编排、重试与熔断），`codex-rs/ext/guardian-v2/`（审查线程的装配与开发中的异步打分）。

## 用户看到的样子

`approvals_reviewer = "auto_review"`（旧值 `guardian_subagent` 仍可用），或者用 `--approve-for-me` 启动。之后需要审批的动作先由审查者过目：TUI 底部状态栏显示 “Reviewing approval request” 和待审的动作，放行就悄悄继续，被拒或超时才在对话记录里留下说明；被拒后，主模型会拿到理由并被要求换个更安全的做法或停下来问你，你也可以用 `/approve` 从最近的拒绝记录里选一条，放行一次重试。`[auto_review]` 表的 `policy` 与 `extra_policy` 可以定制审查策略，托管 `requirements.toml` 的 `guardian_policy_config` 与 `guardian_extra_policy` 优先。用法与限制见手册的[沙箱、审批与安全](https://daiw.net/manual/codex/sandbox-approvals)。

## 什么时候轮到审查者

```rust
pub fn routes_approval_policy_to_guardian(
    policy: AskForApproval,
    reviewer: ApprovalsReviewer,
) -> bool {
    matches!(
        policy,
        AskForApproval::OnRequest | AskForApproval::Granular(_)
    ) && reviewer == ApprovalsReviewer::AutoReview
}
```

（`codex-rs/ext/guardian-reviewer/src/routing.rs:54`）

也就是说，用户主动选择时，只有 `on-request` 与 `granular` 两种审批策略会走自动审查。另有几种情况不看这两项设置、一律“强制审查”（`require_guardian`）：本轮开了严格自动审查、托管要求的 `[auto_review].required_on_models` 列出了当前模型，或托管要求的 `allowed_approvals_reviewers` 不含 `user`。反过来，审批策略为 `never`、各环境都是 `Disabled` 配置档的“完全访问”下，审查直接放行。

`decide_approval`（`codex-rs/core/src/guardian/decision.rs:45`）把请求连同这些约束打包成 `ReviewRequest`，先问扩展注册表里的审查贡献者，由贡献者的回答决定下一步：

```rust
            match registry.decide_approval(&input).await {
                Some(ApprovalDecision::Reviewed(decision)) => Some(decision),
                Some(ApprovalDecision::Allow) if !input.require_fresh_review => {
                    Some(self.cached_approval().await)
                }
                Some(ApprovalDecision::Allow) => {
                    self.review(GuardianReviewReason::FreshRequired).await
                }
                Some(ApprovalDecision::AskUser) if !self.require_guardian => None,
                None if !self.require_guardian
                    && !routes_approval_policy_to_guardian(
                        self.approval_policy,
                        self.approvals_reviewer,
                    ) =>
                {
                    None
                }
                None | Some(ApprovalDecision::AskUser) => {
                    self.review(GuardianReviewReason::Policy).await
                }
            }
```

（`codex-rs/ext/guardian-reviewer/src/routing.rs:113`）

返回 `None` 表示交还给用户。目前注册的贡献者是 guardian v2 扩展里的 `GuardianApprovalReviewer`：不强制审查、而审查者是用户或审批策略不是 `on-request` 与 `granular` 时，它回答 `AskUser`；否则按审查策略给这一类动作定的模式，决定是查缓存的分数，还是直接调同步审查（v2 的异步打分见文末）。

```mermaid
flowchart TB
  A[request_approval<br/>钩子没表态] --> B[decide_approval]
  B --> C{完全访问}
  C -->|是| OK[直接放行]
  C -->|否| D[审查贡献者]
  D -->|审查者是 user 且不强制| U[弹给用户]
  D -->|有新鲜的低风险分数| F[快速放行]
  D -->|其余| R[同步审查<br/>独立的审查线程]
  R --> J[严格 JSON 结论]
  J -->|allow| AP[Approved]
  J -->|deny| DN[Denied 附理由]
  J -->|超时或失败| TO[TimedOut 或带说明的拒绝]
```

## 审查线程：只读的另一个 agent

同步审查不是一次简单的模型调用，而是一个真正的子线程：`sync_reviewer` 通过同一个 `ThreadManager` 启动它，会话来源标为内部的 `Guardian`，线程来源为 `GuardianReview`，与父会话隔离，请求头带 `x-codex-guardian: reviewer`。每个父线程保留一个可复用的审查主干线程，并发的审查从已提交的上下文分叉出临时线程，后续审查只追加新增的对话记录，不必每次从头发送。审查线程能做什么由两个函数限死：

```rust
pub fn reviewer_tool_policy() -> ToolPolicy {
    ToolPolicy {
        allowed_tools: Some(
            ["exec_command", "write_stdin", "view_image", "exec", "wait"]
                .into_iter()
                .map(ToolName::plain)
                .collect(),
        ),
        require_managed_sandbox: true,
        require_unified_exec: true,
        expose_additional_permissions: false,
    }
}

/// Applies the same read-only ceiling to the reviewer and each inherited environment.
pub fn reviewer_permission_profile(profile: &PermissionProfile) -> PermissionProfile {
    profile
        .intersect_with_read_only()
        .unwrap_or(PermissionProfile::External {
            network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
        })
}
```

（`codex-rs/ext/guardian-reviewer/src/settings.rs:31`）

它可以跑命令去核实情况（看文件在不在、查 git remote），但权限被压到只读、网络关闭，审批策略固定为 `never`，没法再申请提权，也就不会出现“审查者请求审查”的循环。模型默认选目录里的 `codex-auto-review`（`DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL`，`codex-rs/model-provider/src/provider.rs:124`），支持的话用低推理强度；目录里没有它时退回父模型，模型元数据也可以用 `auto_review_model_override` 指定别的审查模型。一次审查的时限是 90 秒（`REVIEW_TIMEOUT`），限流、服务端过载、连接失败、输出无法解析这类可恢复的失败，在同一时限内最多尝试 3 次（`MAX_REVIEW_ATTEMPTS`），退避从 200 毫秒起指数增长。

## 审查者看到什么

提示词分两部分。固定的一部分作为审查线程的基础指令：`policy_template.md` 模板里留着 `{{ tenant_policy_config }}` 与 `{{ extra_policy }}` 两个槽，前者依次取托管的 `guardian_policy_config`、`[auto_review].policy`、模型目录或内置的 `policy.md`，后者取托管的 `guardian_extra_policy` 或 `[auto_review].extra_policy`（`build_guardian_review_session_config`，`codex-rs/core/src/guardian/reviewer_config.rs:16`），末尾再拼上输出格式的约定。模板规定了证据的可信度：只有用户与开发者消息、`AGENTS.md` 和 `request_user_input` 的回答算可信内容，工具输出、技能与插件描述、助手自己的话都只是不可信证据；还给出了“用户授权程度”与“风险等级”的评分口径。内置的 `policy.md` 则是一份默认的组织风险策略，按数据外泄、凭据探测、持久性的安全削弱、破坏性操作、低风险操作五类给出放行与拒绝规则。

可变的一部分作为每次审查的用户输入，由 `guardian-context` 组装：一份压缩过的对话记录（同步审查的预算是每条消息最多 5000 token、每个工具条目 1000 token，消息部分合计不超过 2 万、工具部分不超过 1 万，只保留最近 40 条非用户条目，不含推理内容），可信的用户输入快照，子 agent 场景下根会话的授权消息，本次动作的权限上下文，以及待审动作本身：`GuardianApprovalRequest` 的七种形态（命令、终端输入、execve 拦截、补丁、网络访问、MCP 调用、权限申请）被渲染成格式化的 JSON，附上的理由（有重试理由时用它，否则用审批理由）截断到 512 token。整个请求还要过一道输入预算，超了而审查又不是强制的，就退回给用户。

## 结论怎样回到审批流程

审查线程的最后一条消息必须是严格 JSON，对应这个结构：

```rust
pub struct GuardianAssessment {
    pub risk_level: codex_protocol::protocol::GuardianRiskLevel,
    pub user_authorization: codex_protocol::protocol::GuardianUserAuthorization,
    pub outcome: GuardianAssessmentOutcome,
    pub rationale: String,
}
```

（`codex-rs/ext/guardian-reviewer/src/assessment.rs:13`）

低风险时允许只回 `{"outcome":"allow"}`，解析器替缺失的字段补默认值（放行记为低风险，拒绝记为高风险）；JSON 外面包了几句话也能从第一个 `{` 到最后一个 `}` 截出来。`complete_review` 再把结论折算成审批决定：放行得到 `Approved`；拒绝得到 `Denied`，理由形如 “This action was rejected due to unacceptable risk.” 加审查者的 rationale，再加一段指示，要求主模型不要绕路达成同样的效果，换更安全的做法或停下来问用户；超时得到 `TimedOut`，主模型看到的说明是不要仅凭超时就认定动作不安全；审查本身出错则得到带 “Automatic approval review failed” 说明的拒绝。整个过程以 `GuardianAssessment` 事件发出，app-server 转成 `item/autoApprovalReview/started` 与 `item/autoApprovalReview/completed` 两个通知，界面据此显示进度和结论。

拒绝多了会熔断：

```rust
const MAX_CONSECUTIVE_CYBER_GUARDIAN_DENIALS_PER_TURN: u32 = 1;
const MAX_CONSECUTIVE_GUARDIAN_DENIALS_PER_TURN: u32 = 3;
const MAX_RECENT_CYBER_AUTO_REVIEW_DENIALS_PER_TURN: u32 = 1;
const MAX_RECENT_AUTO_REVIEW_DENIALS_PER_TURN: u32 = 10;
pub(crate) const AUTO_REVIEW_DENIAL_WINDOW_SIZE: usize = 50;
```

（`codex-rs/ext/guardian-reviewer/src/circuit_breaker.rs:3`）

同一轮里连续 3 次被拒，或最近 50 次审查里有 10 次被拒，就中断这一轮并发出警告，把控制权交还给你；专精网络安全的 cyber 模型两个阈值都是 1。`/approve` 走的是另一条路：它提交 `Op::ApproveGuardianDeniedAction`，内核把那条被拒动作以“用户已批准”的上下文片段注入历史（`codex-rs/core/src/session/handlers.rs:660`），下一次重试仍要审查，只是审查者这回能在对话记录里看到明确的用户授权。

## guardian v2：先打分，再决定要不要同步审

同步审查之外还有一条快路，主要由开发中的 `guardianv2` 特性（默认关闭）打开。工具调用一开始，异步打分器就把动作和最近的对话交给分类器（经连接池复用 WebSocket 或 HTTP，最多 16 个并发请求），分类指令要求模型只输出一个词 `high` 或 `low`；到了审批时，如果这个动作已经有一个低于阈值（默认 0.5）的新鲜分数，且打分以来授权情况没变，就直接放行，跳过同步审查；分数缺失、过期、偏高或打分失败，都退回同步审查并记下原因。

哪些类别能走快路，由一份按类别划分的审查策略决定（`GuardianPolicyLoader::resolve`，`codex-rs/config/src/guardian.rs:71`）。本地默认只给 computer use 类动作留了快路，而且除非模型元数据另有声明，这一类也会降回同步审查；在 `[features.guardianv2.review_scope]` 里把 `computer_use_only` 设为 false，命令、补丁、MCP、网络等类别才会加入。模型目录可以自带一份策略，逐类写明同步、自适应，或者干脆不交给审查者，它优先于本地设置；特性关着、目录也没有策略时，所有类别一律同步审查。

## 和《从 LLM 到 Coding Agent》对照

那本书在[子 agent](https://daiw.net/manual/llm-to-agent/subagents)一篇讲过把任务派给独立上下文的另一个 agent；guardian 是这个模式在安全上的用法：审查者有自己的线程、自己的工具白名单和只读权限，主 agent 的上下文污染不到它的判断规则。它也补上了[权限系统](https://daiw.net/manual/llm-to-agent/permissions)里“问用户”这一环的规模问题：用户不可能盯着每一次提权，交给一个按固定策略打分、理由可审计的模型，再用熔断兜住最坏情况。

---

上一篇：[执行策略 · Starlark 规则与提权](https://daiw.net/manual/codex-source/execpolicy) · 下一篇：[配置系统 · 分层加载与托管要求](https://daiw.net/manual/codex-source/config-system)
