ApprovalPolicy完整参考
Codex 的 approval policy 不直接回答“命令是否安全”,而是回答一个更窄的问题:当前动作需要升级授权时,是否允许产生审批,以及这个审批应交给谁。真正的命令规则、权限 profile 和 sandbox 强制仍在其他层完成。
rust-v0.150.0 的审批路径已经统一:工具或 exec policy 先产生结构化 requirement,所有允许出现的审批再进入 Permission Request Hook,Hook 没有决定时才交给 Guardian 或用户;最终 ReviewDecision 还要经过统一解释,才能成为工具继续执行、拒绝或终止 Turn 的结果。
本文承接权限审批沙箱三层模型与SandboxPolicy完整参考。前两篇解释权限和强制;本文专门解决四个问题:何时允许提示、提示属于哪一类、谁负责审查、决定能否复用。
这条链中,“policy 允许提示”和“reviewer 批准动作”是两次不同判断。Granular 可以在 reviewer 运行前直接禁止某类 prompt;reviewer 则只处理已经获准进入审批流程的动作。
1. 四种模式
AskForApproval 的公开序列化值是 untrusted、on-request、granular 和 never。旧 on-failure 仍作为 OnRequest 的反序列化 alias,但当前语义不再是“任何失败都询问”。
源码位置:codex-rs/protocol/src/protocol.rs :: AskForApproval
pub enum AskForApproval {
#[serde(rename = "untrusted")]
#[strum(serialize = "untrusted")]
UnlessTrusted,
#[serde(alias = "on-failure")]
OnRequest,
#[strum(serialize = "granular")]
Granular(GranularApprovalConfig),
Never,
}四种模式的基础含义是:
| 模式 | 允许提示的基本原则 | 非危险普通命令 | 需要升级时 |
|---|---|---|---|
UnlessTrusted | 未被显式 allow 的命令倾向询问 | 通常 Prompt | 可询问 |
OnRequest | restricted sandbox 内先依赖 sandbox | 通常直接受限运行 | sandbox override 时 Prompt |
Granular | 类似 OnRequest,但按类别开关 | 通常直接受限运行 | 类别关闭则 Forbidden |
Never | 不产生审批 | 依赖已有 sandbox/rule | 需要 prompt 时 Forbidden |
Never 不是全放行模式。危险命令或没有可执行 sandbox 边界时,启发式 fallback 会返回 Forbidden;普通命令则可以在既有 restricted sandbox 中直接运行。
2. Granular分类
Granular 将五种审批来源拆开控制。字段为 false 的含义是自动拒绝该类请求,而不是退回另一个 reviewer。
源码位置:codex-rs/protocol/src/protocol.rs :: GranularApprovalConfig
pub struct GranularApprovalConfig {
pub sandbox_approval: bool,
pub rules: bool,
#[serde(default)]
pub skill_approval: bool,
#[serde(default)]
pub request_permissions: bool,
pub mcp_elicitations: bool,
}
impl GranularApprovalConfig {
pub const fn allows_sandbox_approval(self) -> bool {
self.sandbox_approval
}
pub const fn allows_rules_approval(self) -> bool {
self.rules
}
pub const fn allows_skill_approval(self) -> bool {
self.skill_approval
}
pub const fn allows_request_permissions(self) -> bool {
self.request_permissions
}
pub const fn allows_mcp_elicitations(self) -> bool {
self.mcp_elicitations
}
}分类不是按工具名简单切分。例如 Unified Exec 的 prompt 可能来自显式 exec-policy Prompt rule,也可能来自 unmatched command 的 sandbox escalation。前者检查 rules,后者检查 sandbox_approval;两者同时存在时,rule prompt 优先。
源码位置:codex-rs/core/src/exec_policy.rs :: prompt_is_rejected_by_policy
pub(crate) fn prompt_is_rejected_by_policy(
approval_policy: AskForApproval,
prompt_is_rule: bool,
) -> Option<&'static str> {
match approval_policy {
AskForApproval::Never => Some(PROMPT_CONFLICT_REASON),
AskForApproval::OnRequest => None,
AskForApproval::UnlessTrusted => None,
AskForApproval::Granular(granular_config) => {
if prompt_is_rule {
if !granular_config.allows_rules_approval() {
Some(REJECT_RULES_APPROVAL_REASON)
} else {
None
}
} else if !granular_config.allows_sandbox_approval() {
Some(REJECT_SANDBOX_APPROVAL_REASON)
} else {
None
}
}
}
}因此 rules=false, sandbox_approval=true 会拒绝显式 Prompt rule,但仍允许普通 sandbox escalation;反过来则允许规则 prompt,却拒绝模型请求额外文件或无 sandbox 权限。
3. 命令Fallback
没有显式 rule 命中时,exec policy fallback 会结合危险命令、文件权限、sandbox override 请求和 approval mode 决定 Allow、Prompt 或 Forbidden。
源码位置:codex-rs/core/src/exec_policy.rs :: default_exec_policy_decision
if dangerous_command_match.is_some() || windows_managed_fs_restrictions_without_sandbox_backend {
return match approval_policy {
AskForApproval::Never => Decision::Forbidden,
AskForApproval::OnRequest
| AskForApproval::UnlessTrusted
| AskForApproval::Granular(_) => Decision::Prompt,
};
}
match approval_policy {
AskForApproval::Never => {
Decision::Allow
}
AskForApproval::UnlessTrusted => {
Decision::Prompt
}
AskForApproval::OnRequest => {
match file_system_sandbox_policy.kind {
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
Decision::Allow
}
FileSystemSandboxKind::Restricted => {
if sandbox_permissions.requests_sandbox_override() {
Decision::Prompt
} else {
Decision::Allow
}
}
}
}
AskForApproval::Granular(_) => match file_system_sandbox_policy.kind {
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
Decision::Allow
}
FileSystemSandboxKind::Restricted => {
if sandbox_permissions.requests_sandbox_override() {
Decision::Prompt
} else {
Decision::Allow
}
}
},
}这段逻辑解释了几个反直觉现象:
Never对普通命令返回 Allow,是因为后续依赖现有 sandbox;危险命令则 Forbidden。OnRequest在 restricted sandbox 中不为普通命令提示,只有请求 override 时才 Prompt。UnlessTrusted对未命中 allow rule 的普通命令也 Prompt。Granular先产生 Prompt,之后再由类别开关决定允许提示还是 Forbidden。
4. Requirement结果
exec policy 最终将 Decision 映射为 ExecApprovalRequirement。Prompt 若被当前 policy 禁止,会转换成 Forbidden;Allow 转成 Skip,但只有每个 command segment 都命中显式 allow rule,才设置 bypass_sandbox=true。
源码位置:codex-rs/core/src/exec_policy.rs :: ExecPolicyManager::exec_approval_requirement
Decision::Prompt => {
let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| {
is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt
});
match prompt_is_rejected_by_policy(approval_policy, prompt_is_rule) {
Some(reason) if prompt_is_rule => ExecApprovalRequirement::Forbidden {
reason: reason.to_string(),
},
Some(reason) => ExecApprovalRequirement::Forbidden {
reason: derive_rejected_prompt_reason(
reason,
dangerous_command_match_for_heuristics(
&evaluation,
Decision::Prompt,
command_origin,
),
),
},
None => ExecApprovalRequirement::NeedsApproval {
reason: derive_prompt_reason(command, &evaluation),
proposed_execpolicy_amendment: requested_amendment.or_else(|| {
if auto_amendment_allowed {
try_derive_execpolicy_amendment_for_prompt_rules(
&evaluation.matched_rules,
)
} else {
None
}
}),
},
}
}
Decision::Allow => ExecApprovalRequirement::Skip {
bypass_sandbox: commands.iter().all(|command| {
exec_policy
.matches_for_command_with_options(
command,
/*heuristics_fallback*/ None,
&match_options,
)
.iter()
.any(|rule_match| {
is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
})
}),
proposed_execpolicy_amendment: if auto_amendment_allowed {
try_derive_execpolicy_amendment_for_allow_rules(&evaluation.matched_rules)
} else {
None
},
},NeedsApproval 的 proposed amendment 只是交互选项,只有 reviewer 返回相应 amendment decision 且后续持久化成功时才会影响未来调用。
5. 集中审批顺序
所有 ApprovalAction 首先转换为 Permission Request Hook payload。Hook 返回 Allow 或 Deny 时不会继续调用 Guardian/用户;只有 Hook 返回 None,才选择 reviewer。
源码位置:codex-rs/core/src/tools/approvals.rs :: Session::request_approval
let resolution = match run_permission_request_hooks(
self,
ctx.review_context.turn(),
&permission_request_run_id,
action.permission_request_payload(),
)
.await
{
Some(PermissionRequestDecision::Allow) => ApprovalResolution {
decision: ReviewDecision::Approved,
source: ApprovalResolutionSource::Hook,
},
Some(PermissionRequestDecision::Deny { message }) => ApprovalResolution {
decision: ReviewDecision::denied(message),
source: ApprovalResolutionSource::Hook,
},
None => self.request_reviewer_approval(action, &ctx).await,
};retry 使用不同 permission_request_run_id:普通动作使用 call ID,retry 追加 :retry;network 与 execve 使用各自稳定 ID。这让 Hook 可以区分初次请求与升级重试。
6. Reviewer路由
reviewer 配置是独立的 ApprovalsReviewer::{User, AutoReview}。AutoReview 只有在 approval policy 为 OnRequest 或 Granular 时自动路由 Guardian;UnlessTrusted 默认仍面向用户。strict auto review 则不受这一普通路由限制,直接强制 Guardian。
源码位置:codex-rs/protocol/src/config_types.rs :: ApprovalsReviewer
pub enum ApprovalsReviewer {
#[default]
#[serde(rename = "user")]
User,
#[serde(rename = "auto_review", alias = "guardian_subagent")]
#[strum(serialize = "auto_review")]
AutoReview,
}源码位置:codex-rs/core/src/guardian/review.rs :: routes_approval_policy_to_guardian
pub(crate) fn routes_approval_policy_to_guardian(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
) -> bool {
matches!(
approval_policy,
AskForApproval::OnRequest | AskForApproval::Granular(_)
) && approvals_reviewer == ApprovalsReviewer::AutoReview
}源码位置:codex-rs/core/src/tools/approvals.rs :: Session::request_reviewer_approval
let reviewer = if ctx.strict_auto_review {
ApprovalReviewer::Guardian
} else if let ApprovalAction::McpToolCall {
approval_policy,
reviewer,
..
} = &action
{
ApprovalReviewer::for_policy(*approval_policy, *reviewer)
} else {
ApprovalReviewer::for_turn(ctx.review_context.turn())
};
let decision = match reviewer {
ApprovalReviewer::Guardian => self.request_guardian_approval(action, ctx).await,
ApprovalReviewer::User => self.request_user_approval(&action, ctx).await,
};MCP action 可以携带自己的 approval policy 与 reviewer,避免把某个 connector 的配置错误套用为整个 Turn 的 reviewer 选择。
7. 决定解释
reviewer 返回的并不只有 Approved/Denied。统一 resolution 层处理 timeout、Turn abort、network policy amendment 和 MCP policy amendment,且部分 special decision 只对对应 action 合法。
源码位置:codex-rs/core/src/tools/approvals.rs :: ApprovalResolution::into_tool_result
match self.decision {
ReviewDecision::ApprovedMcpPolicyAmendment => {
error!("Tool approval received ApprovedMcpPolicyAmendment");
Err(ToolError::Rejected(
"Error while requesting approval".to_string(),
))
}
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} if network_policy_amendment.action == NetworkPolicyRuleAction::Deny => {
let rejection = match source {
ApprovalResolutionSource::Hook => "rejected by configuration",
ApprovalResolutionSource::Guardian => {
"automatic approval review denied the action"
}
ApprovalResolutionSource::User => "rejected by user",
};
Err(ToolError::Rejected(rejection.to_string()))
}
ReviewDecision::Denied { rejection } => Err(ToolError::Rejected(rejection)),
ReviewDecision::TimedOut => {
Err(ToolError::Rejected(guardian_timeout_message(model_info)))
}
ReviewDecision::Abort => Err(ToolError::Codex(CodexErr::TurnAborted)),
decision => Ok(decision),
}Abort 与 Denied 不同:前者终止当前 Turn,后者只是拒绝工具动作。网络 Guardian 的 Abort 又在 request_approval 中被转换为普通 rejection,避免取消自动 reviewer 意外中止主 Turn。
8. 会话缓存
通用 approval cache 只缓存 ApprovedForSession,且 key 必须完整匹配。Unified Exec key 包含 environment ID、可执行文件、canonical command、cwd、TTY、sandbox permission 和 additional permissions;Apply Patch 则按 environment 与每个 path 分别建 key。
源码位置:codex-rs/core/src/tools/approvals.rs :: ApprovalAction::cache_keys
Self::ExecCommand {
environment_id,
command,
cwd,
tty,
sandbox_permissions,
additional_permissions,
..
} => vec![ApprovalCacheKey::ExecCommand(UnifiedExecApprovalKey {
environment_id: environment_id.clone(),
executable: command.first().cloned(),
command: canonicalize_command_for_approval(command),
cwd: cwd.clone(),
tty: *tty,
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
})],源码位置:codex-rs/core/src/tools/sandboxing.rs :: with_cached_approval
let already_approved = {
let store = services.tool_approvals.lock().await;
keys.iter()
.all(|key| matches!(store.get(key), Some(ReviewDecision::ApprovedForSession)))
};
if already_approved {
return ReviewDecision::ApprovedForSession;
}
let decision = fetch().await;
if matches!(decision, ReviewDecision::ApprovedForSession) {
let mut store = services.tool_approvals.lock().await;
for key in keys {
store.put(key, ReviewDecision::ApprovedForSession);
}
}环境、cwd 或权限请求变化都会造成 cache miss。NetworkAccess、MCP 与 RequestPermissions 不使用这个通用 key cache:network 有自己的 host/session policy,MCP 有 session/persistent approval 机制,不能把三者混成一种“批准过就永久放行”。
9. 重试审批
Orchestrator 的 already_approved 只是当前工具调用内的局部变量。sandbox denial 后,非 strict、非 network retry 可以由 should_bypass_approval 避免重复询问;strict auto review 必须再次 Guardian 审查,network retry 也不能复用普通 approval。
源码位置:codex-rs/core/src/tools/orchestrator.rs :: retry approval selection
let bypass_retry_approval = !strict_auto_review
&& tool.should_bypass_approval(approval_policy, already_approved)
&& network_approval_context.is_none();
if !bypass_retry_approval {
let approval_reason = match &requirement {
ExecApprovalRequirement::NeedsApproval { reason, .. } => reason.clone(),
ExecApprovalRequirement::Skip { .. }
| ExecApprovalRequirement::Forbidden { .. } => None,
};
let action = tool
.approval_action(req, &tool_ctx.call_id)
.map_err(|err| {
ToolError::Rejected(format!("could not prepare approval action: {err}"))
})?;
let approval_ctx = ApprovalContext {
review_context: GuardianReviewContext::from(&tool_ctx.step_context),
cancellation_token: Some(tool_ctx.cancellation_token.clone()),
call_id: tool_ctx.call_id.clone(),
tool_name: tool_ctx.tool_name.clone(),
strict_auto_review,
approval_reason,
retry_reason: Some(retry_reason),
network_approval_context: network_approval_context.clone(),
};
tool_ctx
.session
.request_approval(action, approval_ctx)
.await?;
}重试审批拥有明确 retry_reason,因此 reviewer 看到的是“初次 sandbox 为什么失败、现在要求什么变化”,而不是把初次批准静默扩大成更高权限。
10. 模式矩阵
| 场景 | UnlessTrusted | OnRequest | Granular | Never |
|---|---|---|---|---|
| restricted 普通命令 | Prompt | sandboxed Allow | sandboxed Allow | sandboxed Allow |
| dangerous command | Prompt | Prompt | 按类别 Prompt/Forbidden | Forbidden |
| exec-policy Prompt rule | Prompt | Prompt | rules 决定 | Forbidden |
| sandbox override | Prompt | Prompt | sandbox_approval 决定 | Forbidden |
| MCP elicitation | 允许进入流程 | 允许进入流程 | mcp_elicitations 决定 | 禁止 |
| AutoReview reviewer | 默认用户 | Guardian | Guardian | 默认用户 |
| strict auto review | Guardian | Guardian | Guardian | Guardian |
矩阵中的 “Allow” 表示无需审批,不表示无 sandbox。尤其 Never 的 restricted 普通命令仍依赖 sandbox;危险动作才会因无法提示而 Forbidden。
11. 测试反推边界
以下测试覆盖 prompt 分类、集中 resolution、reviewer 路由和会话缓存相关行为:
cd codex-rs
cargo test -p codex-core --lib exec_policy::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib tools::approvals::tests:: -- --nocapture --test-threads=1
cargo test -p codex-protocol --lib config_types::tests::approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent -- --nocapture --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --lib session::tests::guardian_tests::strict_auto_review_turn_grant_forces_guardian_for_exec_command_policy_skip -- --nocapture --test-threads=1
cargo test -p codex-core --lib codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_result -- --nocapture --test-threads=1五组目标共通过 92 项。strict Guardian 用例需要更大的测试线程栈;默认栈会在 approval-review 线程溢出,扩大栈后断言通过。这个运行条件不改变审批逻辑,只影响测试 harness 的线程栈容量。
mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision 同时制造 rule prompt 与 sandbox escalation,再关闭 granular rules。断言拒绝理由来自 rules 类别,证明 prompt 分类有优先级;它不验证 UI 文案。
approval_resolution_aborts_turn_when_approval_is_aborted 构造 Abort decision,断言转换成 CodexErr::TurnAborted;network/MCP special decision 测试则要求错误 action 类型不能被普通工具接受。这验证统一 resolution 层,不验证 reviewer 如何产生决定。
strict_auto_review_turn_grant_forces_guardian_for_exec_command_policy_skip 输入原本可 Skip 的命令与 strict grant,断言仍启动 Guardian,且用户审批通道没有接管。它证明 strict mode 覆盖普通 reviewer routing,但不证明 Guardian 一定批准。
测试能够证明策略分支、决定解释和 reviewer 选择,不能证明用户实际理解提示,也不能把 Guardian 风险判断当成 OS 强制。审批之后仍需继续检查 PermissionProfile 与 sandbox attempt。
调试时先辨认失败发生在哪一步:出现 “policy is Never” 或 granular category reason,说明 prompt 根本未获准;出现 user/Guardian denial,说明进入了 reviewer;出现 TurnAborted,说明 decision 是 Abort;批准后仍有 sandbox denial,则问题已经进入权限与强制层,不应继续归因于 approval policy。
下一篇PermissionProfile解析将继续解释配置 profile、内置 profile、extends、requirements 约束与 environment override 如何生成每个 Turn 的 canonical 权限。
