From 3d8f475bce3becf434792712d6f4fbfcbd59830f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 4 Mar 2026 00:32:38 -0800 Subject: [PATCH] sandboxing: plumb split sandbox policies through runtime --- .../app-server/src/codex_message_processor.rs | 22 ++- codex-rs/core/src/codex.rs | 30 ++++ codex-rs/core/src/exec.rs | 40 +++-- codex-rs/core/src/landlock.rs | 3 +- codex-rs/core/src/sandboxing/mod.rs | 137 ++++++++++++++++-- codex-rs/core/src/seatbelt.rs | 3 +- codex-rs/core/src/spawn.rs | 16 +- codex-rs/core/src/tasks/user_shell.rs | 6 +- codex-rs/core/src/tools/js_repl/mod.rs | 4 +- codex-rs/core/src/tools/orchestrator.rs | 6 +- .../tools/runtimes/shell/unix_escalation.rs | 27 +++- .../runtimes/shell/unix_escalation_tests.rs | 6 + codex-rs/core/src/tools/sandboxing.rs | 6 + codex-rs/core/tests/suite/exec.rs | 14 +- codex-rs/core/tests/suite/user_shell_cmd.rs | 31 ++++ .../linux-sandbox/tests/suite/landlock.rs | 6 + 16 files changed, 317 insertions(+), 40 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 80558642cb..2495eba199 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1506,9 +1506,19 @@ impl CodexMessageProcessor { }; let requested_policy = params.sandbox_policy.map(|policy| policy.to_core()); - let effective_policy = match requested_policy { + let ( + effective_policy, + effective_file_system_sandbox_policy, + effective_network_sandbox_policy, + ) = match requested_policy { Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) { - Ok(()) => policy, + Ok(()) => { + let file_system_sandbox_policy = + codex_protocol::protocol::FileSystemSandboxPolicy::from(&policy); + let network_sandbox_policy = + codex_protocol::protocol::NetworkSandboxPolicy::from(&policy); + (policy, file_system_sandbox_policy, network_sandbox_policy) + } Err(err) => { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -1519,7 +1529,11 @@ impl CodexMessageProcessor { return; } }, - None => self.config.permissions.sandbox_policy.get().clone(), + None => ( + self.config.permissions.sandbox_policy.get().clone(), + self.config.permissions.file_system_sandbox_policy.clone(), + self.config.permissions.network_sandbox_policy, + ), }; let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); @@ -1534,6 +1548,8 @@ impl CodexMessageProcessor { match codex_core::exec::process_exec_tool_call( exec_params, &effective_policy, + &effective_file_system_sandbox_policy, + effective_network_sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, use_linux_sandbox_bwrap, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9b9e0214a8..7e78682d31 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -222,10 +222,12 @@ use crate::protocol::ErrorEvent; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecApprovalRequestEvent; +use crate::protocol::FileSystemSandboxPolicy; use crate::protocol::McpServerRefreshConfig; use crate::protocol::ModelRerouteEvent; use crate::protocol::ModelRerouteReason; use crate::protocol::NetworkApprovalContext; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::Op; use crate::protocol::PlanDeltaEvent; use crate::protocol::RateLimitSnapshot; @@ -482,6 +484,8 @@ impl Codex { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -676,6 +680,8 @@ pub(crate) struct TurnContext { pub(crate) personality: Option, pub(crate) approval_policy: Constrained, pub(crate) sandbox_policy: Constrained, + pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy, + pub(crate) network_sandbox_policy: NetworkSandboxPolicy, pub(crate) network: Option, pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, @@ -764,6 +770,8 @@ impl TurnContext { personality: self.personality, approval_policy: self.approval_policy.clone(), sandbox_policy: self.sandbox_policy.clone(), + file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), + network_sandbox_policy: self.network_sandbox_policy, network: self.network.clone(), windows_sandbox_level: self.windows_sandbox_level, shell_environment_policy: self.shell_environment_policy.clone(), @@ -867,6 +875,8 @@ pub(crate) struct SessionConfiguration { approval_policy: Constrained, /// How to sandbox commands executed in the system sandbox_policy: Constrained, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, windows_sandbox_level: WindowsSandboxLevel, /// Working directory that should be treated as the *root* of the @@ -933,6 +943,10 @@ impl SessionConfiguration { } if let Some(sandbox_policy) = updates.sandbox_policy.clone() { next_configuration.sandbox_policy.set(sandbox_policy)?; + next_configuration.file_system_sandbox_policy = + FileSystemSandboxPolicy::from(next_configuration.sandbox_policy.get()); + next_configuration.network_sandbox_policy = + NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); } if let Some(windows_sandbox_level) = updates.windows_sandbox_level { next_configuration.windows_sandbox_level = windows_sandbox_level; @@ -1145,6 +1159,8 @@ impl Session { personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), sandbox_policy: session_configuration.sandbox_policy.clone(), + file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(), + network_sandbox_policy: session_configuration.network_sandbox_policy, network, windows_sandbox_level: session_configuration.windows_sandbox_level, shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), @@ -4805,6 +4821,8 @@ async fn spawn_review_thread( personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy.clone(), sandbox_policy: parent_turn_context.sandbox_policy.clone(), + file_system_sandbox_policy: parent_turn_context.file_system_sandbox_policy.clone(), + network_sandbox_policy: parent_turn_context.network_sandbox_policy, network: parent_turn_context.network.clone(), windows_sandbox_level: parent_turn_context.windows_sandbox_level, shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), @@ -7832,6 +7850,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -7927,6 +7947,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -8247,6 +8269,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -8306,6 +8330,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -8398,6 +8424,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -8647,6 +8675,8 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), sandbox_policy: config.permissions.sandbox_policy.clone(), + file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 01c559c1c5..e848f2d888 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -24,6 +24,9 @@ use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecCommandOutputDeltaEvent; use crate::protocol::ExecOutputStream; +use crate::protocol::FileSystemSandboxKind; +use crate::protocol::FileSystemSandboxPolicy; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::sandboxing::CommandSpec; use crate::sandboxing::ExecRequest; @@ -149,9 +152,12 @@ pub struct StdoutStream { pub tx_event: Sender, } +#[allow(clippy::too_many_arguments)] pub async fn process_exec_tool_call( params: ExecParams, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, use_linux_sandbox_bwrap: bool, @@ -159,8 +165,8 @@ pub async fn process_exec_tool_call( ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; let enforce_managed_network = params.network.is_some(); - let sandbox_type = match &sandbox_policy { - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { + let sandbox_type = match file_system_sandbox_policy.kind { + FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { if enforce_managed_network { get_platform_sandbox( windows_sandbox_level @@ -215,6 +221,8 @@ pub async fn process_exec_tool_call( .transform(crate::sandboxing::SandboxTransformRequest { spec, policy: sandbox_policy, + file_system_policy: file_system_sandbox_policy, + network_policy: network_sandbox_policy, sandbox: sandbox_type, enforce_managed_network, network: network.as_ref(), @@ -233,7 +241,6 @@ pub async fn process_exec_tool_call( pub(crate) async fn execute_exec_env( env: ExecRequest, - sandbox_policy: &SandboxPolicy, stdout_stream: Option, ) -> Result { let ExecRequest { @@ -245,7 +252,9 @@ pub(crate) async fn execute_exec_env( sandbox, windows_sandbox_level, sandbox_permissions, - sandbox_policy: _sandbox_policy_from_env, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, justification, arg0, } = env; @@ -263,7 +272,15 @@ pub(crate) async fn execute_exec_env( }; let start = Instant::now(); - let raw_output_result = exec(params, sandbox, sandbox_policy, stdout_stream).await; + let raw_output_result = exec( + params, + sandbox, + &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, + stdout_stream, + ) + .await; let duration = start.elapsed(); finalize_exec_result(raw_output_result, sandbox, duration) } @@ -692,14 +709,13 @@ async fn exec( params: ExecParams, sandbox: SandboxType, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, stdout_stream: Option, ) -> Result { #[cfg(target_os = "windows")] if sandbox == SandboxType::WindowsRestrictedToken - && !matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) + && file_system_sandbox_policy.kind == FileSystemSandboxKind::Restricted { return exec_windows_sandbox(params, sandbox_policy).await; } @@ -729,7 +745,7 @@ async fn exec( args: args.into(), arg0: arg0_ref, cwd, - sandbox_policy, + network_sandbox_policy, // The environment already has attempt-scoped proxy settings from // apply_to_env_for_attempt above. Passing network here would reapply // non-attempt proxy vars and drop attempt correlation metadata. @@ -1135,6 +1151,8 @@ mod tests { params, SandboxType::None, &SandboxPolicy::new_read_only_policy(), + &FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()), + NetworkSandboxPolicy::Restricted, None, ) .await?; @@ -1190,6 +1208,8 @@ mod tests { let result = process_exec_tool_call( params, &SandboxPolicy::DangerFullAccess, + &FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), + NetworkSandboxPolicy::Enabled, cwd.as_path(), &None, false, diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 65b2a68073..7e49191067 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,3 +1,4 @@ +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; @@ -44,7 +45,7 @@ where args, arg0, cwd: command_cwd, - sandbox_policy, + network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), network, stdio_policy, env, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 10c5d71da2..2bb80b2464 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -13,6 +13,9 @@ use crate::exec::StdoutStream; use crate::exec::execute_exec_env; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args; +use crate::protocol::FileSystemSandboxKind; +use crate::protocol::FileSystemSandboxPolicy; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; @@ -60,6 +63,8 @@ pub struct ExecRequest { pub windows_sandbox_level: WindowsSandboxLevel, pub sandbox_permissions: SandboxPermissions, pub sandbox_policy: SandboxPolicy, + pub file_system_sandbox_policy: FileSystemSandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, pub justification: Option, pub arg0: Option, } @@ -70,6 +75,8 @@ pub struct ExecRequest { pub(crate) struct SandboxTransformRequest<'a> { pub spec: CommandSpec, 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> @@ -275,7 +282,7 @@ impl SandboxManager { pub(crate) fn select_initial( &self, - policy: &SandboxPolicy, + file_system_policy: &FileSystemSandboxPolicy, pref: SandboxablePreference, windows_sandbox_level: WindowsSandboxLevel, has_managed_network_requirements: bool, @@ -290,8 +297,8 @@ impl SandboxManager { ) .unwrap_or(SandboxType::None) } - SandboxablePreference::Auto => match policy { - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { + SandboxablePreference::Auto => match file_system_policy.kind { + FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { if has_managed_network_requirements { crate::safety::get_platform_sandbox( windows_sandbox_level != WindowsSandboxLevel::Disabled, @@ -316,6 +323,8 @@ impl SandboxManager { let SandboxTransformRequest { mut spec, policy, + file_system_policy, + network_policy, sandbox, enforce_managed_network, network, @@ -326,14 +335,45 @@ impl SandboxManager { use_linux_sandbox_bwrap, windows_sandbox_level, } = request; - let effective_policy = - if let Some(additional_permissions) = spec.additional_permissions.take() { - sandbox_policy_with_additional_permissions(policy, &additional_permissions)? + let effective_policy = if let Some(additional_permissions) = + spec.additional_permissions.take() + { + let (extra_reads, extra_writes) = additional_permission_roots(&additional_permissions); + let effective_policy = + sandbox_policy_with_additional_permissions(policy, &additional_permissions)?; + let file_system_sandbox_policy = if extra_reads.is_empty() && extra_writes.is_empty() { + file_system_policy.clone() } else { - policy.clone() + match file_system_policy.kind { + FileSystemSandboxKind::Restricted => { + FileSystemSandboxPolicy::from(&effective_policy) + } + FileSystemSandboxKind::Unrestricted + | FileSystemSandboxKind::ExternalSandbox => file_system_policy.clone(), + } }; + let network_sandbox_policy = if additional_permissions + .network + .as_ref() + .and_then(|network| network.enabled) + .unwrap_or(false) + { + NetworkSandboxPolicy::Enabled + } else { + network_policy + }; + ( + effective_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } else { + (policy.clone(), file_system_policy.clone(), network_policy) + }; + let (effective_policy, effective_file_system_policy, effective_network_policy) = + effective_policy; let mut env = spec.env; - if !effective_policy.has_full_network_access() { + if !effective_network_policy.is_enabled() { env.insert( CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(), "1".to_string(), @@ -407,6 +447,8 @@ impl SandboxManager { windows_sandbox_level, sandbox_permissions: spec.sandbox_permissions, sandbox_policy: effective_policy, + file_system_sandbox_policy: effective_file_system_policy, + network_sandbox_policy: effective_network_policy, justification: spec.justification, arg0: arg0_override, }) @@ -421,8 +463,7 @@ pub async fn execute_env( env: ExecRequest, stdout_stream: Option, ) -> crate::error::Result { - let effective_policy = env.sandbox_policy.clone(); - execute_exec_env(env, &effective_policy, stdout_stream).await + execute_exec_env(env, stdout_stream).await } #[cfg(test)] @@ -431,6 +472,13 @@ mod tests { use super::normalize_additional_permissions; use super::sandbox_policy_with_additional_permissions; use crate::exec::SandboxType; + use crate::protocol::FileSystemAccessMode; + use crate::protocol::FileSystemPath; + use crate::protocol::FileSystemSandboxEntry; + use crate::protocol::FileSystemSandboxPolicy; + use crate::protocol::FileSystemSpecialPath; + use crate::protocol::FileSystemSpecialPathKind; + use crate::protocol::NetworkSandboxPolicy; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; use crate::tools::sandboxing::SandboxablePreference; @@ -441,13 +489,14 @@ mod tests { use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; use pretty_assertions::assert_eq; + use std::collections::HashMap; use tempfile::TempDir; #[test] fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() { let manager = SandboxManager::new(); let sandbox = manager.select_initial( - &SandboxPolicy::DangerFullAccess, + &FileSystemSandboxPolicy::unrestricted(), SandboxablePreference::Auto, WindowsSandboxLevel::Disabled, false, @@ -460,7 +509,7 @@ mod tests { let manager = SandboxManager::new(); let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None); let sandbox = manager.select_initial( - &SandboxPolicy::DangerFullAccess, + &FileSystemSandboxPolicy::unrestricted(), SandboxablePreference::Auto, WindowsSandboxLevel::Disabled, true, @@ -468,6 +517,70 @@ mod tests { assert_eq!(sandbox, expected); } + #[test] + fn restricted_file_system_uses_platform_sandbox_without_managed_network() { + let manager = SandboxManager::new(); + let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None); + let sandbox = manager.select_initial( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath { + kind: FileSystemSpecialPathKind::Root, + subpath: None, + }, + }, + access: FileSystemAccessMode::Read, + }]), + SandboxablePreference::Auto, + WindowsSandboxLevel::Disabled, + false, + ); + assert_eq!(sandbox, expected); + } + + #[test] + fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() { + let manager = SandboxManager::new(); + let cwd = std::env::current_dir().expect("current dir"); + let exec_request = manager + .transform(super::SandboxTransformRequest { + spec: super::CommandSpec { + program: "true".to_string(), + args: Vec::new(), + cwd: cwd.clone(), + env: HashMap::new(), + expiration: crate::exec::ExecExpiration::DefaultTimeout, + sandbox_permissions: super::SandboxPermissions::UseDefault, + additional_permissions: None, + justification: None, + }, + policy: &SandboxPolicy::ExternalSandbox { + network_access: crate::protocol::NetworkAccess::Restricted, + }, + file_system_policy: &FileSystemSandboxPolicy::unrestricted(), + network_policy: NetworkSandboxPolicy::Restricted, + sandbox: SandboxType::None, + enforce_managed_network: false, + network: None, + sandbox_policy_cwd: cwd.as_path(), + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, + codex_linux_sandbox_exe: None, + use_linux_sandbox_bwrap: false, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }) + .expect("transform"); + + assert_eq!( + exec_request.file_system_sandbox_policy, + FileSystemSandboxPolicy::unrestricted() + ); + assert_eq!( + exec_request.network_sandbox_policy, + NetworkSandboxPolicy::Restricted + ); + } + #[test] fn normalize_additional_permissions_preserves_network() { let temp_dir = TempDir::new().expect("create temp dir"); diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index c9c2801209..f087253b2c 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -15,6 +15,7 @@ use tokio::process::Child; use tracing::warn; use url::Url; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions; use crate::seatbelt_permissions::build_seatbelt_extensions; @@ -51,7 +52,7 @@ pub async fn spawn_command_under_seatbelt( args, arg0, cwd: command_cwd, - sandbox_policy, + network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), network, stdio_policy, env, diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index 67e6ace044..480d1ea5ee 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -6,13 +6,13 @@ use tokio::process::Child; use tokio::process::Command; use tracing::trace; -use crate::protocol::SandboxPolicy; +use crate::protocol::NetworkSandboxPolicy; /// Experimental environment variable that will be set to some non-empty value /// if both of the following are true: /// /// 1. The process was spawned by Codex as part of a shell tool call. -/// 2. SandboxPolicy.has_full_network_access() was false for the tool call. +/// 2. NetworkSandboxPolicy is restricted for the tool call. /// /// We may try to have just one environment variable for all sandboxing /// attributes, so this may change in the future. @@ -33,15 +33,15 @@ pub enum StdioPolicy { /// ensuring the args and environment variables used to create the `Command` /// (and `Child`) honor the configuration. /// -/// For now, we take `SandboxPolicy` as a parameter to spawn_child() because -/// we need to determine whether to set the +/// For now, we take `NetworkSandboxPolicy` as a parameter to spawn_child() +/// because we need to determine whether to set the /// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable. pub(crate) struct SpawnChildRequest<'a> { pub program: PathBuf, pub args: Vec, pub arg0: Option<&'a str>, pub cwd: PathBuf, - pub sandbox_policy: &'a SandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, pub network: Option<&'a NetworkProxy>, pub stdio_policy: StdioPolicy, pub env: HashMap, @@ -53,14 +53,14 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io args, arg0, cwd, - sandbox_policy, + network_sandbox_policy, network, stdio_policy, mut env, } = request; trace!( - "spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {sandbox_policy:?} {stdio_policy:?} {env:?}" + "spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {network_sandbox_policy:?} {stdio_policy:?} {env:?}" ); let mut cmd = Command::new(&program); @@ -74,7 +74,7 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io cmd.env_clear(); cmd.envs(env); - if !sandbox_policy.has_full_network_access() { + if !network_sandbox_policy.is_enabled() { cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1"); } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 18dc5006ed..2e3acc7b84 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -22,6 +22,8 @@ use crate::protocol::ExecCommandBeginEvent; use crate::protocol::ExecCommandEndEvent; use crate::protocol::ExecCommandSource; use crate::protocol::ExecCommandStatus; +use crate::protocol::FileSystemSandboxPolicy; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::protocol::TurnStartedEvent; use crate::sandboxing::ExecRequest; @@ -163,6 +165,8 @@ pub(crate) async fn execute_user_shell_command( windows_sandbox_level: turn_context.windows_sandbox_level, sandbox_permissions: SandboxPermissions::UseDefault, sandbox_policy: sandbox_policy.clone(), + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), justification: None, arg0: None, }; @@ -173,7 +177,7 @@ pub(crate) async fn execute_user_shell_command( tx_event: session.get_tx_event(), }); - let exec_result = execute_exec_env(exec_env, &sandbox_policy, stdout_stream) + let exec_result = execute_exec_env(exec_env, stdout_stream) .or_cancel(&cancellation_token) .await; diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 86463e1b95..634d7c448a 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -852,7 +852,7 @@ impl JsReplManager { .network .is_some(); let sandbox_type = sandbox.select_initial( - &turn.sandbox_policy, + &turn.file_system_sandbox_policy, SandboxablePreference::Auto, turn.windows_sandbox_level, has_managed_network_requirements, @@ -861,6 +861,8 @@ impl JsReplManager { .transform(crate::sandboxing::SandboxTransformRequest { spec, policy: &turn.sandbox_policy, + file_system_policy: &turn.file_system_sandbox_policy, + network_policy: turn.network_sandbox_policy, sandbox: sandbox_type, enforce_managed_network: has_managed_network_requirements, network: None, diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f66c79bbb0..bb2d2d0720 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -169,7 +169,7 @@ impl ToolOrchestrator { let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( - &turn_ctx.sandbox_policy, + &turn_ctx.file_system_sandbox_policy, tool.sandbox_preference(), turn_ctx.windows_sandbox_level, has_managed_network_requirements, @@ -182,6 +182,8 @@ impl ToolOrchestrator { let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, policy: &turn_ctx.sandbox_policy, + file_system_policy: &turn_ctx.file_system_sandbox_policy, + network_policy: turn_ctx.network_sandbox_policy, enforce_managed_network: has_managed_network_requirements, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, @@ -296,6 +298,8 @@ impl ToolOrchestrator { let escalated_attempt = SandboxAttempt { sandbox: crate::exec::SandboxType::None, policy: &turn_ctx.sandbox_policy, + file_system_policy: &turn_ctx.file_system_sandbox_policy, + network_policy: turn_ctx.network_sandbox_policy, enforce_managed_network: has_managed_network_requirements, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 36f63be4fa..c966e39793 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -25,7 +25,9 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::NetworkPolicyRuleAction; +use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; @@ -92,6 +94,8 @@ pub(super) async fn try_run_zsh_fork( windows_sandbox_level, sandbox_permissions, sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, justification, arg0, } = sandbox_exec_request; @@ -107,6 +111,8 @@ pub(super) async fn try_run_zsh_fork( command, cwd: sandbox_cwd, sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, sandbox, env: sandbox_env, network: sandbox_network, @@ -629,6 +635,8 @@ struct CoreShellCommandExecutor { command: Vec, cwd: PathBuf, sandbox_policy: SandboxPolicy, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, sandbox: SandboxType, env: HashMap, network: Option, @@ -669,6 +677,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { windows_sandbox_level: self.windows_sandbox_level, sandbox_permissions: self.sandbox_permissions, sandbox_policy: self.sandbox_policy.clone(), + file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), + network_sandbox_policy: self.network_sandbox_policy, justification: self.justification.clone(), arg0: self.arg0.clone(), }, @@ -713,6 +723,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { workdir, env, &self.sandbox_policy, + &self.file_system_sandbox_policy, + self.network_sandbox_policy, None, self.macos_seatbelt_profile_extensions.as_ref(), )?, @@ -723,15 +735,23 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { workdir, env, &self.sandbox_policy, + &self.file_system_sandbox_policy, + self.network_sandbox_policy, Some(permission_profile), None, )?, EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => { + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from(&permissions.sandbox_policy); + let network_sandbox_policy = + NetworkSandboxPolicy::from(&permissions.sandbox_policy); self.prepare_sandboxed_exec( command, workdir, env, &permissions.sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, None, permissions.macos_seatbelt_profile_extensions.as_ref(), )? @@ -743,12 +763,15 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { } impl CoreShellCommandExecutor { + #[allow(clippy::too_many_arguments)] fn prepare_sandboxed_exec( &self, command: Vec, workdir: &AbsolutePathBuf, env: HashMap, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, additional_permissions: Option, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: Option< &MacOsSeatbeltProfileExtensions, @@ -762,7 +785,7 @@ impl CoreShellCommandExecutor { .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?; let sandbox_manager = crate::sandboxing::SandboxManager::new(); let sandbox = sandbox_manager.select_initial( - sandbox_policy, + file_system_sandbox_policy, SandboxablePreference::Auto, self.windows_sandbox_level, self.network.is_some(), @@ -784,6 +807,8 @@ impl CoreShellCommandExecutor { justification: self.justification.clone(), }, policy: sandbox_policy, + file_system_policy: file_system_sandbox_policy, + network_policy: network_sandbox_policy, sandbox, enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 329d0d3bb9..7c30b5d598 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -422,6 +422,10 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions network: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: FileSystemSandboxPolicy::from( + &SandboxPolicy::new_read_only_policy(), + ), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, windows_sandbox_level: WindowsSandboxLevel::Disabled, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, @@ -472,6 +476,8 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() network: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), + network_sandbox_policy: NetworkSandboxPolicy::Enabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 28d87b5bf3..5ef0297956 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -7,6 +7,8 @@ use crate::codex::Session; use crate::codex::TurnContext; use crate::error::CodexErr; +use crate::protocol::FileSystemSandboxPolicy; +use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::sandboxing::CommandSpec; use crate::sandboxing::SandboxManager; @@ -318,6 +320,8 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { pub(crate) struct SandboxAttempt<'a> { pub sandbox: crate::exec::SandboxType, pub policy: &'a crate::protocol::SandboxPolicy, + pub file_system_policy: &'a FileSystemSandboxPolicy, + pub network_policy: NetworkSandboxPolicy, pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, @@ -336,6 +340,8 @@ impl<'a> SandboxAttempt<'a> { .transform(crate::sandboxing::SandboxTransformRequest { spec, policy: self.policy, + file_system_policy: self.file_system_policy, + network_policy: self.network_policy, sandbox: self.sandbox, enforce_managed_network: self.enforce_managed_network, network, diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 6546600681..d809bd0ed3 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -10,6 +10,8 @@ use codex_core::exec::process_exec_tool_call; use codex_core::sandboxing::SandboxPermissions; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::protocol::FileSystemSandboxPolicy; +use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use tempfile::TempDir; @@ -45,7 +47,17 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result anyh Ok(()) } +#[tokio::test] +async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Result<()> { + let server = responses::start_mock_server().await; + let mut builder = core_test_support::test_codex::test_codex().with_config(|config| { + config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Restricted; + }); + let test = builder.build(&server).await?; + + #[cfg(windows)] + let command = r#"$val = $env:CODEX_SANDBOX_NETWORK_DISABLED; if ([string]::IsNullOrEmpty($val)) { $val = 'not-set' } ; [System.Console]::Write($val)"#.to_string(); + #[cfg(not(windows))] + let command = + r#"sh -c "printf '%s' \"${CODEX_SANDBOX_NETWORK_DISABLED:-not-set}\"""#.to_string(); + + test.codex + .submit(Op::RunUserShellCommand { command }) + .await?; + + let end_event = wait_for_event_match(&test.codex, |ev| match ev { + EventMsg::ExecCommandEnd(event) => Some(event.clone()), + _ => None, + }) + .await; + assert_eq!(end_event.exit_code, 0); + assert_eq!(end_event.stdout.trim(), "not-set"); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[cfg(not(target_os = "windows"))] // TODO: unignore on windows async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<()> { diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 362fcaf35f..b12170584c 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -9,6 +9,8 @@ use codex_core::exec::process_exec_tool_call; use codex_core::exec_env::create_env; use codex_core::sandboxing::SandboxPermissions; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::protocol::FileSystemSandboxPolicy; +use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -102,6 +104,8 @@ async fn run_cmd_result_with_writable_roots( process_exec_tool_call( params, &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::from(&sandbox_policy), sandbox_cwd.as_path(), &codex_linux_sandbox_exe, use_bwrap_sandbox, @@ -333,6 +337,8 @@ async fn assert_network_blocked(cmd: &[&str]) { let result = process_exec_tool_call( params, &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::from(&sandbox_policy), sandbox_cwd.as_path(), &codex_linux_sandbox_exe, false,