权限审批沙箱三层模型
“用户批准了命令”与“命令可以无约束运行”在 Codex 中不是同一句话。批准只解决授权问题;策略层还要把当前 environment、exec policy、permission profile 与单次附加权限编译成 SandboxAttempt;最后由本机 sandbox 或远程 exec-server 把这份 attempt 变成受限进程。
这三层分别拥有不同的数据和失败方式:
- 审批层产生
ReviewDecision,回答动作能否继续。 - 策略层产生
ExecApprovalRequirement、有效PermissionProfile和SandboxAttempt。 - 强制层选择 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、SandboxAttempt | environment、exec policy、附加权限、网络状态 | 初次或重试 attempt |
| 强制 | SandboxManager、FileSystemSandboxContext、exec-server | argv、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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 + OnRequest | NeedsApproval | sandbox_requested=true | 本地 wrapper 或 executor context |
| Never + restricted | Skip(bypass=false) | 仍计算受限 attempt | sandbox 继续生效 |
| exec policy Allow,全 segment 匹配 | Skip(bypass=true) | 无 deny 时可首轮 bypass | 可能无本地 wrapper |
| require escalated + denied-read | 可获批准 | override 被压回 NoOverride | 保留文件 sandbox |
| external profile | 通常不因文件限制提示 | enforcement owner 为 external | Core 不添加外层文件 sandbox |
| sandbox denial retry | 可能第二次审批 | 新建 retry attempt | 按新组合再次强制 |
这张表说明审批结果相同,后两层仍可能不同。两个都显示“Approved”的请求,一个可以在 restricted sandbox 中执行,另一个可能经过 explicit exec-policy allow 首轮 bypass;判断差异必须继续读取 requirement 与 attempt。
12. 测试反推边界
以下测试分别覆盖审批默认值、exec policy segment、权限交集、manager 选择与 executor request:
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
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 与文件、网络权限。
