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

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

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

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

审批流程的第三关里,钩子没表态的请求会先问 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 优先。用法与限制见手册的沙箱、审批与安全。

什么时候轮到审查者

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,先问扩展注册表里的审查贡献者,由贡献者的回答决定下一步:

            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 的异步打分见文末)。

图表加载中…

审查线程:只读的另一个 agent

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

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,对应这个结构:

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 两个通知,界面据此显示进度和结论。

拒绝多了会熔断:

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一篇讲过把任务派给独立上下文的另一个 agent;guardian 是这个模式在安全上的用法:审查者有自己的线程、自己的工具白名单和只读权限,主 agent 的上下文污染不到它的判断规则。它也补上了权限系统里“问用户”这一环的规模问题:用户不可能盯着每一次提权,交给一个按固定策略打分、理由可审计的模型,再用熔断兜住最坏情况。


上一篇:执行策略 · Starlark 规则与提权 · 下一篇:配置系统 · 分层加载与托管要求

本页目录