Skip to content

权限审批沙箱三层模型

沿 Unified Exec 调用链拆解审批解析、权限编译与本地或 executor 强制三层,解释批准为何不等于无沙箱执行。

基于rust-v0.150.0
CodexRustSecuritySandbox

权限审批沙箱三层模型 ​

“用户批准了命令”与“命令可以无约束运行”在 Codex 中不是同一句话。批准只解决授权问题;策略层还要把当前 environment、exec policy、permission profile 与单次附加权限编译成 SandboxAttempt;最后由本机 sandbox 或远程 exec-server 把这份 attempt 变成受限进程。

这三层分别拥有不同的数据和失败方式:

  1. 审批层产生 ReviewDecision,回答动作能否继续。
  2. 策略层产生 ExecApprovalRequirement、有效 PermissionProfile 和 SandboxAttempt。
  3. 强制层选择 Seatbelt、Linux sandbox、Windows token、exec-server sandbox 或外部 caller。

本文承接Codex威胁模型与ToolOrchestrator执行流程。前者回答保护什么、信任谁;本文只追踪一次 Unified Exec 调用如何跨过三层,并解释每次转换保持了哪些不变量。

图中的箭头不是“权限逐层增大”。审批可能拒绝,附加权限必须被归一化或取交集,denied-read 还会阻止无 sandbox 升级;每一步都可能收紧前一步的请求。

1. 三层对象 ​

先把三个最容易混淆的对象放在一起:

层核心对象输入输出
审批AskForApproval、ApprovalAction、ReviewDecision工具动作、理由、Hook、reviewer批准、拒绝、超时、中止或 policy amendment
策略ExecApprovalRequirement、PermissionProfile、SandboxAttemptenvironment、exec policy、附加权限、网络状态初次或重试 attempt
强制SandboxManager、FileSystemSandboxContext、exec-serverargv、cwd、有效权限、平台能力受限进程或执行错误

ReviewDecision::Approved 不携带 OS file descriptor,也不直接改变文件系统。SandboxAttempt 也不是已经运行的进程,它只是对一次执行的完整策略快照。只有最后的 transform、spawn 与 executor 才接触实际资源。

2. 审批策略输入 ​

AskForApproval 决定哪些类别允许进入交互或自动 reviewer,而不是简单的全局 allow/deny。Granular 可以分别控制 shell sandbox、exec-policy rule、skill、permission request 和 MCP elicitation。

源码位置:codex-rs/protocol/src/protocol.rs :: AskForApproval, GranularApprovalConfig

rust
pub enum AskForApproval {
    #[serde(rename = "untrusted")]
    #[strum(serialize = "untrusted")]
    UnlessTrusted,
    #[serde(alias = "on-failure")]
    OnRequest,
    #[strum(serialize = "granular")]
    Granular(GranularApprovalConfig),
    Never,
}

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,
}

Never 的含义是“不询问”,不是“全部允许”。如果规则或工具要求审批但 policy 禁止询问,结果应是拒绝;如果默认 requirement 不需要审批,命令仍可能带着 sandbox 执行。

策略层把审批需求压缩为三个结构化分支。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: ExecApprovalRequirement

rust
pub(crate) enum ExecApprovalRequirement {
    Skip {
        bypass_sandbox: bool,
        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
    },
    NeedsApproval {
        reason: Option<String>,
        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
    },
    Forbidden { reason: String },
}

Skip 只表示“不需要新的审批”。只有其中的 bypass_sandbox: true 才提出首轮绕过 sandbox 的策略意图,而且这个意图仍会受到 denied-read 与 environment owner policy 的限制。

3. 默认需求计算 ​

工具没有提供自定义 requirement 时,Core 根据 approval policy 与文件系统是否 restricted 计算默认值。OnRequest 和 Granular 只在 restricted filesystem 下要求 sandbox approval;UnlessTrusted 总是要求;Never 不询问。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: default_exec_approval_requirement

rust
let needs_approval = match policy {
    AskForApproval::Never => false,
    AskForApproval::OnRequest | AskForApproval::Granular(_) => {
        matches!(
            file_system_sandbox_policy.kind,
            FileSystemSandboxKind::Restricted
        )
    }
    AskForApproval::UnlessTrusted => true,
};

if needs_approval
    && matches!(
        policy,
        AskForApproval::Granular(granular_config)
            if !granular_config.allows_sandbox_approval()
    )
{
    ExecApprovalRequirement::Forbidden {
        reason: "approval policy disallowed sandbox approval prompt".to_string(),
    }
} else if needs_approval {
    ExecApprovalRequirement::NeedsApproval {
        reason: None,
        proposed_execpolicy_amendment: None,
    }
} else {
    ExecApprovalRequirement::Skip {
        bypass_sandbox: false,
        proposed_execpolicy_amendment: None,
    }
}

这里能看出两个常被误读的结果:

  • Never + restricted 得到 Skip { bypass_sandbox: false },即不提示但仍走受限 attempt。
  • Granular { sandbox_approval: false } + restricted 得到 Forbidden,而不是静默当成 Never。

默认 requirement 只是 fallback。Unified Exec 会先经过 exec policy 与命令启发式分析,返回更具体的 requirement。

4. ExecPolicy转换 ​

exec policy 的 Forbidden、Prompt、Allow 会映射到同一 ExecApprovalRequirement。Prompt 是否可以展示,还要结合 granular rules/sandbox category;Allow 只有在解析出的每个命令 segment 都命中显式 allow rule 时,才设置 bypass_sandbox: true。

源码位置:codex-rs/core/src/exec_policy.rs :: ExecPolicyManager::exec_approval_requirement

rust
match evaluation.decision {
    Decision::Forbidden => ExecApprovalRequirement::Forbidden {
        reason: derive_forbidden_reason(
            command,
            &evaluation,
            dangerous_command_match_for_heuristics(
                &evaluation,
                Decision::Forbidden,
                command_origin,
            ),
        ),
    },
    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
        },
    },
}

多 segment 命令的判断是 all,不是只看第一个命令。这样可以防止 allowed-command && unreviewed-command 借第一个 segment 的 allow rule 绕过 sandbox。

5. 集中审批 ​

NeedsApproval 不直接等同于用户弹窗。所有动作先进入 Session::request_approval:Permission Request Hook 优先,Hook 没有决定时,再按 strict auto review、MCP policy 或 Turn 配置选择 Guardian/用户。

源码位置:codex-rs/core/src/tools/approvals.rs :: Session::request_approval, request_reviewer_approval

rust
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,
};

源码位置:codex-rs/core/src/tools/approvals.rs :: Session::request_reviewer_approval

rust
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,
};

审批输出仍是授权决定。即使 decision 是 Approved,策略层也要继续计算是否需要 sandbox、使用哪份权限以及由谁强制。Guardian 也不会直接生成 Seatbelt profile。

6. 权限编译 ​

通过审批后,Orchestrator 从选定 environment 读取 canonical PermissionProfile。本地进程会把 :project_roots 物化为当前 workspace roots;executor-managed 路径保留符号化 root,让目标主机解析自己的路径。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run

rust
let workspace_roots = environment.workspace_roots();
let executor_managed_process_sandbox = tool.uses_executor_managed_process_sandbox(req);
let permission_profile = environment.permission_profile();
let permissions = if executor_managed_process_sandbox {
    permission_profile.clone()
} else {
    environment.permission_profile_with_workspace_roots()
};
let file_system_sandbox_policy = permissions.file_system_sandbox_policy();

工具可以请求 AdditionalPermissionProfile,但附加权限不是把任意 JSON 直接并入基础 profile。它先归一化不支持的模式、路径和空结构;当请求与 reviewer grant 分离时,还要取交集,只保留请求范围内且实际获批的权限。

源码位置:codex-rs/sandboxing/src/policy_transforms.rs :: intersect_permission_profiles

rust
let file_system = requested
    .file_system
    .map(|requested_file_system| {
        let granted_file_system = granted.file_system.unwrap_or_default();
        let requested_policy =
            FileSystemSandboxPolicy::restricted(requested_file_system.entries.clone());
        let requested_read_deny_matcher = ReadDenyMatcher::new(&requested_policy, cwd);
        let mut accepted_entries = Vec::new();
        for entry in granted_file_system.entries.iter().filter(|entry| {
            granted_file_system_entry_within_request(
                &requested_file_system,
                &requested_policy,
                requested_read_deny_matcher.as_ref(),
                entry,
                cwd,
            )
        }) {
            let entry = materialize_cwd_dependent_entry(entry, cwd);
            if !accepted_entries.contains(&entry) {
                accepted_entries.push(entry);
            }
        }
        let mut entries = accepted_entries.clone();
        let requested_retained_deny_entries = retain_constraining_deny_entries(
            &requested_file_system.entries,
            &accepted_entries,
            cwd,
            &mut entries,
        );
        let granted_retained_deny_entries = retain_constraining_deny_entries(
            &granted_file_system.entries,
            &accepted_entries,
            cwd,
            &mut entries,
        );
        FileSystemPermissions {
            glob_scan_max_depth: merge_glob_scan_max_depth(
                &requested_retained_deny_entries,
                requested_file_system.glob_scan_max_depth.map(usize::from),
                &granted_retained_deny_entries,
                granted_file_system.glob_scan_max_depth.map(usize::from),
            )
            .and_then(NonZeroUsize::new),
            entries,
        }
    })
    .filter(|file_system| !file_system.is_empty());
let network = match (requested.network, granted.network) {
    (
        Some(NetworkPermissions {
            enabled: Some(true),
        }),
        Some(NetworkPermissions {
            enabled: Some(true),
        }),
    ) => Some(NetworkPermissions {
        enabled: Some(true),
    }),
    _ => None,
};

AdditionalPermissionProfile {
    network,
    file_system,
}

这里的核心不变量是:grant 不能超出 request,requested 与 granted 两侧的约束 Deny 都不能因为只保留获批 path 而消失;网络也只有在请求和批准双方都明确 enabled 时才生效。

7. 首次Attempt ​

审批与有效权限准备完成后,策略层才决定第一轮是否请求 sandbox。sandbox_override_for_first_attempt 同时观察 requirement、工具的 sandbox permission 请求和 denied-read。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: sandbox_override_for_first_attempt

rust
if !unsandboxed_execution_allowed(file_system_sandbox_policy) {
    return SandboxOverride::NoOverride;
}

if matches!(
    exec_approval_requirement,
    ExecApprovalRequirement::Skip {
        bypass_sandbox: true,
        ..
    }
) {
    return SandboxOverride::BypassSandboxFirstAttempt;
}

if sandbox_permissions.requires_escalated_permissions() {
    SandboxOverride::BypassSandboxFirstAttempt
} else {
    SandboxOverride::NoOverride
}

存在 denied-read 时第一行直接返回 NoOverride。这意味着 exec-policy allow 和模型显式 require_escalated 都不能删除读取禁区。

Orchestrator 随后计算 sandbox_requested,再决定本地主机是否选择具体 wrapper。两者必须分开:远程 executor 路径可能 sandbox_requested=true,但 Core 的 initial_sandbox 是 None,因为 wrapper 应由 executor 创建。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: first attempt selection

rust
let sandbox_requested = match sandbox_override {
    SandboxOverride::BypassSandboxFirstAttempt => false,
    SandboxOverride::NoOverride => self.sandbox.should_sandbox(
        &permissions,
        sandbox_preference,
        managed_network_active,
    ),
};
let initial_sandbox = if sandbox_requested && !executor_managed_process_sandbox {
    self.sandbox.select_initial(
        &permissions,
        sandbox_preference,
        turn_ctx.windows_sandbox_level,
        managed_network_active,
    )
} else {
    SandboxType::None
};

最终 SandboxAttempt 同时保存已经物化的 permissions 与原始 exec_server_permissions,让本地和远程强制使用各自正确的表示。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt

rust
pub(crate) struct SandboxAttempt<'a> {
    pub sandbox: SandboxType,
    pub sandbox_requested: bool,
    pub permissions: &'a codex_protocol::models::PermissionProfile,
    pub exec_server_permissions: &'a codex_protocol::models::PermissionProfile,
    pub enforce_managed_network: bool,
    pub(crate) manager: &'a SandboxManager,
    pub(crate) sandbox_cwd: &'a PathUri,
    pub(crate) workspace_roots: &'a [PathUri],
    pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
    pub use_legacy_landlock: bool,
    pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
    pub windows_sandbox_private_desktop: bool,
    pub network_denial_cancellation_token: Option<CancellationToken>,
    pub(crate) network_proxy: Option<&'a NetworkProxy>,
}

8. 本地强制 ​

本地路径由 SandboxManager::should_sandbox 判断策略是否需要平台 sandbox,再由 select_initial 映射到当前 OS 的 concrete type。

源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::select_initial, should_sandbox

rust
pub fn select_initial(
    &self,
    permission_profile: &PermissionProfile,
    pref: SandboxablePreference,
    windows_sandbox_level: WindowsSandboxLevel,
    has_managed_network_requirements: bool,
) -> SandboxType {
    if self.should_sandbox(permission_profile, pref, has_managed_network_requirements) {
        get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
            .unwrap_or(SandboxType::None)
    } else {
        SandboxType::None
    }
}

pub fn should_sandbox(
    &self,
    permission_profile: &PermissionProfile,
    pref: SandboxablePreference,
    has_managed_network_requirements: bool,
) -> bool {
    match pref {
        SandboxablePreference::Forbid => false,
        SandboxablePreference::Require => true,
        SandboxablePreference::Auto => {
            let (file_system_policy, network_policy) =
                permission_profile.to_runtime_permissions();
            should_require_platform_sandbox(
                &file_system_policy,
                network_policy,
                has_managed_network_requirements,
            )
        }
    }
}

should_sandbox=true 说明策略需要强制;select_initial=None 只说明当前 host 没有选择 wrapper。调用方不能用 SandboxType::None 单独推导“策略没有请求 sandbox”。

SandboxManager::transform 才把 permission profile、cwd、网络上下文与 argv 变成 SandboxExecRequest。macOS 生成 Seatbelt wrapper,Linux 生成 sandbox helper 参数,Windows 生成 restricted/elevated launch;transform 失败会在 spawn 前终止。

9. Executor强制 ​

Unified Exec 在 remote environment 或 shell-snapshot 路径把进程 sandbox 交给 executor。

源码位置:codex-rs/core/src/tools/runtimes/unified_exec.rs :: UnifiedExecRuntime::uses_executor_managed_process_sandbox

rust
fn uses_executor_managed_process_sandbox(&self, req: &UnifiedExecRequest) -> bool {
    req.turn_environment.environment.is_remote() || req.shell_snapshot.is_some()
}

Core 不给原始 argv 套本机 wrapper,而是在 request 中附加 FileSystemSandboxContext。workspace roots 保持 PathUri,目标 executor 按自身路径约定解析。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt::env_for_exec_server

rust
let request = self
    .manager
    .transform(SandboxTransformRequest {
        command,
        permissions: self.permissions,
        sandbox: SandboxType::None,
        enforce_managed_network: self.enforce_managed_network,
        environment_id: None,
        network: None,
        sandbox_policy_cwd: self.sandbox_cwd,
        codex_linux_sandbox_exe: None,
        use_legacy_landlock: self.use_legacy_landlock,
        windows_sandbox_level: self.windows_sandbox_level,
        windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
    })
    .map_err(CodexErr::from)?;

if self.sandbox_requested {
    exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
        permissions: exec_server_permissions.into(),
        cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
        workspace_roots: self.workspace_roots.to_vec(),
        windows_sandbox_level: executor_windows_sandbox_level(
            self.windows_sandbox_level,
            self.sandbox_cwd,
        ),
        windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
        windows_sandbox_proxy_settings_mode: None,
        use_legacy_landlock: self.use_legacy_landlock,
    });
}

三层责任到此发生转移:审批与策略仍在 Core,最终文件强制在 exec-server。Core 可以验证 request 是否携带正确 context,却不能把远程 executor 的实际实现当成本地 OS sandbox 的直接延伸。

10. 重试重新授权 ​

第一轮 sandbox denial 不是自动升级信号。Orchestrator 先确认 denial 可归因、runtime 允许 escalation、approval policy 允许无 sandbox 请求,并且 denied-read 不会因 bypass 消失。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: sandbox denial handling

rust
if network_policy_decision.is_some() && network_approval_context.is_none() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}
if !tool.escalate_on_failure() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}
// Under `Never` or `OnRequest`, do not retry without sandbox;
// surface a concise sandbox denial that preserves the
// original output.
if !tool.wants_no_sandbox_approval(approval_policy) {
    let allow_on_request_network_prompt =
        matches!(approval_policy, AskForApproval::OnRequest)
            && network_approval_context.is_some()
            && matches!(
                default_exec_approval_requirement(
                    approval_policy,
                    &file_system_sandbox_policy
                ),
                ExecApprovalRequirement::NeedsApproval { .. }
            );
    if !allow_on_request_network_prompt {
        otel.sandbox_outcome(
            &otel_tn,
            otel_ci,
            "denied",
            initial_duration,
            /*escalated_duration*/ None,
        );
        return Err(ToolError::Codex(err));
    }
}
if !unsandboxed_allowed && network_approval_context.is_none() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}
let retry_reason =
    if let Some(network_approval_context) = network_approval_context.as_ref() {
        format!(
            "Network access to \"{}\" is blocked by policy.",
            network_approval_context.host
        )
    } else {
        build_denial_reason_from_output(output.as_ref())
    };

// Strict auto-review approval covers the sandboxed attempt only;
// retrying without the sandbox requires a fresh guardian review.
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?;
}

判断顺序决定是否允许继续。strict auto review 的初次批准只覆盖 sandboxed attempt,无 sandbox retry 必须重新经过 Guardian。非 strict 路径只有在 should_bypass_approval 明确允许时才复用已有决定。

重试 attempt 也不一定完全无 sandbox。存在 denied-read 时 unsandboxed_allowed=false,但网络 denial 可能仍允许在保留文件 sandbox 的条件下重新执行;所以“升级”应理解为新的权限/网络组合,不是固定等于 SandboxType::None。

11. 四种场景 ​

场景审批层策略层强制层
restricted + OnRequestNeedsApprovalsandbox_requested=true本地 wrapper 或 executor context
Never + restrictedSkip(bypass=false)仍计算受限 attemptsandbox 继续生效
exec policy Allow,全 segment 匹配Skip(bypass=true)无 deny 时可首轮 bypass可能无本地 wrapper
require escalated + denied-read可获批准override 被压回 NoOverride保留文件 sandbox
external profile通常不因文件限制提示enforcement owner 为 externalCore 不添加外层文件 sandbox
sandbox denial retry可能第二次审批新建 retry attempt按新组合再次强制

这张表说明审批结果相同,后两层仍可能不同。两个都显示“Approved”的请求,一个可以在 restricted sandbox 中执行,另一个可能经过 explicit exec-policy allow 首轮 bypass;判断差异必须继续读取 requirement 与 attempt。

12. 测试反推边界 ​

以下测试分别覆盖审批默认值、exec policy segment、权限交集、manager 选择与 executor request:

text
cd codex-rs
cargo test -p codex-core --lib tools::sandboxing::tests:: -- --nocapture --test-threads=1
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-sandboxing --lib policy_transforms::tests:: -- --nocapture --test-threads=1
cargo test -p codex-sandboxing --lib manager::tests:: -- --nocapture --test-threads=1
cargo test -p codex-exec-server --lib process_sandbox::tests:: -- --nocapture --test-threads=1

六组测试分别通过 11、83、6、26、8、6 项,共 140 项。它们覆盖 requirement、审批 resolution、附加权限交集、manager 选择和 executor request 消费;未把 macOS、Linux、Windows 的所有真实 backend 都做端到端运行。

default_exec_approval_requirement_rejects_sandbox_prompt_when_granular_disables_it 输入 restricted filesystem 与 Granular { sandbox_approval: false },断言 requirement 是 Forbidden;配套测试把开关改为 true,断言恢复 NeedsApproval。它验证审批类别控制,不验证任何 UI。

源码位置:codex-rs/core/src/tools/sandboxing_tests.rs :: default_exec_approval_requirement_rejects_sandbox_prompt_when_granular_disables_it

rust
let policy = AskForApproval::Granular(GranularApprovalConfig {
    sandbox_approval: false,
    rules: true,
    skill_approval: true,
    request_permissions: true,
    mcp_elicitations: true,
});

let requirement =
    default_exec_approval_requirement(policy, &FileSystemSandboxPolicy::default());

assert_eq!(
    requirement,
    ExecApprovalRequirement::Forbidden {
        reason: "approval policy disallowed sandbox approval prompt".to_string(),
    }
);

multi_segment_shell_requires_policy_allow_for_every_segment_to_bypass_sandbox 给复合 shell 命令只配置部分 allow rule,断言 requirement 即使最终 Skip,也不能设置 bypass;对应 all-segments 测试才允许 bypass。它验证 exec policy 不会把局部 allow 扩大到整个 shell script。

intersect_permission_profiles_drops_ungranted_nonempty_path_requests 输入请求的 path grant 与不包含该 path 的 reviewer grant,断言结果删除未获批路径;deny 相关测试还要求约束条目在交集后保留。它验证 approval grant 不能超出实际请求,也不能移除限制。

exec_server_env_keeps_command_native_and_carries_sandbox_context 断言 executor request 保留原始 argv、Core wrapper 为 None,同时携带 sandbox context。process_sandbox 测试再验证 executor 会按目标平台包装 argv;两者共同覆盖策略传输与执行器消费,但不等于在每个 OS 上完成真实内核隔离测试。

沿源码排查时,看到“为什么批准后仍被拒绝”,先看策略层是否保留 denied-read、owner network policy 或 external enforcement;看到“为什么没有本地 wrapper”,检查是否为 executor-managed;看到“为什么 Allow 仍未 bypass”,检查每个 command segment 是否都有显式 allow。三层逐一定位,比只看最终错误文本更可靠。

下一篇SandboxPolicy完整参考将继续处理兼容层,逐项解释旧 SandboxPolicy 如何投影到 canonical PermissionProfile 与文件、网络权限。