#[cfg(target_os = "linux")] use crate::bwrap::WSL1_BWRAP_WARNING; #[cfg(target_os = "linux")] use crate::bwrap::is_wsl1; use crate::landlock::CODEX_LINUX_SANDBOX_ARG0; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; use crate::policy_transforms::EffectiveSandboxPermissions; use crate::policy_transforms::effective_file_system_sandbox_policy; use crate::policy_transforms::effective_network_sandbox_policy; use crate::policy_transforms::should_require_platform_sandbox; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::ffi::OsString; use std::path::Path; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SandboxType { None, MacosSeatbelt, LinuxSeccomp, WindowsRestrictedToken, } impl SandboxType { pub fn as_metric_tag(self) -> &'static str { match self { SandboxType::None => "none", SandboxType::MacosSeatbelt => "seatbelt", SandboxType::LinuxSeccomp => "seccomp", SandboxType::WindowsRestrictedToken => "windows_sandbox", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SandboxablePreference { Auto, Require, Forbid, } pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { 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 } } #[derive(Debug)] pub struct SandboxCommand { pub program: OsString, pub args: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, pub additional_permissions: Option, } #[derive(Debug)] pub struct SandboxExecRequest { pub command: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, pub network: Option, pub sandbox: SandboxType, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, pub arg0: Option, } /// Bundled arguments for sandbox transformation. /// /// This keeps call sites self-documenting when several fields are optional. pub struct SandboxTransformRequest<'a> { pub command: SandboxCommand, pub policy: &'a SandboxPolicy, pub file_system_policy: &'a FileSystemSandboxPolicy, pub network_policy: NetworkSandboxPolicy, pub sandbox: SandboxType, pub enforce_managed_network: bool, // TODO(viyatb): Evaluate switching this to Option> // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a Path>, pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, } #[derive(Debug)] pub enum SandboxTransformError { MissingLinuxSandboxExecutable, #[cfg(target_os = "linux")] Wsl1UnsupportedForBubblewrap, #[cfg(not(target_os = "macos"))] SeatbeltUnavailable, } impl std::fmt::Display for SandboxTransformError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::MissingLinuxSandboxExecutable => { write!(f, "missing codex-linux-sandbox executable path") } #[cfg(target_os = "linux")] Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"), #[cfg(not(target_os = "macos"))] Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"), } } } impl std::error::Error for SandboxTransformError {} #[derive(Default)] pub struct SandboxManager; impl SandboxManager { pub fn new() -> Self { Self } pub fn select_initial( &self, file_system_policy: &FileSystemSandboxPolicy, network_policy: NetworkSandboxPolicy, pref: SandboxablePreference, windows_sandbox_level: WindowsSandboxLevel, has_managed_network_requirements: bool, ) -> SandboxType { match pref { SandboxablePreference::Forbid => SandboxType::None, SandboxablePreference::Require => { get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) .unwrap_or(SandboxType::None) } SandboxablePreference::Auto => { if should_require_platform_sandbox( file_system_policy, network_policy, has_managed_network_requirements, ) { get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) .unwrap_or(SandboxType::None) } else { SandboxType::None } } } } pub fn transform( &self, request: SandboxTransformRequest<'_>, ) -> Result { let SandboxTransformRequest { mut command, policy, file_system_policy, network_policy, sandbox, enforce_managed_network, network, sandbox_policy_cwd, codex_linux_sandbox_exe, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, } = request; let additional_permissions = command.additional_permissions.take(); let EffectiveSandboxPermissions { sandbox_policy: effective_policy, } = EffectiveSandboxPermissions::new(policy, additional_permissions.as_ref()); let effective_file_system_policy = effective_file_system_sandbox_policy( file_system_policy, additional_permissions.as_ref(), ); let effective_network_policy = effective_network_sandbox_policy(network_policy, additional_permissions.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)); let (argv, arg0_override) = match sandbox { SandboxType::None => (os_argv_to_strings(argv), None), #[cfg(target_os = "macos")] SandboxType::MacosSeatbelt => { use crate::seatbelt::CreateSeatbeltCommandArgsParams; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use crate::seatbelt::create_seatbelt_command_args; let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: os_argv_to_strings(argv), file_system_sandbox_policy: &effective_file_system_policy, network_sandbox_policy: effective_network_policy, sandbox_policy_cwd, enforce_managed_network, network, extra_allow_unix_sockets: &[], }); 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) } #[cfg(not(target_os = "macos"))] SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), SandboxType::LinuxSeccomp => { 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( &effective_file_system_policy, use_legacy_landlock, allow_proxy_network, is_wsl1(), )?; let mut args = create_linux_sandbox_command_args_for_policies( os_argv_to_strings(argv), command.cwd.as_path(), &effective_policy, &effective_file_system_policy, effective_network_policy, sandbox_policy_cwd, 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))) } #[cfg(target_os = "windows")] SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None), #[cfg(not(target_os = "windows"))] SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None), }; Ok(SandboxExecRequest { command: argv, cwd: command.cwd, env: command.env, network: network.cloned(), sandbox, windows_sandbox_level, windows_sandbox_private_desktop, sandbox_policy: effective_policy, file_system_sandbox_policy: effective_file_system_policy, network_sandbox_policy: effective_network_policy, arg0: arg0_override, }) } } #[cfg(target_os = "linux")] fn ensure_linux_bubblewrap_is_supported( file_system_sandbox_policy: &FileSystemSandboxPolicy, use_legacy_landlock: bool, allow_network_for_proxy: bool, is_wsl1: bool, ) -> Result<(), SandboxTransformError> { let requires_bubblewrap = !use_legacy_landlock && (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy); if is_wsl1 && requires_bubblewrap { return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap); } Ok(()) } fn os_argv_to_strings(argv: Vec) -> Vec { argv.into_iter() .map(os_string_to_command_component) .collect() } fn os_string_to_command_component(value: OsString) -> String { value .into_string() .unwrap_or_else(|value| value.to_string_lossy().into_owned()) } fn linux_sandbox_arg0_override(exe: &Path) -> String { if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) { os_string_to_command_component(exe.as_os_str().to_owned()) } else { CODEX_LINUX_SANDBOX_ARG0.to_string() } } #[cfg(test)] #[path = "manager_tests.rs"] mod tests;