跨平台Sandbox抽象
Codex 的 sandbox 抽象不只是在 macOS、Linux 和 Windows 三种 wrapper 之间做 match。真正需要分开的有三个问题:策略是否要求隔离、当前进程是否拥有隔离 backend、最终在哪台机器把 PermissionProfile 物化成平台规则。SandboxManager::should_sandbox 只回答第一个问题,select_initial 为本机执行选择具体 SandboxType,remote 或 snapshot-backed Unified Exec 则保留原始 argv,把 sandbox context 交给 exec-server。
因此,SandboxType::None 不能直接翻译为“不使用 sandbox”。它可能表示当前权限确实不要求隔离,也可能表示 Core 主机不是 enforcement owner,还可能表示 Windows direct-spawn wrapper 已经把限制编码进新的 argv。0.150.0 用独立的 sandbox_requested 保存策略意图,避免 concrete backend 字段丢失所有权信息。
本文承接命令规范化与审批缓存和PermissionProfile解析。前文解释 approval 与 canonical 权限;本文追踪 Orchestrator、SandboxManager、Unified Exec 和 exec-server 之间的职责,不展开 Seatbelt、Landlock、Seccomp、Bubblewrap 或 Windows token 的规则细节。
1. 抽象对象
1.1 Backend枚举
SandboxType 表示当前 launch request 上已经选择的具体 backend,而不是抽象权限策略。macOS 使用 Seatbelt,Linux 通过 Codex sandbox helper 组合 filesystem 与 Seccomp,Windows 使用 restricted/elevated token 家族;None 表示当前 request 没有本机 wrapper。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxType, SandboxablePreference, get_platform_sandbox
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxType {
None,
MacosSeatbelt,
LinuxSeccomp,
WindowsRestrictedToken,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxablePreference {
Auto,
Require,
Forbid,
}
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
if cfg!(target_os = "macos") {
Some(SandboxType::MacosSeatbelt)
} else if cfg!(target_os = "linux") {
Some(SandboxType::LinuxSeccomp)
} else if cfg!(target_os = "windows") {
if windows_sandbox_enabled {
Some(SandboxType::WindowsRestrictedToken)
} else {
None
}
} else {
None
}
}SandboxablePreference 由工具实现提供:Auto 让权限决定,Require 强制提出 sandbox intent,Forbid 禁止当前工具使用平台 sandbox。它不是用户的 ApprovalPolicy,也不是 PermissionProfile 的变体。
1.2 传输对象
SandboxCommand 保留 PathUri cwd、环境变量、managed network context 和 additional permissions。SandboxTransformRequest 再加入 canonical PermissionProfile、选中的 backend、network owner 信息和平台配置。SandboxExecRequest 是 transform 后的 launch snapshot。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxCommand, SandboxExecRequest, SandboxTransformRequest
#[derive(Debug)]
pub struct SandboxCommand {
pub program: OsString,
pub args: Vec<String>,
pub cwd: PathUri,
pub env: HashMap<String, String>,
pub managed_network: Option<ManagedNetworkSandboxContext>,
pub additional_permissions: Option<AdditionalPermissionProfile>,
}
#[derive(Debug)]
pub struct SandboxExecRequest {
pub command: Vec<String>,
pub cwd: PathUri,
pub sandbox_policy_cwd: PathUri,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub network_environment_id: Option<String>,
pub sandbox: SandboxType,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub permission_profile: PermissionProfile,
pub arg0: Option<String>,
}
pub struct SandboxTransformRequest<'a> {
pub command: SandboxCommand,
pub permissions: &'a PermissionProfile,
pub sandbox: SandboxType,
pub enforce_managed_network: bool,
pub environment_id: Option<&'a str>,
pub network: Option<&'a NetworkProxy>,
pub sandbox_policy_cwd: &'a PathUri,
pub codex_linux_sandbox_exe: Option<&'a Path>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
}PathUri 一直保留到 enforcement owner,避免 Core 在 POSIX 主机上把 Windows path 或远端 cwd 提前解释成错误的本机路径。只有真正要构造本机 sandbox wrapper 时,才转为 AbsolutePathBuf。
2. Sandbox需求
2.1 工具偏好
SandboxManager::should_sandbox 独立计算 intent。Forbid 和 Require 直接返回;Auto 才把 PermissionProfile 投影为 filesystem/network runtime policy,再调用 should_require_platform_sandbox。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::should_sandbox
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,
)
}
}
}2.2 文件与网络组合
managed network requirement 无条件需要 sandbox,因为进程必须被约束到代理边界。没有 managed requirement 时,如果 canonical network policy 是 Restricted,除了 ExternalSandbox 外仍需要平台 sandbox。网络已启用时,只有 filesystem 仍受限且不是 full-disk-write 才继续需要平台 sandbox。
源码位置:codex-rs/sandboxing/src/policy_transforms.rs :: should_require_platform_sandbox
pub fn should_require_platform_sandbox(
file_system_policy: &FileSystemSandboxPolicy,
network_policy: NetworkSandboxPolicy,
has_managed_network_requirements: bool,
) -> bool {
if has_managed_network_requirements {
return true;
}
if !network_policy.is_enabled() {
return !matches!(
file_system_policy.kind,
FileSystemSandboxKind::ExternalSandbox
);
}
match file_system_policy.kind {
FileSystemSandboxKind::Restricted => !file_system_policy.has_full_disk_write_access(),
FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => false,
}
}full disk write 仍可能包含 deny carveout。has_full_disk_write_access() 会考虑这些限制,因此“Root Write + Deny path”仍需要 sandbox;测试不会把表面上的根目录 Write 简化成 unrestricted。
源码位置:codex-rs/sandboxing/src/policy_transforms_tests.rs :: root_write_policy_with_carveouts_still_uses_platform_sandbox
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: blocked.into(),
access: FileSystemAccessMode::Deny,
missing_path_behavior: None,
},
]);
assert_eq!(
should_require_platform_sandbox(
&policy,
NetworkSandboxPolicy::Enabled,
/*has_managed_network_requirements*/ false
),
true
);3. 本机Backend
select_initial 先调用 should_sandbox,再按编译平台和 Windows level 选择具体 wrapper。需求判断和 backend availability 在此处正式分开:如果策略要求 sandbox,但当前平台没有实现,返回值仍可能是 SandboxType::None。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::select_initial
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
}
}对本机 Core 路径,None 可以表示需求为 false 或 backend unavailable;调用方必须同时保留 sandbox_requested 才能区分。对 exec-server,后者不能静默接受:它使用 Require 重新选择,若仍是 None 就返回“sandbox intent cannot be enforced”。
4. Attempt双状态
4.1 Orchestrator分派
ToolOrchestrator 先询问 runtime 是否由 executor 管理 sandbox。local path 会把 project roots 物化到本机 PermissionProfile;executor-managed path 保留 symbolic roots,让远端 owner 解释自己的 workspace。然后它独立计算 sandbox_requested 和 initial_sandbox。
源码位置: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 {
// Executor-native roots remain symbolic until the executor applies its own sandbox.
permission_profile.clone()
} else {
environment.permission_profile_with_workspace_roots()
};
let file_system_sandbox_policy = permissions.file_system_sandbox_policy();
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run
let sandbox_preference = tool.sandbox_preference();
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
};remote request 即使 sandbox_requested=true,initial_sandbox 也会是 None,因为 Core 不应该把本机 Seatbelt 或 Linux helper 包到远端 argv 上。这个组合是正常的 owner handoff,不是降级。
4.2 Attempt快照
SandboxAttempt 同时保存两套权限:permissions 是本机可能已经物化 workspace roots 的 profile;exec_server_permissions 是 executor 应接收的 canonical profile。它还保存 intent、concrete type、network enforcement 和平台参数,供第一次执行与 retry 分别构造 request。
源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt
pub(crate) struct SandboxAttempt<'a> {
pub sandbox: SandboxType,
/// Whether policy requested sandboxing, independent of this host's concrete wrapper.
pub sandbox_requested: bool,
pub permissions: &'a codex_protocol::models::PermissionProfile,
/// Canonical permissions before this host materializes workspace roots.
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>,
}5. Local执行
5.1 权限合并
SandboxManager::transform 先把 additional permissions 合并到基础 profile,再把 managed MITM CA bundle 加为 readable root。只有 sandbox branch 需要 native paths 时,PendingSandboxedExecRequest::new 的 PathUri 转换错误才会真正被消费。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform
let additional_permissions = command.additional_permissions.take();
let managed_mitm_ca_trust_bundle_path =
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
let base_effective_permission_profile =
effective_permission_profile(permissions, additional_permissions.as_ref());
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
&command.cwd,
sandbox_policy_cwd,
base_effective_permission_profile.clone(),
managed_mitm_ca_trust_bundle_path.as_ref(),
);
let mut argv = Vec::with_capacity(1 + command.args.len());
argv.push(command.program);
argv.extend(command.args.into_iter().map(OsString::from));additional write grant 不得覆盖原 profile 的 Deny entry。测试构造 Root Read + denied path,再增加 allowed path Write,最终 profile 同时保留三条 entry。
源码位置:codex-rs/sandboxing/src/manager_tests.rs :: transform_additional_permissions_preserves_denied_entries
assert_eq!(
exec_request.permission_profile.file_system_sandbox_policy(),
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: denied_path.into(),
access: FileSystemAccessMode::Deny,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: allowed_path.into(),
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
])
);5.2 平台转换
transform 的 backend match 才生成平台 argv。macOS 把原命令交给 Seatbelt profile builder,然后在前面加入 /usr/bin/sandbox-exec;Linux 要求 helper path,生成 PermissionProfile 参数并保留 arg0 override;Windows 普通 transform 暂不包装 argv,但验证 managed network 必须使用 Elevated backend。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform
let pending = pending_sandboxed_request?;
let (file_system_sandbox_policy, network_sandbox_policy) = pending
.effective_permission_profile
.to_runtime_permissions();
let mut args = create_seatbelt_command_args_with_profile(
CreateSeatbeltCommandArgsParams {
command: os_argv_to_strings(argv),
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
enforce_managed_network,
managed_network,
environment_id,
network,
extra_allow_unix_sockets: &[],
},
self.seatbelt_profile,
)
.map_err(|err| match err {
SeatbeltPreparationError::FileSystem(message) => {
SandboxTransformError::SeatbeltPreparation(message)
}
SeatbeltPreparationError::EnvironmentNetworkProxy(message) => {
SandboxTransformError::EnvironmentNetworkProxy(message)
}
})?;
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
(full_command, None, Some(pending))源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform
SandboxType::LinuxSeccomp => {
let pending = pending_sandboxed_request?;
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
#[cfg(target_os = "linux")]
ensure_linux_bubblewrap_is_supported(
&pending
.effective_permission_profile
.file_system_sandbox_policy(),
use_legacy_landlock,
allow_proxy_network,
is_wsl1(),
)?;
let mut args = create_linux_sandbox_command_args_for_permission_profile(
os_argv_to_strings(argv),
pending.native_command_cwd.as_path(),
&pending.effective_permission_profile,
pending.native_sandbox_policy_cwd.as_path(),
use_legacy_landlock,
allow_proxy_network,
);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
full_command.append(&mut args);
(
full_command,
Some(linux_sandbox_arg0_override(exe)),
Some(pending),
)
}这些代码块各自摘自条件编译分支,不能从 macOS 测试通过外推 Linux 或 Windows 内核实际 enforcement;它们证明的是参数生成与 owner 选择。
5.3 Foreign cwd
SandboxType::None 分支不会读取 pending_sandboxed_request。因此 foreign cwd 可以保持 PathUri,effective PermissionProfile 仍随 request 传递。只有本机 wrapper 分支才要求 cwd 能在当前 host 转为 AbsolutePathBuf。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform
// Unsandboxed exec-server requests may have foreign cwd values that cannot be prepared
// locally, but their effective permissions must still be preserved. In that case, carry
// forward the base profile.
let permission_profile = pending_sandboxed_request
.map_or(base_effective_permission_profile, |pending| {
pending.effective_permission_profile
});
Ok(SandboxExecRequest {
command: argv,
cwd: command.cwd,
sandbox_policy_cwd: sandbox_policy_cwd.clone(),
env: command.env,
network: network.cloned(),
network_environment_id: environment_id.map(str::to_string),
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile,
arg0: arg0_override,
})6. Executor执行
6.1 Runtime选择owner
Unified Exec 将 remote environment 或 shell snapshot 标记为 executor-managed。Apply Patch 只在 remote environment 使用 executor-managed filesystem sandbox。两者都让 Orchestrator 保留 sandbox intent,而不在 Core 主机选择 wrapper。
源码位置:
codex-rs/core/src/tools/runtimes/unified_exec.rs :: UnifiedExecRuntime::uses_executor_managed_process_sandboxcodex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchRuntime::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()
}Apply Patch 的对应实现只检查 environment.is_remote(),因为 local Apply Patch 直接通过 environment filesystem API 执行,而 shell snapshot 是进程执行能力。
6.2 Native argv
env_for_exec_server 明确用 SandboxType::None 调用本机 manager,只为整理通用 request,不能把 host wrapper 发送给 executor。若 sandbox_requested=true,它再附加 canonical permissions、cwd、workspace roots 和 executor Windows level。
源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt::env_for_exec_server
let request = self
.manager
.transform(SandboxTransformRequest {
command,
permissions: self.permissions,
// The exec-server must receive the native command, not this host's wrapper.
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)?;
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
Vec::new(),
)?;
exec_request.exec_server_managed_network = managed_network;源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt::env_for_exec_server
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,
});
exec_request.exec_server_enforce_managed_network = self.enforce_managed_network;
}
Ok(exec_request)Windows path convention还有一个兼容点:Core 默认 Windows sandbox level 为 Disabled,但远端 cwd 明确使用 Windows convention 时,executor_windows_sandbox_level 会升级为 RestrictedToken,避免把 host 平台默认错误传给 Windows executor。
7. ExecServer强制
7.1 Context物化
exec-server 收到 sandbox context 后才把 PathUri 转成本机路径、物化 project roots、加入 CA bundle 与 helper readable roots。然后使用 SandboxablePreference::Require 选择当前 executor backend。
源码位置:codex-rs/exec-server/src/process_sandbox.rs :: prepare_exec_request
let permissions: PermissionProfile = sandbox_context
.permissions
.clone()
.try_into()
.map_err(|err| invalid_params(format!("invalid sandbox permission path URI: {err}")))?;
let sandbox_policy_cwd = sandbox_context.cwd.as_ref().unwrap_or(¶ms.cwd);
let native_sandbox_policy_cwd = native_path(sandbox_policy_cwd, "sandbox cwd")?;
let native_workspace_roots = sandbox_context
.workspace_roots
.iter()
.map(|root| native_path(root, "sandbox workspace root"))
.collect::<Result<Vec<_>, _>>()?;
let workspace_roots = native_workspace_roots.as_slice();
let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots);7.2 Fail closed
executor 使用 Require 后若得到 SandboxType::None,直接返回 invalid params。这样 Core 传来的 intent 不会因为 executor 缺少平台实现而静默变成 native launch。
源码位置:codex-rs/exec-server/src/process_sandbox.rs :: prepare_exec_request
let sandbox_manager = SandboxManager::new();
let sandbox = sandbox_manager.select_initial(
&permissions,
SandboxablePreference::Require,
sandbox_context.windows_sandbox_level,
params.enforce_managed_network,
);
if sandbox == SandboxType::None {
return Err(invalid_params(
"sandbox intent cannot be enforced on this executor".to_string(),
));
}随后 exec-server 构造自己的 SandboxDirectSpawnTransformRequest。Unix backend 把 wrapper 编进 command;Windows shared launcher 保留独立 PreparedWindowsSandboxRequest,由 native session spawner 消费。
8. Windows包装
Windows direct spawn 与 Unix 不同:common transform 的 WindowsRestrictedToken 暂时保留原 argv;wrap_windows_sandbox_exec_request_for_direct_spawn 解析 native cwd、寻找 helper、计算 filesystem overrides,再把 inner command 编码到 wrapper args 中。
源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::transform_for_direct_spawn_with_codex_home
let workspace_roots = request.workspace_roots;
let proxy_settings_mode = request.windows_sandbox_proxy_settings_mode;
let mut request = self.transform(request.transform)?;
if request.sandbox == SandboxType::WindowsRestrictedToken {
wrap_windows_sandbox_exec_request_for_direct_spawn(
&mut request,
workspace_roots,
codex_home,
proxy_settings_mode,
)?;
}
Ok(request)包装完成后外层 request 的 sandbox 设为 None,因为后续 generic spawner 不再应用第二层 wrapper;限制已经转移到 command 和 Windows spawn request 中。arg0 同时清空,避免外层错误覆盖 helper 身份。
源码位置:codex-rs/sandboxing/src/manager.rs :: wrap_windows_sandbox_exec_request_for_direct_spawn
request.command = Vec::with_capacity(1 + wrapper_args.len());
request.command.push(source.to_string_lossy().into_owned());
request.command.append(&mut wrapper_args);
request.sandbox = SandboxType::None;
request.arg0 = None;
add_windows_sandbox_wrapper_setup_env(&mut request.env);
Ok(())Windows managed network 还要求 Elevated backend;unelevated restricted token 无法满足该 enforcement 组合时,transform 返回 WindowsSandboxPreparation,不会退回 unsandboxed。
9. Denial识别
本机拒绝可以依据 concrete SandboxType 解析 backend-specific 输出。executor-managed Apply Patch 的 attempt 则是 sandbox=None,所以它必须组合 sandbox_requested 与 generic executor denial heuristic;否则 remote sandbox denial 会被误判成普通工具失败。
源码位置:codex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchRuntime::run
let sandbox_denied = failed
&& if attempt.sandbox == SandboxType::None {
attempt.sandbox_requested && is_likely_executor_managed_sandbox_denied(&output)
} else {
is_likely_sandbox_denied(attempt.sandbox, &output)
};
if sandbox_denied {
if attempt.sandbox != SandboxType::None {
record_filesystem_sandbox_violation(attempt.sandbox, &output);
}
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
network_policy_decision: None,
})));
}这再次说明 None 只描述当前进程看见的 concrete backend。是否应按 sandbox denial 处理,还需要 intent 与 owner 信息。
10. 重试路径
sandbox denial 后,Orchestrator 不复用第一次 attempt 的 concrete type。它根据审批结果、denied-read 约束和当前 owner 重新计算 retry_sandbox_requested;local owner 再选择 backend,executor owner 仍保持 None 并传递新的 context。
源码位置:codex-rs/core/src/tools/orchestrator.rs :: retry sandbox construction
let retry_sandbox_requested = !unsandboxed_allowed
&& self.sandbox.should_sandbox(
&permissions,
sandbox_preference,
managed_network_active,
);
let retry_sandbox = if retry_sandbox_requested && !executor_managed_process_sandbox {
self.sandbox.select_initial(
&permissions,
sandbox_preference,
turn_ctx.windows_sandbox_level,
managed_network_active,
)
} else {
SandboxType::None
};
let retry_codex_linux_sandbox_exe = if unsandboxed_allowed {
None
} else {
turn_ctx.config.codex_linux_sandbox_exe.as_ref()
};
let retry_attempt = SandboxAttempt {
sandbox: retry_sandbox,
sandbox_requested: retry_sandbox_requested,
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,如果 PermissionProfile 有 denied-read,unsandboxed_execution_allowed 为 false,即使用户批准 escalation 也不能丢掉 sandbox;retry 会继续携带 intent 和 Deny entries。这个约束由上一层 approval/sandbox orchestration 保证,不由平台 wrapper 自行猜测。
11. 测试边界
本篇运行了三层测试:
codex-sandboxing94 项覆盖 intent 选择、additional permissions、Deny 保留、foreign cwd、Seatbelt 参数、Landlock 参数和 denial 分类。- Core sandboxing 11 项覆盖 deny-read、first-attempt override、exec-server context 和 Windows unsupported backend 拒绝。
- exec-server process sandbox 6 项覆盖 executor-native argv 包装、custom arg0、managed proxy、native launch 和 fail-closed proxy 输入。
- Unified Exec runtime 6 项覆盖 environment key、trusted sandbox cwd、zsh-fork override 与 additional permissions 传播。
关键反向测试是 exec_server_env_keeps_command_native_and_carries_sandbox_context。输入 attempt 为 sandbox=None, sandbox_requested=true,断言 command 仍是 /bin/bash -lc pwd,同时 exec_server_sandbox 包含 canonical PermissionProfile、cwd 和 workspace roots;把 intent 改为 false 后,context 消失,但 managed network payload仍可作为 execution data保留。
源码位置:codex-rs/core/src/tools/sandboxing_tests.rs :: exec_server_env_keeps_command_native_and_carries_sandbox_context
assert_eq!(
request.command,
vec![
"/bin/bash".to_string(),
"-lc".to_string(),
"pwd".to_string()
]
);
assert_eq!(request.arg0, None);
assert_eq!(request.sandbox, SandboxType::None);
assert_eq!(
request.exec_server_sandbox,
Some(codex_exec_server::FileSystemSandboxContext {
permissions: exec_server_permissions.clone().into(),
cwd: Some(cwd_uri.clone()),
workspace_roots: vec![cwd_uri.clone()],
windows_sandbox_level: if cfg!(windows) {
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
} else {
codex_protocol::config_types::WindowsSandboxLevel::Disabled
},
windows_sandbox_private_desktop: false,
windows_sandbox_proxy_settings_mode: None,
use_legacy_landlock: false,
})
);
assert!(request.exec_server_enforce_managed_network);exec-server 的 sandbox_request_wraps_native_argv_on_executor 则从另一端验证 owner:收到 native argv 与 workspace PermissionProfile 后,macOS 首 token 变为 /usr/bin/sandbox-exec,Linux command 包含 materialized PermissionProfile 和 executor helper。
源码位置:codex-rs/exec-server/src/process_sandbox_tests.rs :: sandbox_request_wraps_native_argv_on_executor
assert_ne!(prepared.command, params.argv);
assert_eq!(prepared.cwd, cwd);
#[cfg(target_os = "linux")]
{
assert_eq!(
prepared.command.first(),
Some(&runtime_paths.codex_self_exe.to_string_lossy().into_owned())
);
let permission_profile_json = prepared
.command
.iter()
.position(|arg| arg == "--permission-profile")
.and_then(|index| prepared.command.get(index + 1))
.expect("sandbox wrapper permission profile");
let permission_profile: PermissionProfile =
serde_json::from_str(permission_profile_json).expect("permission profile JSON");
assert_eq!(
permission_profile,
PermissionProfile::workspace_write()
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd))
);
}
#[cfg(target_os = "macos")]
assert_eq!(
prepared.command.first().map(String::as_str),
Some("/usr/bin/sandbox-exec")
);这些测试证明选择、参数传输和本机可执行平台上的 wrapper 构造;不能证明未运行平台的内核 enforcement,也不能证明外部 PermissionProfile::External 的隔离实现。ExternalSandbox 的 owner 在 Codex 之外,Codex 只保留其权限语义并避免重复选择本机 backend。
读者可以在 codex-rs 工作区执行对应测试组,先观察 manager 与 policy transform,再观察 Core 如何携带 intent,最后观察 exec-server 如何在 executor 端包装原始 argv:
cargo test -p codex-sandboxing --lib -- --nocapture --test-threads=1
cargo test -p codex-core --lib tools::sandboxing::tests:: -- --nocapture --test-threads=1
cargo test -p codex-exec-server --lib process_sandbox::tests:: -- --nocapture --test-threads=112. 继续阅读
可以用四个问题复述调用链:
- 为什么 remote Unified Exec 的
SandboxAttempt.sandbox是None,却仍可能被视为 sandboxed execution? - 为什么 Core local path 物化 workspace roots,而 exec-server path 保留 canonical
project_roots直到 executor? - 为什么 Windows wrapper 完成后把外层
sandbox改成None,这与无隔离有什么区别? - 为什么 sandbox denial 判断必须同时查看 concrete backend、
sandbox_requested和 execution owner?
下一篇macOS Seatbelt规则生成会进入本地 macOS backend,分析 PermissionProfile 如何编译为 SBPL 参数、路径规则与网络代理约束。
