diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d0e10a84eaf..e069f9abb36 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1657,9 +1657,19 @@ impl CodexMessageProcessor { }; let requested_policy = 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::permissions::FileSystemSandboxPolicy::from(&policy); + let network_sandbox_policy = + codex_protocol::permissions::NetworkSandboxPolicy::from(&policy); + (policy, file_system_sandbox_policy, network_sandbox_policy) + } Err(err) => { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -1670,7 +1680,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(); @@ -1691,6 +1705,8 @@ impl CodexMessageProcessor { match codex_core::exec::build_exec_request( 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/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 85448ea9635..540298b0412 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -702,6 +702,8 @@ mod tests { use std::path::PathBuf; use codex_protocol::config_types::WindowsSandboxLevel; + use codex_protocol::permissions::FileSystemSandboxPolicy; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; @@ -719,6 +721,10 @@ mod tests { use crate::outgoing_message::OutgoingMessage; fn windows_sandbox_exec_request() -> ExecRequest { + let sandbox_policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + network_access: false, + }; ExecRequest { command: vec!["cmd".to_string()], cwd: PathBuf::from("."), @@ -728,10 +734,9 @@ mod tests { sandbox: SandboxType::WindowsRestrictedToken, windows_sandbox_level: WindowsSandboxLevel::Disabled, sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, - sandbox_policy: SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, - network_access: false, - }, + sandbox_policy: sandbox_policy.clone(), + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), justification: None, arg0: None, } @@ -821,6 +826,10 @@ mod tests { connection_id: ConnectionId(8), request_id: codex_app_server_protocol::RequestId::Integer(100), }; + let sandbox_policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + network_access: false, + }; manager .start(StartCommandExecParams { @@ -836,10 +845,9 @@ mod tests { sandbox: SandboxType::None, windows_sandbox_level: WindowsSandboxLevel::Disabled, sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, - sandbox_policy: SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, - network_access: false, - }, + sandbox_policy: sandbox_policy.clone(), + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), justification: None, arg0: None, }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4292552a318..87240d398c9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -87,6 +87,8 @@ use codex_protocol::models::BaseInstructions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ModelInfo; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::HasLegacyEvent; use codex_protocol::protocol::ItemCompletedEvent; @@ -488,6 +490,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(), @@ -683,6 +687,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, @@ -774,6 +780,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(), @@ -879,6 +887,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 @@ -945,6 +955,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; @@ -1158,6 +1172,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(), @@ -4986,6 +5002,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(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 7fe6076f721..b5f2ea2ef44 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1416,6 +1416,8 @@ async fn set_rate_limits_retains_previous_credits() { 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(), @@ -1510,6 +1512,8 @@ async fn set_rate_limits_updates_plan_type_when_present() { 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(), @@ -1862,6 +1866,8 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati 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(), @@ -1919,6 +1925,8 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { 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(), @@ -2009,6 +2017,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { 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(), @@ -2414,6 +2424,8 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( 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(), @@ -3841,11 +3853,15 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { // Now retry the same command WITHOUT escalated permissions; should succeed. // Force DangerFullAccess to avoid platform sandbox dependencies in tests. - Arc::get_mut(&mut turn_context) - .expect("unique turn context Arc") + let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc"); + turn_context_mut .sandbox_policy .set(SandboxPolicy::DangerFullAccess) .expect("test setup should allow updating sandbox policy"); + turn_context_mut.file_system_sandbox_policy = + FileSystemSandboxPolicy::from(turn_context_mut.sandbox_policy.get()); + turn_context_mut.network_sandbox_policy = + NetworkSandboxPolicy::from(turn_context_mut.sandbox_policy.get()); let resp2 = handler .handle(ToolInvocation { diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index b88b4983495..7ea18afc210 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -19,7 +19,6 @@ use tokio_util::sync::CancellationToken; use crate::error::CodexErr; use crate::error::Result; use crate::error::SandboxErr; -use crate::get_platform_sandbox; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecCommandOutputDeltaEvent; @@ -33,7 +32,11 @@ use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use crate::text_encoding::bytes_to_string_smart; +use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; +use codex_protocol::permissions::FileSystemSandboxKind; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use codex_utils_pty::process_group::kill_child_process_group; @@ -82,6 +85,21 @@ pub struct ExecParams { pub arg0: Option, } +fn select_process_exec_tool_sandbox_type( + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, + enforce_managed_network: bool, +) -> SandboxType { + SandboxManager::new().select_initial( + file_system_sandbox_policy, + network_sandbox_policy, + SandboxablePreference::Auto, + windows_sandbox_level, + enforce_managed_network, + ) +} + /// Mechanism to terminate an exec invocation before it finishes naturally. #[derive(Clone, Debug)] pub enum ExecExpiration { @@ -159,9 +177,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, @@ -170,6 +191,8 @@ pub async fn process_exec_tool_call( let exec_req = build_exec_request( params, sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, sandbox_cwd, codex_linux_sandbox_exe, use_linux_sandbox_bwrap, @@ -184,29 +207,20 @@ pub async fn process_exec_tool_call( pub fn build_exec_request( 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, ) -> 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 { .. } => { - if enforce_managed_network { - get_platform_sandbox( - windows_sandbox_level - != codex_protocol::config_types::WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None) - } else { - SandboxType::None - } - } - _ => get_platform_sandbox( - windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None), - }; + let sandbox_type = select_process_exec_tool_sandbox_type( + file_system_sandbox_policy, + network_sandbox_policy, + windows_sandbox_level, + enforce_managed_network, + ); tracing::debug!("Sandbox type: {sandbox_type:?}"); let ExecParams { @@ -246,6 +260,8 @@ pub fn build_exec_request( .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(), @@ -276,9 +292,12 @@ pub(crate) async fn execute_exec_request( windows_sandbox_level, sandbox_permissions, sandbox_policy: _sandbox_policy_from_env, + file_system_sandbox_policy, + network_sandbox_policy, justification, arg0, } = exec_request; + let _ = _sandbox_policy_from_env; let params = ExecParams { command, @@ -293,7 +312,16 @@ pub(crate) async fn execute_exec_request( }; let start = Instant::now(); - let raw_output_result = exec(params, sandbox, sandbox_policy, stdout_stream, after_spawn).await; + let raw_output_result = exec( + params, + sandbox, + sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, + stdout_stream, + after_spawn, + ) + .await; let duration = start.elapsed(); finalize_exec_result(raw_output_result, sandbox, duration) } @@ -722,16 +750,21 @@ async fn exec( params: ExecParams, sandbox: SandboxType, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, stdout_stream: Option, after_spawn: Option>, ) -> Result { #[cfg(target_os = "windows")] - if sandbox == SandboxType::WindowsRestrictedToken - && !matches!( + if sandbox == SandboxType::WindowsRestrictedToken { + if let Some(reason) = unsupported_windows_restricted_token_sandbox_reason( + sandbox, sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) - { + file_system_sandbox_policy, + network_sandbox_policy, + ) { + return Err(CodexErr::Io(io::Error::other(reason))); + } return exec_windows_sandbox(params, sandbox_policy).await; } let ExecParams { @@ -760,7 +793,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. @@ -775,6 +808,43 @@ async fn exec( consume_truncated_output(child, expiration, stdout_stream).await } +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn should_use_windows_restricted_token_sandbox( + sandbox: SandboxType, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> bool { + sandbox == SandboxType::WindowsRestrictedToken + && file_system_sandbox_policy.kind == FileSystemSandboxKind::Restricted + && !matches!( + sandbox_policy, + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + ) +} + +#[cfg(any(target_os = "windows", test))] +fn unsupported_windows_restricted_token_sandbox_reason( + sandbox: SandboxType, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, +) -> Option { + if should_use_windows_restricted_token_sandbox( + sandbox, + sandbox_policy, + file_system_sandbox_policy, + ) { + return None; + } + + (sandbox == SandboxType::WindowsRestrictedToken).then(|| { + format!( + "windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed", + file_system_sandbox_policy.kind, + ) + }) +} + /// Consumes the output of a child process, truncating it so it is suitable for /// use as the output of a `shell` tool call. Also enforces specified timeout. async fn consume_truncated_output( @@ -1117,6 +1187,111 @@ mod tests { assert_eq!(aggregated.truncated_after_lines, None); } + #[test] + fn windows_restricted_token_skips_external_sandbox_policies() { + let policy = SandboxPolicy::ExternalSandbox { + network_access: codex_protocol::protocol::NetworkAccess::Restricted, + }; + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + + assert_eq!( + should_use_windows_restricted_token_sandbox( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + ), + false + ); + } + + #[test] + fn windows_restricted_token_runs_for_legacy_restricted_policies() { + let policy = SandboxPolicy::new_read_only_policy(); + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + + assert_eq!( + should_use_windows_restricted_token_sandbox( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + ), + true + ); + } + + #[test] + fn windows_restricted_token_rejects_network_only_restrictions() { + let policy = SandboxPolicy::ExternalSandbox { + network_access: codex_protocol::protocol::NetworkAccess::Restricted, + }; + let file_system_policy = FileSystemSandboxPolicy::unrestricted(); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ), + Some( + "windows sandbox backend cannot enforce file_system=Unrestricted, network=Restricted, legacy_policy=ExternalSandbox { network_access: Restricted }; refusing to run unsandboxed".to_string() + ) + ); + } + + #[test] + fn windows_restricted_token_allows_legacy_restricted_policies() { + let policy = SandboxPolicy::new_read_only_policy(); + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![]); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ), + None + ); + } + + #[test] + fn windows_restricted_token_allows_legacy_workspace_write_policies() { + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + let file_system_policy = FileSystemSandboxPolicy::from(&policy); + + assert_eq!( + unsupported_windows_restricted_token_sandbox_reason( + SandboxType::WindowsRestrictedToken, + &policy, + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ), + None + ); + } + + #[test] + fn process_exec_tool_call_uses_platform_sandbox_for_network_only_restrictions() { + let expected = crate::get_platform_sandbox(false).unwrap_or(SandboxType::None); + + assert_eq!( + select_process_exec_tool_sandbox_type( + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + codex_protocol::config_types::WindowsSandboxLevel::Disabled, + false, + ), + expected + ); + } + #[cfg(unix)] #[test] fn sandbox_detection_flags_sigsys_exit_code() { @@ -1159,6 +1334,8 @@ mod tests { params, SandboxType::None, &SandboxPolicy::new_read_only_policy(), + &FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()), + NetworkSandboxPolicy::Restricted, None, None, ) @@ -1215,6 +1392,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 65b2a68073d..fdb8a52af1d 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -3,6 +3,7 @@ use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use codex_network_proxy::NetworkProxy; +use codex_protocol::permissions::NetworkSandboxPolicy; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -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 9258889c79d..7c204f4362c 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -30,6 +30,14 @@ use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::PermissionProfile; pub use codex_protocol::models::SandboxPermissions; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxKind; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::ReadOnlyAccess; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; @@ -62,6 +70,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, } @@ -72,6 +82,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> @@ -203,6 +215,41 @@ fn additional_permission_roots( ) } +#[cfg_attr(not(test), allow(dead_code))] +fn merge_file_system_policy_with_additional_permissions( + file_system_policy: &FileSystemSandboxPolicy, + extra_reads: Vec, + extra_writes: Vec, +) -> FileSystemSandboxPolicy { + match file_system_policy.kind { + FileSystemSandboxKind::Restricted => { + let mut merged_policy = file_system_policy.clone(); + for path in extra_reads { + let entry = FileSystemSandboxEntry { + path: FileSystemPath::Path { path }, + access: FileSystemAccessMode::Read, + }; + if !merged_policy.entries.contains(&entry) { + merged_policy.entries.push(entry); + } + } + for path in extra_writes { + let entry = FileSystemSandboxEntry { + path: FileSystemPath::Path { path }, + access: FileSystemAccessMode::Write, + }; + if !merged_policy.entries.contains(&entry) { + merged_policy.entries.push(entry); + } + } + merged_policy + } + FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { + file_system_policy.clone() + } + } +} + fn merge_read_only_access_with_additional_reads( read_only_access: &ReadOnlyAccess, extra_reads: Vec, @@ -246,9 +293,17 @@ fn sandbox_policy_with_additional_permissions( let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions); match sandbox_policy { - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - sandbox_policy.clone() - } + SandboxPolicy::DangerFullAccess => SandboxPolicy::DangerFullAccess, + SandboxPolicy::ExternalSandbox { network_access } => SandboxPolicy::ExternalSandbox { + network_access: if merge_network_access( + network_access.is_enabled(), + additional_permissions, + ) { + NetworkAccess::Enabled + } else { + NetworkAccess::Restricted + }, + }, SandboxPolicy::WorkspaceWrite { writable_roots, read_only_access, @@ -297,6 +352,35 @@ fn sandbox_policy_with_additional_permissions( } } +pub(crate) 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.entries.iter().any(|entry| { + entry.access == FileSystemAccessMode::Write + && matches!( + &entry.path, + FileSystemPath::Special { value } + if matches!(value, FileSystemSpecialPath::Root) + ) + }), + FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => false, + } +} + #[derive(Default)] pub struct SandboxManager; @@ -307,7 +391,8 @@ impl SandboxManager { pub(crate) fn select_initial( &self, - policy: &SandboxPolicy, + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, pref: SandboxablePreference, windows_sandbox_level: WindowsSandboxLevel, has_managed_network_requirements: bool, @@ -322,22 +407,20 @@ impl SandboxManager { ) .unwrap_or(SandboxType::None) } - SandboxablePreference::Auto => match policy { - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - if has_managed_network_requirements { - crate::safety::get_platform_sandbox( - windows_sandbox_level != WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None) - } else { - SandboxType::None - } + SandboxablePreference::Auto => { + if should_require_platform_sandbox( + file_system_policy, + network_policy, + has_managed_network_requirements, + ) { + crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None) + } else { + SandboxType::None } - _ => crate::safety::get_platform_sandbox( - windows_sandbox_level != WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None), - }, + } } } @@ -348,6 +431,8 @@ impl SandboxManager { let SandboxTransformRequest { mut spec, policy, + file_system_policy, + network_policy, sandbox, enforce_managed_network, network, @@ -360,16 +445,41 @@ impl SandboxManager { } = request; #[cfg(not(target_os = "macos"))] let macos_seatbelt_profile_extensions = None; - let effective_permissions = EffectiveSandboxPermissions::new( + let additional_permissions = spec.additional_permissions.take(); + let EffectiveSandboxPermissions { + sandbox_policy: effective_policy, + macos_seatbelt_profile_extensions: _effective_macos_seatbelt_profile_extensions, + } = EffectiveSandboxPermissions::new( policy, macos_seatbelt_profile_extensions, - spec.additional_permissions.as_ref(), + additional_permissions.as_ref(), ); + let (effective_file_system_policy, effective_network_policy) = + if let Some(additional_permissions) = additional_permissions { + let (extra_reads, extra_writes) = + additional_permission_roots(&additional_permissions); + let file_system_sandbox_policy = + if extra_reads.is_empty() && extra_writes.is_empty() { + file_system_policy.clone() + } else { + merge_file_system_policy_with_additional_permissions( + file_system_policy, + extra_reads, + extra_writes, + ) + }; + let network_sandbox_policy = + if merge_network_access(network_policy.is_enabled(), &additional_permissions) { + NetworkSandboxPolicy::Enabled + } else { + NetworkSandboxPolicy::Restricted + }; + (file_system_sandbox_policy, network_sandbox_policy) + } else { + (file_system_policy.clone(), network_policy) + }; let mut env = spec.env; - if !effective_permissions - .sandbox_policy - .has_full_network_access() - { + if !effective_network_policy.is_enabled() { env.insert( CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(), "1".to_string(), @@ -388,13 +498,11 @@ impl SandboxManager { seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); let mut args = create_seatbelt_command_args_with_extensions( command.clone(), - &effective_permissions.sandbox_policy, + &effective_policy, sandbox_policy_cwd, enforce_managed_network, network, - effective_permissions - .macos_seatbelt_profile_extensions - .as_ref(), + _effective_macos_seatbelt_profile_extensions.as_ref(), ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); @@ -409,7 +517,7 @@ impl SandboxManager { let allow_proxy_network = allow_network_for_proxy(enforce_managed_network); let mut args = create_linux_sandbox_command_args( command.clone(), - &effective_permissions.sandbox_policy, + &effective_policy, sandbox_policy_cwd, use_linux_sandbox_bwrap, allow_proxy_network, @@ -444,7 +552,9 @@ impl SandboxManager { sandbox, windows_sandbox_level, sandbox_permissions: spec.sandbox_permissions, - sandbox_policy: effective_permissions.sandbox_policy, + sandbox_policy: effective_policy, + file_system_sandbox_policy: effective_file_system_policy, + network_sandbox_policy: effective_network_policy, justification: spec.justification, arg0: arg0_override, }) @@ -477,9 +587,12 @@ mod tests { #[cfg(target_os = "macos")] use super::EffectiveSandboxPermissions; use super::SandboxManager; + use super::merge_file_system_policy_with_additional_permissions; use super::normalize_additional_permissions; use super::sandbox_policy_with_additional_permissions; + use super::should_require_platform_sandbox; use crate::exec::SandboxType; + use crate::protocol::NetworkAccess; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; use crate::tools::sandboxing::SandboxablePreference; @@ -493,16 +606,24 @@ mod tests { use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSandboxPolicy; + use codex_protocol::permissions::FileSystemSpecialPath; + use codex_protocol::permissions::NetworkSandboxPolicy; 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(), + NetworkSandboxPolicy::Enabled, SandboxablePreference::Auto, WindowsSandboxLevel::Disabled, false, @@ -515,7 +636,8 @@ 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(), + NetworkSandboxPolicy::Enabled, SandboxablePreference::Auto, WindowsSandboxLevel::Disabled, true, @@ -523,6 +645,98 @@ 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::Root, + }, + access: FileSystemAccessMode::Read, + }]), + NetworkSandboxPolicy::Enabled, + SandboxablePreference::Auto, + WindowsSandboxLevel::Disabled, + false, + ); + assert_eq!(sandbox, expected); + } + + #[test] + fn full_access_restricted_policy_skips_platform_sandbox_when_network_is_enabled() { + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]); + + assert_eq!( + should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Enabled, false), + false + ); + } + + #[test] + fn full_access_restricted_policy_still_uses_platform_sandbox_for_restricted_network() { + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]); + + assert_eq!( + should_require_platform_sandbox(&policy, NetworkSandboxPolicy::Restricted, false), + true + ); + } + + #[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"); @@ -624,7 +838,6 @@ mod tests { } ); } - #[cfg(target_os = "macos")] #[test] fn effective_permissions_merge_macos_extensions_with_additional_permissions() { @@ -679,4 +892,223 @@ mod tests { }) ); } + + #[test] + fn external_sandbox_additional_permissions_can_enable_network() { + let temp_dir = TempDir::new().expect("create temp dir"); + let path = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp dir"); + let policy = sandbox_policy_with_additional_permissions( + &SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Restricted, + }, + &PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + file_system: Some(FileSystemPermissions { + read: Some(vec![path]), + write: Some(Vec::new()), + }), + ..Default::default() + }, + ); + + assert_eq!( + policy, + SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Enabled, + } + ); + } + + #[test] + fn transform_additional_permissions_enable_network_for_external_sandbox() { + let manager = SandboxManager::new(); + let cwd = std::env::current_dir().expect("current dir"); + let temp_dir = TempDir::new().expect("create temp dir"); + let path = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp 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::WithAdditionalPermissions, + additional_permissions: Some(PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + file_system: Some(FileSystemPermissions { + read: Some(vec![path]), + write: Some(Vec::new()), + }), + ..Default::default() + }), + justification: None, + }, + policy: &SandboxPolicy::ExternalSandbox { + network_access: 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.sandbox_policy, + SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Enabled, + } + ); + assert_eq!( + exec_request.network_sandbox_policy, + NetworkSandboxPolicy::Enabled + ); + } + + #[test] + fn transform_additional_permissions_preserves_denied_entries() { + let manager = SandboxManager::new(); + let cwd = std::env::current_dir().expect("current dir"); + let temp_dir = TempDir::new().expect("create temp dir"); + let workspace_root = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp dir"); + let allowed_path = workspace_root.join("allowed").expect("allowed path"); + let denied_path = workspace_root.join("denied").expect("denied path"); + 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::WithAdditionalPermissions, + additional_permissions: Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![allowed_path.clone()]), + }), + ..Default::default() + }), + justification: None, + }, + policy: &SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + network_access: false, + }, + file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: denied_path.clone(), + }, + access: FileSystemAccessMode::None, + }, + ]), + 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::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: denied_path }, + access: FileSystemAccessMode::None, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: allowed_path }, + access: FileSystemAccessMode::Write, + }, + ]) + ); + assert_eq!( + exec_request.network_sandbox_policy, + NetworkSandboxPolicy::Restricted + ); + } + + #[test] + fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roots() { + let temp_dir = TempDir::new().expect("create temp dir"); + let cwd = AbsolutePathBuf::from_absolute_path( + canonicalize(temp_dir.path()).expect("canonicalize temp dir"), + ) + .expect("absolute temp dir"); + let allowed_path = cwd.join("allowed").expect("allowed path"); + let denied_path = cwd.join("denied").expect("denied path"); + let merged_policy = merge_file_system_policy_with_additional_permissions( + &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: denied_path.clone(), + }, + access: FileSystemAccessMode::None, + }, + ]), + vec![allowed_path.clone()], + Vec::new(), + ); + + assert_eq!( + merged_policy.entries.contains(&FileSystemSandboxEntry { + path: FileSystemPath::Path { path: denied_path }, + access: FileSystemAccessMode::None, + }), + true + ); + assert_eq!( + merged_policy.entries.contains(&FileSystemSandboxEntry { + path: FileSystemPath::Path { path: allowed_path }, + access: FileSystemAccessMode::Read, + }), + true + ); + } } diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 8d556a8eec5..760c6bd8ae9 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -22,6 +22,7 @@ use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; +use codex_protocol::permissions::NetworkSandboxPolicy; const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl"); const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl"); @@ -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 67e6ace0447..575a2f777cd 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 codex_protocol::permissions::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 a6a1301cd78..9357f154fca 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -36,6 +36,8 @@ use super::SessionTaskContext; use crate::codex::Session; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; const USER_SHELL_TIMEOUT_MS: u64 = 60 * 60 * 1000; // 1 hour @@ -167,6 +169,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, }; diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 42016ff9bf9..f9cc3d854b0 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -852,7 +852,8 @@ impl JsReplManager { .network .is_some(); let sandbox_type = sandbox.select_initial( - &turn.sandbox_policy, + &turn.file_system_sandbox_policy, + turn.network_sandbox_policy, SandboxablePreference::Auto, turn.windows_sandbox_level, has_managed_network_requirements, @@ -861,6 +862,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 e78f0bcfce9..d92ed5f6211 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -169,7 +169,8 @@ 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, + turn_ctx.network_sandbox_policy, tool.sandbox_preference(), turn_ctx.windows_sandbox_level, has_managed_network_requirements, @@ -182,6 +183,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 +299,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 28f4a65beac..99d7c14761a 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -24,6 +24,8 @@ use codex_execpolicy::RuleMatch; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::RejectConfig; @@ -98,6 +100,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; @@ -113,6 +117,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, @@ -158,7 +164,9 @@ pub(super) async fn try_run_zsh_fork( turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), approval_policy: ctx.turn.approval_policy.value(), - sandbox_policy: attempt.policy.clone(), + sandbox_policy: command_executor.sandbox_policy.clone(), + file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(), + network_sandbox_policy: command_executor.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, prompt_permissions: req.additional_permissions.clone(), stopwatch: stopwatch.clone(), @@ -180,7 +188,7 @@ pub(super) async fn try_run_zsh_fork( pub(crate) async fn prepare_unified_exec_zsh_fork( req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest, - attempt: &SandboxAttempt<'_>, + _attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, exec_request: ExecRequest, ) -> Result, ToolError> { @@ -220,6 +228,8 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( command: exec_request.command.clone(), cwd: exec_request.cwd.clone(), sandbox_policy: exec_request.sandbox_policy.clone(), + file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), + network_sandbox_policy: exec_request.network_sandbox_policy, sandbox: exec_request.sandbox, env: exec_request.env.clone(), network: exec_request.network.clone(), @@ -253,7 +263,9 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), approval_policy: ctx.turn.approval_policy.value(), - sandbox_policy: attempt.policy.clone(), + sandbox_policy: exec_request.sandbox_policy.clone(), + file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), + network_sandbox_policy: exec_request.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, prompt_permissions: req.additional_permissions.clone(), stopwatch: Stopwatch::unlimited(), @@ -282,6 +294,8 @@ struct CoreShellActionProvider { call_id: String, approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, sandbox_permissions: SandboxPermissions, prompt_permissions: Option, stopwatch: Stopwatch, @@ -308,6 +322,8 @@ impl CoreShellActionProvider { fn shell_request_escalation_execution( sandbox_permissions: SandboxPermissions, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, additional_permissions: Option<&PermissionProfile>, macos_seatbelt_profile_extensions: Option<&MacOsSeatbeltProfileExtensions>, ) -> EscalationExecution { @@ -321,6 +337,8 @@ impl CoreShellActionProvider { EscalationExecution::Permissions(EscalationPermissions::Permissions( EscalatedPermissions { sandbox_policy: sandbox_policy.clone(), + file_system_sandbox_policy: file_system_sandbox_policy.clone(), + network_sandbox_policy, macos_seatbelt_profile_extensions: macos_seatbelt_profile_extensions .cloned(), }, @@ -634,6 +652,8 @@ impl EscalationPolicy for CoreShellActionProvider { DecisionSource::UnmatchedCommandFallback => Self::shell_request_escalation_execution( self.sandbox_permissions, &self.sandbox_policy, + &self.file_system_sandbox_policy, + self.network_sandbox_policy, self.prompt_permissions.as_ref(), self.turn .config @@ -741,6 +761,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, @@ -760,6 +782,8 @@ struct PrepareSandboxedExecParams<'a> { workdir: &'a AbsolutePathBuf, env: HashMap, sandbox_policy: &'a SandboxPolicy, + file_system_sandbox_policy: &'a FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, additional_permissions: Option, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, @@ -795,6 +819,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(), }, @@ -841,6 +867,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { workdir, env, sandbox_policy: &self.sandbox_policy, + file_system_sandbox_policy: &self.file_system_sandbox_policy, + network_sandbox_policy: self.network_sandbox_policy, additional_permissions: None, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: self @@ -858,6 +886,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { workdir, env, sandbox_policy: &self.sandbox_policy, + file_system_sandbox_policy: &self.file_system_sandbox_policy, + network_sandbox_policy: self.network_sandbox_policy, additional_permissions: Some(permission_profile), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: self @@ -872,6 +902,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { workdir, env, sandbox_policy: &permissions.sandbox_policy, + file_system_sandbox_policy: &permissions.file_system_sandbox_policy, + network_sandbox_policy: permissions.network_sandbox_policy, additional_permissions: None, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: permissions @@ -886,6 +918,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { } impl CoreShellCommandExecutor { + #[allow(clippy::too_many_arguments)] fn prepare_sandboxed_exec( &self, params: PrepareSandboxedExecParams<'_>, @@ -895,6 +928,8 @@ impl CoreShellCommandExecutor { workdir, env, sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, additional_permissions, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, @@ -904,7 +939,8 @@ 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, + network_sandbox_policy, SandboxablePreference::Auto, self.windows_sandbox_level, self.network.is_some(), @@ -926,6 +962,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 5d7b0f5e04f..af71bd5e4ea 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 @@ -31,6 +31,11 @@ use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::MacOsPreferencesPermission; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SkillScope; use codex_shell_escalation::EscalationExecution; use codex_shell_escalation::EscalationPermissions; @@ -223,6 +228,21 @@ fn shell_request_escalation_execution_is_explicit() { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; + let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap(), + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: AbsolutePathBuf::from_absolute_path("/tmp/secret").unwrap(), + }, + access: FileSystemAccessMode::None, + }, + ]); + let network_sandbox_policy = NetworkSandboxPolicy::Restricted; let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::ReadWrite, ..Default::default() @@ -232,6 +252,8 @@ fn shell_request_escalation_execution_is_explicit() { CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::UseDefault, &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, None, Some(&macos_seatbelt_profile_extensions), ), @@ -241,6 +263,8 @@ fn shell_request_escalation_execution_is_explicit() { CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::RequireEscalated, &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, None, Some(&macos_seatbelt_profile_extensions), ), @@ -250,12 +274,16 @@ fn shell_request_escalation_execution_is_explicit() { CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::WithAdditionalPermissions, &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, Some(&requested_permissions), Some(&macos_seatbelt_profile_extensions), ), EscalationExecution::Permissions(EscalationPermissions::Permissions( EscalatedPermissions { sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions), }, )), @@ -474,6 +502,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, @@ -524,6 +556,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, @@ -560,6 +594,8 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() EscalationExecution::Permissions(EscalationPermissions::Permissions( EscalatedPermissions { sandbox_policy: permissions.sandbox_policy.get().clone(), + file_system_sandbox_policy: permissions.file_system_sandbox_policy.clone(), + network_sandbox_policy: permissions.network_sandbox_policy, macos_seatbelt_profile_extensions: permissions .macos_seatbelt_profile_extensions .clone(), @@ -588,13 +624,16 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() #[tokio::test] async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_macos_extensions() { let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap(); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); let executor = CoreShellCommandExecutor { command: vec!["echo".to_string(), "ok".to_string()], cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, sandbox: SandboxType::None, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + sandbox_policy: sandbox_policy.clone(), + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), 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 df8f7c152b6..b06b3295405 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -17,6 +17,8 @@ use crate::tools::network_approval::NetworkApprovalSpec; use codex_network_proxy::NetworkProxy; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; use futures::Future; @@ -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/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 6323857150e..2e46d6a9432 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -205,6 +205,10 @@ mod tests { turn.sandbox_policy .set(SandboxPolicy::DangerFullAccess) .expect("test setup should allow updating sandbox policy"); + turn.file_system_sandbox_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from(turn.sandbox_policy.get()); + turn.network_sandbox_policy = + codex_protocol::permissions::NetworkSandboxPolicy::from(turn.sandbox_policy.get()); (Arc::new(session), Arc::new(turn)) } diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 65466006817..e00ec963df1 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::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::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 362fcaf35fd..76021081454 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::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::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, diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 680462b720d..4116777afb8 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -5,6 +5,8 @@ use crate::mcp::RequestId; use crate::models::MacOsSeatbeltProfileExtensions; use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; +use crate::permissions::FileSystemSandboxPolicy; +use crate::permissions::NetworkSandboxPolicy; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; @@ -17,6 +19,8 @@ use ts_rs::TS; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Permissions { pub sandbox_policy: SandboxPolicy, + pub file_system_sandbox_policy: FileSystemSandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, pub macos_seatbelt_profile_extensions: Option, }