Skip to content

跨平台Sandbox抽象

从sandbox intent、PermissionProfile和本机backend选择走到executor-managed强制、平台命令包装与拒绝识别。

基于rust-v0.150.0
CodexRustSecuritySandbox

跨平台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

rust
#[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

rust
#[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

rust
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

rust
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

rust
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

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

对本机 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

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 {
    // 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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
// 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_sandbox
  • codex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchRuntime::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()
}

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

rust
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

rust
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

rust
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(&params.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

rust
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

rust
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

rust
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

rust
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

rust
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-sandboxing 94 项覆盖 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

rust
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

rust
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:

bash
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=1

12. 继续阅读 ​

可以用四个问题复述调用链:

  1. 为什么 remote Unified Exec 的 SandboxAttempt.sandbox 是 None,却仍可能被视为 sandboxed execution?
  2. 为什么 Core local path 物化 workspace roots,而 exec-server path 保留 canonical project_roots 直到 executor?
  3. 为什么 Windows wrapper 完成后把外层 sandbox 改成 None,这与无隔离有什么区别?
  4. 为什么 sandbox denial 判断必须同时查看 concrete backend、sandbox_requested 和 execution owner?

下一篇macOS Seatbelt规则生成会进入本地 macOS backend,分析 PermissionProfile 如何编译为 SBPL 参数、路径规则与网络代理约束。