From 6c4fe1f1f333e561d6e18f87dd2afa419682066c Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Thu, 20 Nov 2025 16:12:33 -0800 Subject: [PATCH 1/9] refactor sandboxing (1/?) --- .../app-server/src/codex_message_processor.rs | 14 +- codex-rs/cli/src/debug_sandbox.rs | 212 ++++++++---------- codex-rs/core/src/config/mod.rs | 9 +- codex-rs/core/src/exec.rs | 88 +++----- codex-rs/core/src/lib.rs | 8 +- codex-rs/core/src/safety.rs | 48 ++-- .../src/{ => sandboxing/linux}/landlock.rs | 0 codex-rs/core/src/sandboxing/linux/mod.rs | 3 + codex-rs/core/src/sandboxing/mac/mod.rs | 3 + .../core/src/{ => sandboxing/mac}/seatbelt.rs | 6 +- codex-rs/core/src/sandboxing/mod.rs | 164 ++++++++------ codex-rs/core/src/tasks/user_shell.rs | 3 +- codex-rs/core/src/tools/orchestrator.rs | 8 +- codex-rs/core/src/tools/sandboxing.rs | 4 +- codex-rs/core/src/unified_exec/mod.rs | 2 +- codex-rs/core/src/unified_exec/session.rs | 19 +- .../core/src/unified_exec/session_manager.rs | 2 +- codex-rs/core/tests/suite/exec.rs | 9 +- .../exec-server/src/posix/escalate_server.rs | 4 +- .../linux-sandbox/tests/suite/landlock.rs | 5 +- codex-rs/tui/src/app.rs | 6 +- codex-rs/tui/src/chatwidget.rs | 8 +- codex-rs/tui/src/lib.rs | 4 +- 23 files changed, 302 insertions(+), 327 deletions(-) rename codex-rs/core/src/{ => sandboxing/linux}/landlock.rs (100%) create mode 100644 codex-rs/core/src/sandboxing/linux/mod.rs create mode 100644 codex-rs/core/src/sandboxing/mac/mod.rs rename codex-rs/core/src/{ => sandboxing/mac}/seatbelt.rs (98%) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index bf9f0b9403..a8e6ba66f7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -115,7 +115,6 @@ use codex_core::exec::ExecParams; use codex_core::exec_env::create_env; use codex_core::features::Feature; use codex_core::find_conversation_path_by_id_str; -use codex_core::get_platform_sandbox; use codex_core::git_info::git_diff_to_remote; use codex_core::parse_cursor; use codex_core::protocol::EventMsg; @@ -1181,13 +1180,10 @@ impl CodexMessageProcessor { .sandbox_policy .unwrap_or_else(|| self.config.sandbox_policy.clone()); - let sandbox_type = match &effective_policy { - codex_core::protocol::SandboxPolicy::DangerFullAccess => { - codex_core::exec::SandboxType::None - } - _ => get_platform_sandbox().unwrap_or(codex_core::exec::SandboxType::None), - }; - tracing::debug!("Sandbox type: {sandbox_type:?}"); + let sandboxed = !matches!( + &effective_policy, + codex_core::protocol::SandboxPolicy::DangerFullAccess + ); let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone(); let outgoing = self.outgoing.clone(); let req_id = request_id; @@ -1196,7 +1192,7 @@ impl CodexMessageProcessor { tokio::spawn(async move { match codex_core::exec::process_exec_tool_call( exec_params, - sandbox_type, + sandboxed, &effective_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 26fecd55c6..372c410232 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -9,12 +9,14 @@ use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::exec_env::create_env; -use codex_core::landlock::spawn_command_under_linux_sandbox; #[cfg(target_os = "macos")] use codex_core::seatbelt::spawn_command_under_seatbelt; use codex_core::spawn::StdioPolicy; use codex_protocol::config_types::SandboxMode; +#[cfg(target_os = "linux")] +use codex_core::landlock::spawn_command_under_linux_sandbox; + use crate::LandlockCommand; use crate::SeatbeltCommand; use crate::WindowsCommand; @@ -39,7 +41,6 @@ pub async fn run_command_under_seatbelt( command, config_overrides, codex_linux_sandbox_exe, - SandboxType::Seatbelt, log_denials, ) .await @@ -53,6 +54,7 @@ pub async fn run_command_under_seatbelt( anyhow::bail!("Seatbelt sandbox is only available on macOS"); } +#[cfg(target_os = "linux")] pub async fn run_command_under_landlock( command: LandlockCommand, codex_linux_sandbox_exe: Option, @@ -67,12 +69,19 @@ pub async fn run_command_under_landlock( command, config_overrides, codex_linux_sandbox_exe, - SandboxType::Landlock, false, ) .await } +#[cfg(not(target_os = "linux"))] +pub async fn run_command_under_landlock( + _command: LandlockCommand, + _codex_linux_sandbox_exe: Option, +) -> anyhow::Result<()> { + anyhow::bail!("Landlock sandbox is only available on Linux"); +} + pub async fn run_command_under_windows( command: WindowsCommand, codex_linux_sandbox_exe: Option, @@ -87,25 +96,16 @@ pub async fn run_command_under_windows( command, config_overrides, codex_linux_sandbox_exe, - SandboxType::Windows, false, ) .await } -enum SandboxType { - #[cfg(target_os = "macos")] - Seatbelt, - Landlock, - Windows, -} - async fn run_command_under_sandbox( full_auto: bool, command: Vec, config_overrides: CliConfigOverrides, codex_linux_sandbox_exe: Option, - sandbox_type: SandboxType, log_denials: bool, ) -> anyhow::Result<()> { let sandbox_mode = create_sandbox_mode(full_auto); @@ -133,120 +133,106 @@ async fn run_command_under_sandbox( let env = create_env(&config.shell_environment_policy); // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio. - if let SandboxType::Windows = sandbox_type { - #[cfg(target_os = "windows")] - { - use codex_windows_sandbox::run_windows_sandbox_capture; - - let policy_str = serde_json::to_string(&config.sandbox_policy)?; - - let sandbox_cwd = sandbox_policy_cwd.clone(); - let cwd_clone = cwd.clone(); - let env_map = env.clone(); - let command_vec = command.clone(); - let base_dir = config.codex_home.clone(); - - // Preflight audit is invoked elsewhere at the appropriate times. - let res = tokio::task::spawn_blocking(move || { - run_windows_sandbox_capture( - policy_str.as_str(), - &sandbox_cwd, - base_dir.as_path(), - command_vec, - &cwd_clone, - env_map, - None, - ) - }) - .await; - - let capture = match res { - Ok(Ok(v)) => v, - Ok(Err(err)) => { - eprintln!("windows sandbox failed: {err}"); - std::process::exit(1); - } - Err(join_err) => { - eprintln!("windows sandbox join error: {join_err}"); - std::process::exit(1); - } - }; - - if !capture.stdout.is_empty() { - use std::io::Write; - let _ = std::io::stdout().write_all(&capture.stdout); + #[cfg(target_os = "windows")] + { + use codex_windows_sandbox::run_windows_sandbox_capture; + + let policy_str = serde_json::to_string(&config.sandbox_policy)?; + + let sandbox_cwd = sandbox_policy_cwd.clone(); + let cwd_clone = cwd.clone(); + let env_map = env.clone(); + let command_vec = command.clone(); + let base_dir = config.codex_home.clone(); + + // Preflight audit is invoked elsewhere at the appropriate times. + let res = tokio::task::spawn_blocking(move || { + run_windows_sandbox_capture( + policy_str.as_str(), + &sandbox_cwd, + base_dir.as_path(), + command_vec, + &cwd_clone, + env_map, + None, + ) + }) + .await; + + let capture = match res { + Ok(Ok(v)) => v, + Ok(Err(err)) => { + eprintln!("windows sandbox failed: {err}"); + std::process::exit(1); } - if !capture.stderr.is_empty() { - use std::io::Write; - let _ = std::io::stderr().write_all(&capture.stderr); + Err(join_err) => { + eprintln!("windows sandbox join error: {join_err}"); + std::process::exit(1); } + }; - std::process::exit(capture.exit_code); + if !capture.stdout.is_empty() { + use std::io::Write; + let _ = std::io::stdout().write_all(&capture.stdout); } - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!("Windows sandbox is only available on Windows"); + if !capture.stderr.is_empty() { + use std::io::Write; + let _ = std::io::stderr().write_all(&capture.stderr); } + + std::process::exit(capture.exit_code); } #[cfg(target_os = "macos")] - let mut denial_logger = log_denials.then(DenialLogger::new).flatten(); - #[cfg(not(target_os = "macos"))] - let _ = log_denials; - - let mut child = match sandbox_type { - #[cfg(target_os = "macos")] - SandboxType::Seatbelt => { - spawn_command_under_seatbelt( - command, - cwd, - &config.sandbox_policy, - sandbox_policy_cwd.as_path(), - stdio_policy, - env, - ) - .await? + let status = { + let mut denial_logger = log_denials.then(DenialLogger::new).flatten(); + let mut child = spawn_command_under_seatbelt( + command, + cwd, + &config.sandbox_policy, + sandbox_policy_cwd.as_path(), + stdio_policy, + env, + ) + .await?; + if let Some(denial_logger) = &mut denial_logger { + denial_logger.on_child_spawn(&child); } - SandboxType::Landlock => { - #[expect(clippy::expect_used)] - let codex_linux_sandbox_exe = config - .codex_linux_sandbox_exe - .expect("codex-linux-sandbox executable not found"); - spawn_command_under_linux_sandbox( - codex_linux_sandbox_exe, - command, - cwd, - &config.sandbox_policy, - sandbox_policy_cwd.as_path(), - stdio_policy, - env, - ) - .await? - } - SandboxType::Windows => { - unreachable!("Windows sandbox should have been handled above"); - } - }; - - #[cfg(target_os = "macos")] - if let Some(denial_logger) = &mut denial_logger { - denial_logger.on_child_spawn(&child); - } - let status = child.wait().await?; + let status = child.wait().await?; - #[cfg(target_os = "macos")] - if let Some(denial_logger) = denial_logger { - let denials = denial_logger.finish().await; - eprintln!("\n=== Sandbox denials ==="); - if denials.is_empty() { - eprintln!("None found."); - } else { - for seatbelt::SandboxDenial { name, capability } in denials { - eprintln!("({name}) {capability}"); + if let Some(denial_logger) = denial_logger { + let denials = denial_logger.finish().await; + eprintln!("\n=== Sandbox denials ==="); + if denials.is_empty() { + eprintln!("None found."); + } else { + for seatbelt::SandboxDenial { name, capability } in denials { + eprintln!("({name}) {capability}"); + } } } - } + + status + }; + #[cfg(target_os = "linux")] + let status = { + #[expect(clippy::expect_used)] + let codex_linux_sandbox_exe = config + .codex_linux_sandbox_exe + .expect("codex-linux-sandbox executable not found"); + let mut child = spawn_command_under_linux_sandbox( + codex_linux_sandbox_exe, + command, + cwd, + &config.sandbox_policy, + sandbox_policy_cwd.as_path(), + stdio_policy, + env, + ) + .await?; + child.wait().await? + }; handle_exit_status(status); } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index bf7203a9bb..4de970e262 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -209,10 +209,9 @@ pub struct Config { /// output will be hyperlinked using the specified URI scheme. pub file_opener: UriBasedFileOpener, - /// Path to the `codex-linux-sandbox` executable. This must be set if - /// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this - /// cannot be set in the config file: it must be set in code via - /// [`ConfigOverrides`]. + /// Path to the `codex-linux-sandbox` executable. This must be set when the + /// Linux sandbox is used. Note that this cannot be set in the config file: + /// it must be set in code via [`ConfigOverrides`]. /// /// When this program is invoked, arg0 will be set to `codex-linux-sandbox`. pub codex_linux_sandbox_exe: Option, @@ -826,7 +825,7 @@ impl ConfigToml { if cfg!(target_os = "windows") && matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) // If the experimental Windows sandbox is enabled, do not force a downgrade. - && crate::safety::get_platform_sandbox().is_none() + && !crate::safety::get_platform_has_sandbox() { sandbox_policy = SandboxPolicy::new_read_only_policy(); forced_auto_mode_downgraded_on_windows = true; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index f7a145663b..b7a78c3b7d 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -64,20 +64,6 @@ impl ExecParams { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SandboxType { - None, - - /// Only available on macOS. - MacosSeatbelt, - - /// Only available on Linux. - LinuxSeccomp, - - /// Only available on Windows. - WindowsRestrictedToken, -} - #[derive(Clone)] pub struct StdoutStream { pub sub_id: String, @@ -87,7 +73,7 @@ pub struct StdoutStream { pub async fn process_exec_tool_call( params: ExecParams, - sandbox_type: SandboxType, + sandboxed: bool, sandbox_policy: &SandboxPolicy, sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, @@ -125,7 +111,7 @@ pub async fn process_exec_tool_call( .transform( &spec, sandbox_policy, - sandbox_type, + sandboxed, sandbox_cwd, codex_linux_sandbox_exe.as_ref(), ) @@ -145,7 +131,7 @@ pub(crate) async fn execute_exec_env( cwd, env, timeout_ms, - sandbox, + sandboxed, with_escalated_permissions, justification, arg0, @@ -162,9 +148,9 @@ 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, sandboxed, sandbox_policy, stdout_stream).await; let duration = start.elapsed(); - finalize_exec_result(raw_output_result, sandbox, duration) + finalize_exec_result(raw_output_result, sandboxed, duration) } #[cfg(target_os = "windows")] @@ -250,7 +236,7 @@ async fn exec_windows_sandbox( fn finalize_exec_result( raw_output_result: std::result::Result, - sandbox_type: SandboxType, + sandboxed: bool, duration: Duration, ) -> Result { match raw_output_result { @@ -292,7 +278,7 @@ fn finalize_exec_result( })); } - if is_likely_sandbox_denied(sandbox_type, &exec_output) { + if sandboxed && is_likely_sandbox_denied(&exec_output) { return Err(CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(exec_output), })); @@ -311,19 +297,23 @@ pub(crate) mod errors { use super::CodexErr; use crate::sandboxing::SandboxTransformError; + #[cfg(target_os = "linux")] impl From for CodexErr { fn from(err: SandboxTransformError) -> Self { match err { SandboxTransformError::MissingLinuxSandboxExecutable => { CodexErr::LandlockSandboxExecutableNotProvided } - #[cfg(not(target_os = "macos"))] - SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( - "seatbelt sandbox is only available on macOS".to_string(), - ), } } } + + #[cfg(not(target_os = "linux"))] + impl From for CodexErr { + fn from(err: SandboxTransformError) -> Self { + match err {} + } + } } /// We don't have a fully deterministic way to tell if our command failed @@ -331,11 +321,8 @@ pub(crate) mod errors { /// error, but the command itself might fail or succeed for other reasons. /// For now, we conservatively check for well known command failure exit codes and /// also look for common sandbox denial keywords in the command output. -pub(crate) fn is_likely_sandbox_denied( - sandbox_type: SandboxType, - exec_output: &ExecToolCallOutput, -) -> bool { - if sandbox_type == SandboxType::None || exec_output.exit_code == 0 { +pub(crate) fn is_likely_sandbox_denied(exec_output: &ExecToolCallOutput) -> bool { + if exec_output.exit_code == 0 { return false; } @@ -375,12 +362,10 @@ pub(crate) fn is_likely_sandbox_denied( return false; } - #[cfg(unix)] + #[cfg(target_os = "linux")] { const SIGSYS_CODE: i32 = libc::SIGSYS; - if sandbox_type == SandboxType::LinuxSeccomp - && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE - { + if exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE { return true; } } @@ -439,14 +424,12 @@ pub struct ExecToolCallOutput { #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] async fn exec( params: ExecParams, - sandbox: SandboxType, + sandboxed: bool, sandbox_policy: &SandboxPolicy, stdout_stream: Option, ) -> Result { #[cfg(target_os = "windows")] - if sandbox == SandboxType::WindowsRestrictedToken - && !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) - { + if sandboxed && !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) { return exec_windows_sandbox(params, sandbox_policy).await; } let timeout = params.timeout_duration(); @@ -729,31 +712,19 @@ mod tests { #[test] fn sandbox_detection_requires_keywords() { let output = make_exec_output(1, "", "", ""); - assert!(!is_likely_sandbox_denied( - SandboxType::LinuxSeccomp, - &output - )); + assert!(!is_likely_sandbox_denied(&output)); } #[test] fn sandbox_detection_identifies_keyword_in_stderr() { let output = make_exec_output(1, "", "Operation not permitted", ""); - assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output)); + assert!(is_likely_sandbox_denied(&output)); } #[test] fn sandbox_detection_respects_quick_reject_exit_codes() { let output = make_exec_output(127, "", "command not found", ""); - assert!(!is_likely_sandbox_denied( - SandboxType::LinuxSeccomp, - &output - )); - } - - #[test] - fn sandbox_detection_ignores_non_sandbox_mode() { - let output = make_exec_output(1, "", "Operation not permitted", ""); - assert!(!is_likely_sandbox_denied(SandboxType::None, &output)); + assert!(!is_likely_sandbox_denied(&output)); } #[test] @@ -764,18 +735,15 @@ mod tests { "", "cargo failed: Read-only file system when writing target", ); - assert!(is_likely_sandbox_denied( - SandboxType::MacosSeatbelt, - &output - )); + assert!(is_likely_sandbox_denied(&output)); } - #[cfg(unix)] + #[cfg(target_os = "linux")] #[test] fn sandbox_detection_flags_sigsys_exit_code() { let exit_code = EXIT_CODE_SIGNAL_BASE + libc::SIGSYS; let output = make_exec_output(exit_code, "", "", ""); - assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output)); + assert!(is_likely_sandbox_denied(&output)); } #[cfg(unix)] @@ -797,7 +765,7 @@ mod tests { arg0: None, }; - let output = exec(params, SandboxType::None, &SandboxPolicy::ReadOnly, None).await?; + let output = exec(params, false, &SandboxPolicy::ReadOnly, None).await?; assert!(output.timed_out); let stdout = output.stdout.from_utf8_lossy().text; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 6906489e7e..5577d8aeed 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -29,7 +29,6 @@ mod exec_policy; pub mod features; mod flags; pub mod git_info; -pub mod landlock; pub mod mcp; mod mcp_connection_manager; mod mcp_tool_call; @@ -67,7 +66,6 @@ mod openai_model_info; pub mod project_doc; mod rollout; pub(crate) mod safety; -pub mod seatbelt; pub mod shell; pub mod spawn; pub mod terminal; @@ -84,6 +82,10 @@ pub use rollout::list::ConversationsPage; pub use rollout::list::Cursor; pub use rollout::list::parse_cursor; pub use rollout::list::read_head_for_summary; +#[cfg(target_os = "linux")] +pub use sandboxing::linux::landlock; +#[cfg(target_os = "macos")] +pub use sandboxing::mac::seatbelt; mod function_tool; mod state; mod tasks; @@ -93,7 +95,7 @@ pub mod util; pub use apply_patch::CODEX_APPLY_PATCH_ARG1; pub use command_safety::is_safe_command; -pub use safety::get_platform_sandbox; +pub use safety::get_platform_has_sandbox; pub use safety::set_windows_sandbox_enabled; // Re-export the protocol types from the standalone `codex-protocol` crate so existing // `codex_core::protocol::...` references continue to work across the workspace. diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 1e81981fd1..156f6b7e5f 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -5,8 +5,6 @@ use std::path::PathBuf; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchFileChange; -use crate::exec::SandboxType; - use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; @@ -30,7 +28,7 @@ pub fn set_windows_sandbox_enabled(_enabled: bool) {} #[derive(Debug, PartialEq)] pub enum SafetyCheck { AutoApprove { - sandbox_type: SandboxType, + sandboxed: bool, user_explicitly_approved: bool, }, AskUser, @@ -71,19 +69,19 @@ pub fn assess_patch_safety( if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) { // DangerFullAccess is intended to bypass sandboxing entirely. SafetyCheck::AutoApprove { - sandbox_type: SandboxType::None, + sandboxed: false, user_explicitly_approved: false, } } else { // Only auto‑approve when we can actually enforce a sandbox. Otherwise // fall back to asking the user because the patch may touch arbitrary // paths outside the project. - match get_platform_sandbox() { - Some(sandbox_type) => SafetyCheck::AutoApprove { - sandbox_type, + match get_platform_has_sandbox() { + true => SafetyCheck::AutoApprove { + sandboxed: true, user_explicitly_approved: false, }, - None => SafetyCheck::AskUser, + false => SafetyCheck::AskUser, } } } else if policy == AskForApproval::Never { @@ -96,22 +94,24 @@ pub fn assess_patch_safety( } } -pub fn get_platform_sandbox() -> Option { - if cfg!(target_os = "macos") { - Some(SandboxType::MacosSeatbelt) - } else if cfg!(target_os = "linux") { - Some(SandboxType::LinuxSeccomp) - } else if cfg!(target_os = "windows") { - #[cfg(target_os = "windows")] - { - if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) { - return Some(SandboxType::WindowsRestrictedToken); - } - } - None - } else { - None - } +#[cfg(target_os = "macos")] +pub fn get_platform_has_sandbox() -> bool { + true +} + +#[cfg(target_os = "linux")] +pub fn get_platform_has_sandbox() -> bool { + true +} + +#[cfg(target_os = "windows")] +pub fn get_platform_has_sandbox() -> bool { + WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) +} + +#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] +pub fn get_platform_has_sandbox() -> bool { + false } fn is_write_patch_constrained_to_writable_paths( diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/sandboxing/linux/landlock.rs similarity index 100% rename from codex-rs/core/src/landlock.rs rename to codex-rs/core/src/sandboxing/linux/landlock.rs diff --git a/codex-rs/core/src/sandboxing/linux/mod.rs b/codex-rs/core/src/sandboxing/linux/mod.rs new file mode 100644 index 0000000000..d935a411d3 --- /dev/null +++ b/codex-rs/core/src/sandboxing/linux/mod.rs @@ -0,0 +1,3 @@ +#![cfg(target_os = "linux")] + +pub mod landlock; diff --git a/codex-rs/core/src/sandboxing/mac/mod.rs b/codex-rs/core/src/sandboxing/mac/mod.rs new file mode 100644 index 0000000000..b7a641cd4e --- /dev/null +++ b/codex-rs/core/src/sandboxing/mac/mod.rs @@ -0,0 +1,3 @@ +#![cfg(target_os = "macos")] + +pub mod seatbelt; diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/sandboxing/mac/seatbelt.rs similarity index 98% rename from codex-rs/core/src/seatbelt.rs rename to codex-rs/core/src/sandboxing/mac/seatbelt.rs index 8ca7e4357e..54e8821e36 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/sandboxing/mac/seatbelt.rs @@ -1,5 +1,3 @@ -#![cfg(target_os = "macos")] - use std::collections::HashMap; use std::ffi::CStr; use std::path::Path; @@ -11,8 +9,8 @@ use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; -const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl"); -const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl"); +const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("../../seatbelt_base_policy.sbpl"); +const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("../../seatbelt_network_policy.sbpl"); /// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin` /// to defend against an attacker trying to inject a malicious version on the diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 4ecb2a8c12..30dcabf533 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -1,31 +1,37 @@ -/* -Module: sandboxing - -Build platform wrappers and produce ExecEnv for execution. Owns low‑level -sandbox placement and transformation of portable CommandSpec into a -ready‑to‑spawn environment. -*/ +//! # Sandboxing +//! +//! This module provides platform wrappers and constructs `ExecEnv` objects for +//! command execution. It owns low-level sandbox placement logic and transforms +//! portable `CommandSpec` structs into ready-to-spawn execution environments. pub mod assessment; +pub mod linux; +pub mod mac; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_env; -use crate::landlock::create_linux_sandbox_command_args; use crate::protocol::SandboxPolicy; -#[cfg(target_os = "macos")] -use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -#[cfg(target_os = "macos")] -use crate::seatbelt::create_seatbelt_command_args; -#[cfg(target_os = "macos")] -use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; + use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; +#[cfg(target_os = "macos")] +use crate::spawn::CODEX_SANDBOX_ENV_VAR; +#[cfg(target_os = "macos")] +use mac::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; +#[cfg(target_os = "macos")] +use mac::seatbelt::create_seatbelt_command_args; + +#[cfg(target_os = "linux")] +use linux::landlock::create_linux_sandbox_command_args; + +type TransformResult = + Result<(Vec, HashMap, Option), SandboxTransformError>; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SandboxPermissions { UseDefault, @@ -65,7 +71,7 @@ pub struct ExecEnv { pub cwd: PathBuf, pub env: HashMap, pub timeout_ms: Option, - pub sandbox: SandboxType, + pub sandboxed: bool, pub with_escalated_permissions: Option, pub justification: Option, pub arg0: Option, @@ -79,11 +85,9 @@ pub enum SandboxPreference { #[derive(Debug, thiserror::Error)] pub(crate) enum SandboxTransformError { + #[cfg(target_os = "linux")] #[error("missing codex-linux-sandbox executable path")] MissingLinuxSandboxExecutable, - #[cfg(not(target_os = "macos"))] - #[error("seatbelt sandbox is only available on macOS")] - SeatbeltUnavailable, } #[derive(Default)] @@ -98,17 +102,17 @@ impl SandboxManager { &self, policy: &SandboxPolicy, pref: SandboxablePreference, - ) -> SandboxType { + ) -> bool { match pref { - SandboxablePreference::Forbid => SandboxType::None, + SandboxablePreference::Forbid => false, SandboxablePreference::Require => { // Require a platform sandbox when available; on Windows this // respects the enable_experimental_windows_sandbox feature. - crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None) + crate::safety::get_platform_has_sandbox() } SandboxablePreference::Auto => match policy { - SandboxPolicy::DangerFullAccess => SandboxType::None, - _ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None), + SandboxPolicy::DangerFullAccess => false, + _ => crate::safety::get_platform_has_sandbox(), }, } } @@ -117,7 +121,7 @@ impl SandboxManager { &self, spec: &CommandSpec, policy: &SandboxPolicy, - sandbox: SandboxType, + sandboxed: bool, sandbox_policy_cwd: &Path, codex_linux_sandbox_exe: Option<&PathBuf>, ) -> Result { @@ -133,44 +137,21 @@ impl SandboxManager { command.push(spec.program.clone()); command.extend(spec.args.iter().cloned()); - let (command, sandbox_env, arg0_override) = match sandbox { - SandboxType::None => (command, HashMap::new(), None), - #[cfg(target_os = "macos")] - SandboxType::MacosSeatbelt => { - let mut seatbelt_env = HashMap::new(); - seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - let mut args = - create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); - full_command.append(&mut args); - (full_command, seatbelt_env, None) - } - #[cfg(not(target_os = "macos"))] - SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), - SandboxType::LinuxSeccomp => { - let exe = codex_linux_sandbox_exe - .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let mut args = - create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(exe.to_string_lossy().to_string()); - full_command.append(&mut args); - ( - full_command, - HashMap::new(), - Some("codex-linux-sandbox".to_string()), - ) - } - // On Windows, the restricted token sandbox executes in-process via the - // codex-windows-sandbox crate. We leave the command unchanged here and - // branch during execution based on the sandbox type. - #[cfg(target_os = "windows")] - SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None), - // When building for non-Windows targets, this variant is never constructed. - #[cfg(not(target_os = "windows"))] - SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None), - }; + if !sandboxed { + return Ok(ExecEnv { + command, + cwd: spec.cwd.clone(), + env, + timeout_ms: spec.timeout_ms, + sandboxed, + with_escalated_permissions: spec.with_escalated_permissions, + justification: spec.justification.clone(), + arg0: None, + }); + } + + let (command, sandbox_env, arg0_override) = + self.transform_platform(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)?; env.extend(sandbox_env); @@ -179,15 +160,66 @@ impl SandboxManager { cwd: spec.cwd.clone(), env, timeout_ms: spec.timeout_ms, - sandbox, + sandboxed, with_escalated_permissions: spec.with_escalated_permissions, justification: spec.justification.clone(), arg0: arg0_override, }) } +} + +impl SandboxManager { + #[cfg(target_os = "macos")] + fn transform_platform( + &self, + command: Vec, + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + _codex_linux_sandbox_exe: Option<&PathBuf>, + ) -> TransformResult { + let mut seatbelt_env = HashMap::new(); + seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); + let mut args = create_seatbelt_command_args(command, policy, sandbox_policy_cwd); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); + full_command.append(&mut args); + Ok((full_command, seatbelt_env, None)) + } - pub fn denied(&self, sandbox: SandboxType, out: &ExecToolCallOutput) -> bool { - crate::exec::is_likely_sandbox_denied(sandbox, out) + #[cfg(target_os = "linux")] + fn transform_platform( + &self, + command: Vec, + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + codex_linux_sandbox_exe: Option<&PathBuf>, + ) -> TransformResult { + let exe = + codex_linux_sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; + let mut args = + create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(exe.to_string_lossy().to_string()); + full_command.append(&mut args); + Ok(( + full_command, + HashMap::new(), + Some("codex-linux-sandbox".to_string()), + )) + } + + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + // On Windows, the restricted token sandbox executes in-process via the + // codex-windows-sandbox crate. We leave the command unchanged and branch + // during execution based on the sandbox type. + fn transform_platform( + &self, + command: Vec, + _policy: &SandboxPolicy, + _sandbox_policy_cwd: &Path, + _codex_linux_sandbox_exe: Option<&PathBuf>, + ) -> TransformResult { + Ok((command, HashMap::new(), None)) } } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 9644d3678e..45c0a6a92b 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -11,7 +11,6 @@ use uuid::Uuid; use crate::codex::TurnContext; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::StreamOutput; use crate::exec::execute_exec_env; @@ -94,7 +93,7 @@ impl SessionTask for UserShellCommandTask { cwd: cwd.clone(), env: create_env(&turn_context.shell_environment_policy), timeout_ms: None, - sandbox: SandboxType::None, + sandboxed: false, with_escalated_permissions: None, justification: None, arg0: None, diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 7e8e152f67..90ae979ba7 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -104,12 +104,12 @@ impl ToolOrchestrator { .sandbox .select_initial(&turn_ctx.sandbox_policy, tool.sandbox_preference()); if tool.wants_escalated_first_attempt(req) { - initial_sandbox = crate::exec::SandboxType::None; + initial_sandbox = false; } // Platform-specific flag gating is handled by SandboxManager::select_initial - // via crate::safety::get_platform_sandbox(). + // via crate::safety::get_platform_has_sandbox(). let initial_attempt = SandboxAttempt { - sandbox: initial_sandbox, + sandboxed: initial_sandbox, policy: &turn_ctx.sandbox_policy, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, @@ -178,7 +178,7 @@ impl ToolOrchestrator { } let escalated_attempt = SandboxAttempt { - sandbox: crate::exec::SandboxType::None, + sandboxed: false, policy: &turn_ctx.sandbox_policy, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index e694c7fbef..23dc8cd3a2 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -206,7 +206,7 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { } pub(crate) struct SandboxAttempt<'a> { - pub sandbox: crate::exec::SandboxType, + pub sandboxed: bool, pub policy: &'a crate::protocol::SandboxPolicy, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, @@ -221,7 +221,7 @@ impl<'a> SandboxAttempt<'a> { self.manager.transform( spec, self.policy, - self.sandbox, + self.sandboxed, self.sandbox_cwd, self.codex_linux_sandbox_exe, ) diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 390401d789..dd2fa3acaf 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -13,7 +13,7 @@ //! 1) Build a small request `{ command, cwd }`. //! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run. //! 3) Runtime: transform `CommandSpec` → `ExecEnv` → spawn PTY. -//! 4) If denial, orchestrator retries with `SandboxType::None`. +//! 4) If denial, orchestrator retries without sandboxing. //! 5) Session is returned with streaming output + metadata. //! //! This keeps policy logic and user interaction centralized while the PTY/session diff --git a/codex-rs/core/src/unified_exec/session.rs b/codex-rs/core/src/unified_exec/session.rs index 710334c806..dd884e0097 100644 --- a/codex-rs/core/src/unified_exec/session.rs +++ b/codex-rs/core/src/unified_exec/session.rs @@ -11,7 +11,6 @@ use tokio::time::Duration; use tokio_util::sync::CancellationToken; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StreamOutput; use crate::exec::is_likely_sandbox_denied; use crate::truncate::TruncationPolicy; @@ -80,14 +79,14 @@ pub(crate) struct UnifiedExecSession { output_notify: Arc, cancellation_token: CancellationToken, output_task: JoinHandle<()>, - sandbox_type: SandboxType, + sandboxed: bool, } impl UnifiedExecSession { pub(super) fn new( session: ExecCommandSession, initial_output_rx: tokio::sync::broadcast::Receiver>, - sandbox_type: SandboxType, + sandboxed: bool, ) -> Self { let output_buffer = Arc::new(Mutex::new(OutputBufferState::default())); let output_notify = Arc::new(Notify::new()); @@ -120,7 +119,7 @@ impl UnifiedExecSession { output_notify, cancellation_token, output_task, - sandbox_type, + sandboxed, } } @@ -149,12 +148,12 @@ impl UnifiedExecSession { guard.snapshot() } - fn sandbox_type(&self) -> SandboxType { - self.sandbox_type + fn sandboxed(&self) -> bool { + self.sandboxed } pub(super) async fn check_for_sandbox_denial(&self) -> Result<(), UnifiedExecError> { - if self.sandbox_type() == SandboxType::None || !self.has_exited() { + if !self.sandboxed() || !self.has_exited() { return Ok(()); } @@ -178,7 +177,7 @@ impl UnifiedExecSession { timed_out: false, }; - if is_likely_sandbox_denied(self.sandbox_type(), &exec_output) { + if is_likely_sandbox_denied(&exec_output) { let snippet = formatted_truncate_text( &aggregated_text, TruncationPolicy::Tokens(UNIFIED_EXEC_OUTPUT_MAX_TOKENS), @@ -196,14 +195,14 @@ impl UnifiedExecSession { pub(super) async fn from_spawned( spawned: SpawnedPty, - sandbox_type: SandboxType, + sandboxed: bool, ) -> Result { let SpawnedPty { session, output_rx, mut exit_rx, } = spawned; - let managed = Self::new(session, output_rx, sandbox_type); + let managed = Self::new(session, output_rx, sandboxed); let exit_ready = match exit_rx.try_recv() { Ok(_) | Err(TryRecvError::Closed) => true, diff --git a/codex-rs/core/src/unified_exec/session_manager.rs b/codex-rs/core/src/unified_exec/session_manager.rs index d9f99b9eaf..006b7d8a59 100644 --- a/codex-rs/core/src/unified_exec/session_manager.rs +++ b/codex-rs/core/src/unified_exec/session_manager.rs @@ -443,7 +443,7 @@ impl UnifiedExecSessionManager { ) .await .map_err(|err| UnifiedExecError::create_session(err.to_string()))?; - UnifiedExecSession::from_spawned(spawned, env.sandbox).await + UnifiedExecSession::from_spawned(spawned, env.sandboxed).await } pub(super) async fn open_session_with_sandbox( diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index ea5ab84879..f991a96170 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -5,7 +5,6 @@ use std::string::ToString; use codex_core::exec::ExecParams; use codex_core::exec::ExecToolCallOutput; -use codex_core::exec::SandboxType; use codex_core::exec::process_exec_tool_call; use codex_core::protocol::SandboxPolicy; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; @@ -13,8 +12,6 @@ use tempfile::TempDir; use codex_core::error::Result; -use codex_core::get_platform_sandbox; - fn skip_test() -> bool { if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); @@ -24,11 +21,7 @@ fn skip_test() -> bool { false } -#[expect(clippy::expect_used)] async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result { - let sandbox_type = get_platform_sandbox().expect("should be able to get sandbox type"); - assert_eq!(sandbox_type, SandboxType::MacosSeatbelt); - let params = ExecParams { command: cmd.iter().map(ToString::to_string).collect(), cwd: tmp.path().to_path_buf(), @@ -41,7 +34,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result = Some(PathBuf::from(sandbox_program)); let result = process_exec_tool_call( params, - SandboxType::LinuxSeccomp, + true, &sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 7c86dd3b6e..b8b154815a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -327,7 +327,7 @@ impl App { // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. #[cfg(target_os = "windows")] { - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = codex_core::get_platform_has_sandbox() && matches!( app.config.sandbox_policy, codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } @@ -693,7 +693,7 @@ impl App { self.config.sandbox_policy = policy.clone(); #[cfg(target_os = "windows")] if !matches!(policy, codex_core::protocol::SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some() + || codex_core::get_platform_has_sandbox() { self.config.forced_auto_mode_downgraded_on_windows = false; } @@ -708,7 +708,7 @@ impl App { return Ok(true); } - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = codex_core::get_platform_has_sandbox() && policy_is_workspace_write_or_ro && !self.chat_widget.world_writable_warning_hidden(); if should_check { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 501515aec0..7dca3814a3 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2263,7 +2263,7 @@ impl ChatWidget { } else if preset.id == "auto" { #[cfg(target_os = "windows")] { - if codex_core::get_platform_sandbox().is_none() { + if !codex_core::get_platform_has_sandbox() { let preset_clone = preset.clone(); vec![Box::new(move |tx| { tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { @@ -2583,7 +2583,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { if self.config.forced_auto_mode_downgraded_on_windows - && codex_core::get_platform_sandbox().is_none() + && !codex_core::get_platform_has_sandbox() && let Some(preset) = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -2612,8 +2612,8 @@ impl ChatWidget { /// Set the sandbox policy in the widget's config copy. pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) { #[cfg(target_os = "windows")] - let should_clear_downgrade = !matches!(policy, SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some(); + let should_clear_downgrade = + !matches!(policy, SandboxPolicy::ReadOnly) || codex_core::get_platform_has_sandbox(); self.config.sandbox_policy = policy; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 33bd18c437..5b25a05745 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -20,7 +20,7 @@ use codex_core::config::find_codex_home; use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config::resolve_oss_provider; use codex_core::find_conversation_path_by_id_str; -use codex_core::get_platform_sandbox; +use codex_core::get_platform_has_sandbox; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; @@ -568,7 +568,7 @@ async fn load_config_or_exit( /// or if the current cwd project is already trusted. If not, we need to /// show the trust screen. fn should_show_trust_screen(config: &Config) -> bool { - if cfg!(target_os = "windows") && get_platform_sandbox().is_none() { + if cfg!(target_os = "windows") && !get_platform_has_sandbox() { // If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely. return false; } From 2123c851a28eea5e53e954a5923d84c2d5d223dc Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 11:30:50 -0800 Subject: [PATCH 2/9] fix --- codex-rs/core/src/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 2530b52742..7c5d1699b4 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -861,7 +861,7 @@ mod tests { }); let result = process_exec_tool_call( params, - SandboxType::None, + false, &SandboxPolicy::DangerFullAccess, cwd.as_path(), &None, From 59d933589c6439cb40c0faaed3b645e7428caf8b Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 11:39:38 -0800 Subject: [PATCH 3/9] clippy --- codex-rs/core/src/sandboxing/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 944c9e1d72..a0ca8bb088 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -197,8 +197,7 @@ impl SandboxManager { ) -> TransformResult { let exe = codex_linux_sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let mut args = - create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd); + let mut args = create_linux_sandbox_command_args(command, policy, sandbox_policy_cwd); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); full_command.append(&mut args); From 552568c40b6e92b42dd3efc9f00db44f86c9e02c Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 12:46:36 -0800 Subject: [PATCH 4/9] dynamic cfg checks --- codex-rs/core/src/sandboxing/linux/mod.rs | 2 - codex-rs/core/src/sandboxing/mac/mod.rs | 2 - codex-rs/core/src/sandboxing/mod.rs | 93 ++++++++++------------- 3 files changed, 40 insertions(+), 57 deletions(-) diff --git a/codex-rs/core/src/sandboxing/linux/mod.rs b/codex-rs/core/src/sandboxing/linux/mod.rs index d935a411d3..517579e280 100644 --- a/codex-rs/core/src/sandboxing/linux/mod.rs +++ b/codex-rs/core/src/sandboxing/linux/mod.rs @@ -1,3 +1 @@ -#![cfg(target_os = "linux")] - pub mod landlock; diff --git a/codex-rs/core/src/sandboxing/mac/mod.rs b/codex-rs/core/src/sandboxing/mac/mod.rs index b7a641cd4e..c625d0b047 100644 --- a/codex-rs/core/src/sandboxing/mac/mod.rs +++ b/codex-rs/core/src/sandboxing/mac/mod.rs @@ -1,3 +1 @@ -#![cfg(target_os = "macos")] - pub mod seatbelt; diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index a0ca8bb088..5ebb4031f3 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -151,8 +151,13 @@ impl SandboxManager { }); } - let (command, sandbox_env, arg0_override) = - self.transform_platform(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)?; + let (command, sandbox_env, arg0_override) = if cfg!(target_os = "macos") { + self.transform_macos(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? + } else if cfg!(target_os = "linux") { + self.transform_linux(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? + } else { + (command, HashMap::new(), None) + }; env.extend(sandbox_env); @@ -169,58 +174,40 @@ impl SandboxManager { } } -impl SandboxManager { - #[cfg(target_os = "macos")] - fn transform_platform( - &self, - command: Vec, - policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - _codex_linux_sandbox_exe: Option<&PathBuf>, - ) -> TransformResult { - let mut seatbelt_env = HashMap::new(); - seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - let mut args = create_seatbelt_command_args(command, policy, sandbox_policy_cwd); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); - full_command.append(&mut args); - Ok((full_command, seatbelt_env, None)) - } - - #[cfg(target_os = "linux")] - fn transform_platform( - &self, - command: Vec, - policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - codex_linux_sandbox_exe: Option<&PathBuf>, - ) -> TransformResult { - let exe = - codex_linux_sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let mut args = create_linux_sandbox_command_args(command, policy, sandbox_policy_cwd); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(exe.to_string_lossy().to_string()); - full_command.append(&mut args); - Ok(( - full_command, - HashMap::new(), - Some("codex-linux-sandbox".to_string()), - )) - } +fn transform_macos( + &self, + command: Vec, + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + _codex_linux_sandbox_exe: Option<&PathBuf>, +) -> TransformResult { + let mut seatbelt_env = HashMap::new(); + seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); + let mut args = create_seatbelt_command_args(command, policy, sandbox_policy_cwd); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); + full_command.append(&mut args); + Ok((full_command, seatbelt_env, None)) +} - #[cfg(not(any(target_os = "macos", target_os = "linux")))] - // On Windows, the restricted token sandbox executes in-process via the - // codex-windows-sandbox crate. We leave the command unchanged and branch - // during execution based on the sandbox type. - fn transform_platform( - &self, - command: Vec, - _policy: &SandboxPolicy, - _sandbox_policy_cwd: &Path, - _codex_linux_sandbox_exe: Option<&PathBuf>, - ) -> TransformResult { - Ok((command, HashMap::new(), None)) - } +fn transform_linux( + &self, + command: Vec, + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + codex_linux_sandbox_exe: Option<&PathBuf>, +) -> TransformResult { + let exe = + codex_linux_sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; + let mut args = create_linux_sandbox_command_args(command, policy, sandbox_policy_cwd); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(exe.to_string_lossy().to_string()); + full_command.append(&mut args); + Ok(( + full_command, + HashMap::new(), + Some("codex-linux-sandbox".to_string()), + )) } pub async fn execute_env( From 1e5f476e8ab9f51204b27e78023964d30ac77d4b Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 12:46:56 -0800 Subject: [PATCH 5/9] fix --- codex-rs/core/src/sandboxing/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 5ebb4031f3..59991c92b1 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -152,9 +152,9 @@ impl SandboxManager { } let (command, sandbox_env, arg0_override) = if cfg!(target_os = "macos") { - self.transform_macos(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? + transform_macos(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? } else if cfg!(target_os = "linux") { - self.transform_linux(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? + transform_linux(command, policy, sandbox_policy_cwd, codex_linux_sandbox_exe)? } else { (command, HashMap::new(), None) }; From 26feebe89d28a2451c23e79e256b6fc89730d9f9 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 12:47:08 -0800 Subject: [PATCH 6/9] fix --- codex-rs/core/src/sandboxing/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 59991c92b1..d6979f900d 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -20,14 +20,10 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -#[cfg(target_os = "macos")] use crate::spawn::CODEX_SANDBOX_ENV_VAR; -#[cfg(target_os = "macos")] use mac::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -#[cfg(target_os = "macos")] use mac::seatbelt::create_seatbelt_command_args; -#[cfg(target_os = "linux")] use linux::landlock::create_linux_sandbox_command_args; type TransformResult = @@ -86,7 +82,6 @@ pub enum SandboxPreference { #[derive(Debug, thiserror::Error)] pub(crate) enum SandboxTransformError { - #[cfg(target_os = "linux")] #[error("missing codex-linux-sandbox executable path")] MissingLinuxSandboxExecutable, } @@ -175,7 +170,6 @@ impl SandboxManager { } fn transform_macos( - &self, command: Vec, policy: &SandboxPolicy, sandbox_policy_cwd: &Path, @@ -191,7 +185,6 @@ fn transform_macos( } fn transform_linux( - &self, command: Vec, policy: &SandboxPolicy, sandbox_policy_cwd: &Path, From 7e92099157676854d5b24a60109b664183470ce2 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 13:09:31 -0800 Subject: [PATCH 7/9] fix --- codex-rs/core/src/exec.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 7c5d1699b4..0e47cc1369 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -340,7 +340,6 @@ pub(crate) mod errors { use super::CodexErr; use crate::sandboxing::SandboxTransformError; - #[cfg(target_os = "linux")] impl From for CodexErr { fn from(err: SandboxTransformError) -> Self { match err { @@ -350,13 +349,6 @@ pub(crate) mod errors { } } } - - #[cfg(not(target_os = "linux"))] - impl From for CodexErr { - fn from(err: SandboxTransformError) -> Self { - match err {} - } - } } /// We don't have a fully deterministic way to tell if our command failed From a0dfec72f50d3d2908774ca1b44487498bd2bd0f Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 13:21:51 -0800 Subject: [PATCH 8/9] macos --- codex-rs/core/src/sandboxing/mac/seatbelt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/core/src/sandboxing/mac/seatbelt.rs b/codex-rs/core/src/sandboxing/mac/seatbelt.rs index 54e8821e36..0c7ad7fb14 100644 --- a/codex-rs/core/src/sandboxing/mac/seatbelt.rs +++ b/codex-rs/core/src/sandboxing/mac/seatbelt.rs @@ -145,6 +145,7 @@ fn confstr_path(name: libc::c_int) -> Option { } fn macos_dir_params() -> Vec<(String, PathBuf)> { + #[cfg(target_os = "macos")] if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) { return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)]; } From cce4b2616920bfd970d44f6c502a00f495c62b16 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 15:09:48 -0800 Subject: [PATCH 9/9] fix --- codex-rs/core/src/sandboxing/mac/mod.rs | 3 + codex-rs/core/src/sandboxing/mac/seatbelt.rs | 64 ++++++++----------- codex-rs/core/src/sandboxing/mac/sys.rs | 28 ++++++++ .../core/src/seatbelt_network_policy.sbpl | 4 -- 4 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 codex-rs/core/src/sandboxing/mac/sys.rs diff --git a/codex-rs/core/src/sandboxing/mac/mod.rs b/codex-rs/core/src/sandboxing/mac/mod.rs index c625d0b047..c51ad32ae5 100644 --- a/codex-rs/core/src/sandboxing/mac/mod.rs +++ b/codex-rs/core/src/sandboxing/mac/mod.rs @@ -1 +1,4 @@ pub mod seatbelt; + +#[cfg(target_os = "macos")] +pub mod sys; diff --git a/codex-rs/core/src/sandboxing/mac/seatbelt.rs b/codex-rs/core/src/sandboxing/mac/seatbelt.rs index 0c7ad7fb14..055d686d38 100644 --- a/codex-rs/core/src/sandboxing/mac/seatbelt.rs +++ b/codex-rs/core/src/sandboxing/mac/seatbelt.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; -use std::ffi::CStr; use std::path::Path; use std::path::PathBuf; use tokio::process::Child; use crate::protocol::SandboxPolicy; +#[cfg(target_os = "macos")] +use crate::sandboxing::mac::sys; use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; @@ -102,18 +103,26 @@ pub(crate) fn create_seatbelt_command_args( "" }; - // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. let network_policy = if sandbox_policy.has_full_network_access() { MACOS_SEATBELT_NETWORK_POLICY } else { "" }; + let (user_cache_dir_policy, user_cache_dir_params) = user_cache_dir() + .map(|p| { + ( + "(allow file-write* (subpath (param \"DARWIN_USER_CACHE_DIR\")))", + vec![("DARWIN_USER_CACHE_DIR".to_string(), p)], + ) + }) + .unwrap_or_default(); + let full_policy = format!( - "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" + "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}\n{user_cache_dir_policy}" ); - let dir_params = [file_write_dir_params, macos_dir_params()].concat(); + let dir_params = [file_write_dir_params, user_cache_dir_params].concat(); let mut seatbelt_args: Vec = vec!["-p".to_string(), full_policy]; let definition_args = dir_params @@ -125,39 +134,22 @@ pub(crate) fn create_seatbelt_command_args( seatbelt_args } -/// Wraps libc::confstr to return a String. -fn confstr(name: libc::c_int) -> Option { - let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1]; - let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) }; - if len == 0 { - return None; - } - // confstr guarantees NUL-termination when len > 0. - let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) }; - cstr.to_str().ok().map(ToString::to_string) +#[cfg(target_os = "macos")] +fn user_cache_dir() -> Option { + sys::user_cache_dir() } -/// Wraps confstr to return a canonicalized PathBuf. -fn confstr_path(name: libc::c_int) -> Option { - let s = confstr(name)?; - let path = PathBuf::from(s); - path.canonicalize().ok().or(Some(path)) -} - -fn macos_dir_params() -> Vec<(String, PathBuf)> { - #[cfg(target_os = "macos")] - if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) { - return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)]; - } - vec![] +#[cfg(not(target_os = "macos"))] +fn user_cache_dir() -> Option { + None } #[cfg(test)] mod tests { use super::MACOS_SEATBELT_BASE_POLICY; use super::create_seatbelt_command_args; - use super::macos_dir_params; use crate::protocol::SandboxPolicy; + use crate::seatbelt::user_cache_dir; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -226,11 +218,9 @@ mod tests { format!("-DWRITABLE_ROOT_2={}", cwd.to_string_lossy()), ]; - expected_args.extend( - macos_dir_params() - .into_iter() - .map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy())), - ); + if let Some(p) = user_cache_dir() { + expected_args.push(format!("-DDARWIN_USER_CACHE_DIR={}", p.to_string_lossy())); + } expected_args.extend(vec![ "--".to_string(), @@ -320,11 +310,9 @@ mod tests { expected_args.push(format!("-DWRITABLE_ROOT_2={p}")); } - expected_args.extend( - macos_dir_params() - .into_iter() - .map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy())), - ); + if let Some(p) = user_cache_dir() { + expected_args.push(format!("-DDARWIN_USER_CACHE_DIR={}", p.to_string_lossy())); + } expected_args.extend(vec![ "--".to_string(), diff --git a/codex-rs/core/src/sandboxing/mac/sys.rs b/codex-rs/core/src/sandboxing/mac/sys.rs new file mode 100644 index 0000000000..127bf6c920 --- /dev/null +++ b/codex-rs/core/src/sandboxing/mac/sys.rs @@ -0,0 +1,28 @@ +use std::ffi::CStr; +use std::path::PathBuf; +use std::string::ToString; + +use libc; + +/// Wraps libc::confstr to return a String. +fn confstr(name: libc::c_int) -> Option { + let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1]; + let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) }; + if len == 0 { + return None; + } + // confstr guarantees NUL-termination when len > 0. + let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) }; + cstr.to_str().ok().map(ToString::to_string) +} + +/// Wraps confstr to return a canonicalized PathBuf. +fn confstr_path(name: libc::c_int) -> Option { + let s = confstr(name)?; + let path = PathBuf::from(s); + path.canonicalize().ok().or(Some(path)) +} + +pub fn user_cache_dir() -> Option { + confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) +} diff --git a/codex-rs/core/src/seatbelt_network_policy.sbpl b/codex-rs/core/src/seatbelt_network_policy.sbpl index 2a72f95fd3..046879bb35 100644 --- a/codex-rs/core/src/seatbelt_network_policy.sbpl +++ b/codex-rs/core/src/seatbelt_network_policy.sbpl @@ -24,7 +24,3 @@ (allow sysctl-read (sysctl-name-regex #"^net.routetable") ) - -(allow file-write* - (subpath (param "DARWIN_USER_CACHE_DIR")) -)