From 3855146038cb675a8ac2ee88838d75a4de94b111 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 15:24:55 -0700 Subject: [PATCH 01/22] Fix old system bwrap fallback by avoiding --argv0 --- codex-rs/core/README.md | 9 +-- codex-rs/core/src/config/config_tests.rs | 6 +- codex-rs/core/src/config/mod.rs | 23 +------- codex-rs/core/src/landlock.rs | 7 ++- codex-rs/linux-sandbox/README.md | 27 ++++----- codex-rs/linux-sandbox/src/launcher.rs | 42 +++++++++++--- codex-rs/linux-sandbox/src/linux_run_main.rs | 56 +++++++++++++++---- .../linux-sandbox/src/linux_run_main_tests.rs | 34 ++++++++++- 8 files changed, 138 insertions(+), 66 deletions(-) diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index c27ec5460eb0..1873091ca32c 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -60,10 +60,11 @@ only when the split filesystem policy round-trips through the legacy cases like `/repo = write`, `/repo/a = none`, `/repo/a/b = write`, where the more specific writable child must reopen under a denied parent. -The Linux sandbox helper prefers `/usr/bin/bwrap` whenever it is available and -supports the required argv-rewrite flags, and falls back to the vendored -bubblewrap path compiled into the binary otherwise. When `/usr/bin/bwrap` is -missing or too old to support the required flags, Codex also surfaces a startup +The Linux sandbox helper prefers `/usr/bin/bwrap` whenever it is available. If +`/usr/bin/bwrap` is present but too old to support `--argv0`, the helper keeps +using system bubblewrap and switches to a no-`--argv0` compatibility path for +the inner re-exec. If `/usr/bin/bwrap` is missing, it falls back to the +vendored bubblewrap path compiled into the binary and Codex surfaces a startup warning through its normal notification path instead of printing directly from the sandbox helper. diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 7d3af8024974..6b0d8de58a82 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5593,7 +5593,7 @@ fn system_bwrap_warning_reports_missing_system_bwrap() { #[cfg(target_os = "linux")] #[test] -fn system_bwrap_warning_reports_too_old_system_bwrap() { +fn system_bwrap_warning_skips_too_old_system_bwrap() { let fake_bwrap = write_fake_bwrap( r#"#!/bin/sh if [ "$1" = "--help" ]; then @@ -5604,10 +5604,8 @@ exit 1 "#, ); let fake_bwrap_path: &Path = fake_bwrap.as_ref(); - let warning = system_bwrap_warning_for_path(fake_bwrap_path) - .expect("old system bwrap should emit a warning"); - assert!(warning.contains("too old to support `--argv0`")); + assert_eq!(system_bwrap_warning_for_path(fake_bwrap_path), None); } #[cfg(target_os = "linux")] diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 539663c90a7a..3c5201a12b15 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -96,8 +96,6 @@ use std::collections::HashMap; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; -#[cfg(target_os = "linux")] -use std::process::Command; use crate::config::permissions::compile_permission_profile; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; @@ -172,27 +170,8 @@ fn system_bwrap_warning_for_path(system_bwrap_path: &Path) -> Option { system_bwrap_path.display() )); } - if system_bwrap_supports_argv0(system_bwrap_path) { - return None; - } - - Some(format!( - "Codex found system bubblewrap at {}, but it is too old to support `--argv0`. Please upgrade bubblewrap with your package manager. Codex will use the vendored bubblewrap in the meantime.", - system_bwrap_path.display() - )) -} -#[cfg(target_os = "linux")] -fn system_bwrap_supports_argv0(system_bwrap_path: &Path) -> bool { - // bubblewrap added `--argv0` in v0.9.0: - // https://github.com/containers/bubblewrap/releases/tag/v0.9.0 - let output = match Command::new(system_bwrap_path).arg("--help").output() { - Ok(output) => output, - Err(_) => return false, - }; - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - stdout.contains("--argv0") || stderr.contains("--argv0") + None } fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index c1dc4ba5423d..14cfceea2fb7 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -48,11 +48,14 @@ where use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), ); - let arg0 = Some("codex-linux-sandbox"); + let arg0 = codex_linux_sandbox_exe + .as_ref() + .to_string_lossy() + .into_owned(); spawn_child_async(SpawnChildRequest { program: codex_linux_sandbox_exe.as_ref().to_path_buf(), args, - arg0, + arg0: Some(&arg0), cwd: command_cwd, network_sandbox_policy, network, diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index ac34fb4edcb2..5898a9c688c5 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -8,23 +8,24 @@ This crate is responsible for producing: - this should also be true of the `codex` multitool CLI On Linux, the bubblewrap pipeline prefers the system `/usr/bin/bwrap` whenever -it is available and supports the required argv-rewrite flags. If `/usr/bin/bwrap` -is missing or too old to support the required flags, the helper falls back to -the vendored bubblewrap path compiled into this binary. -Codex also surfaces a startup warning when `/usr/bin/bwrap` is missing or too -old to support the required flags so users know it is falling back to the -vendored helper. +it is available. If `/usr/bin/bwrap` is present but too old to support +`--argv0`, the helper keeps using system bubblewrap and switches to a +no-`--argv0` compatibility path for the inner re-exec. If `/usr/bin/bwrap` is +missing, the helper falls back to the vendored bubblewrap path compiled into +this binary. +Codex also surfaces a startup warning when `/usr/bin/bwrap` is missing so users +know it is falling back to the vendored helper. **Current Behavior** - Legacy `SandboxPolicy` / `sandbox_mode` configs remain supported. - Bubblewrap is the default filesystem sandbox pipeline. -- If `/usr/bin/bwrap` is present and supports the required argv-rewrite flags, - the helper uses it. -- If `/usr/bin/bwrap` is missing or too old to support the required flags, the - helper falls back to the vendored bubblewrap path. -- If `/usr/bin/bwrap` is missing or too old to support the required flags, - Codex also surfaces a startup warning instead of printing directly from the - sandbox helper. +- If `/usr/bin/bwrap` is present, the helper uses it. +- If `/usr/bin/bwrap` is present but too old to support `--argv0`, the helper + uses a no-`--argv0` compatibility path for the inner re-exec. +- If `/usr/bin/bwrap` is missing, the helper falls back to the vendored + bubblewrap path. +- If `/usr/bin/bwrap` is missing, Codex also surfaces a startup warning instead + of printing directly from the sandbox helper. - Legacy Landlock + mount protections remain available as an explicit legacy fallback path. - Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`) diff --git a/codex-rs/linux-sandbox/src/launcher.rs b/codex-rs/linux-sandbox/src/launcher.rs index 7d7e040844f0..0df667f4233a 100644 --- a/codex-rs/linux-sandbox/src/launcher.rs +++ b/codex-rs/linux-sandbox/src/launcher.rs @@ -14,13 +14,21 @@ const SYSTEM_BWRAP_PATH: &str = "/usr/bin/bwrap"; #[derive(Debug, Clone, PartialEq, Eq)] enum BubblewrapLauncher { - System(AbsolutePathBuf), + System(SystemBwrapLauncher), Vendored, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct SystemBwrapLauncher { + program: AbsolutePathBuf, + supports_argv0: bool, +} + pub(crate) fn exec_bwrap(argv: Vec, preserved_files: Vec) -> ! { match preferred_bwrap_launcher() { - BubblewrapLauncher::System(program) => exec_system_bwrap(&program, argv, preserved_files), + BubblewrapLauncher::System(launcher) => { + exec_system_bwrap(&launcher.program, argv, preserved_files) + } BubblewrapLauncher::Vendored => exec_vendored_bwrap(argv, preserved_files), } } @@ -33,10 +41,11 @@ fn preferred_bwrap_launcher() -> BubblewrapLauncher { } fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLauncher { - if !system_bwrap_supports_argv0(system_bwrap_path) { + if !system_bwrap_path.is_file() { return BubblewrapLauncher::Vendored; } + let supports_argv0 = system_bwrap_supports_argv0(system_bwrap_path); let system_bwrap_path = match AbsolutePathBuf::from_absolute_path(system_bwrap_path) { Ok(path) => path, Err(err) => panic!( @@ -44,14 +53,25 @@ fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLaun system_bwrap_path.display() ), }; - BubblewrapLauncher::System(system_bwrap_path) + BubblewrapLauncher::System(SystemBwrapLauncher { + program: system_bwrap_path, + supports_argv0, + }) +} + +pub(crate) fn preferred_bwrap_supports_argv0() -> bool { + match preferred_bwrap_launcher() { + BubblewrapLauncher::System(launcher) => launcher.supports_argv0, + BubblewrapLauncher::Vendored => true, + } } fn system_bwrap_supports_argv0(system_bwrap_path: &Path) -> bool { // bubblewrap added `--argv0` in v0.9.0: // https://github.com/containers/bubblewrap/releases/tag/v0.9.0 // Older distro packages (for example Ubuntu 20.04/22.04) ship builds that - // reject `--argv0`, so prefer the vendored build in that case. + // reject `--argv0`, so use the system binary's no-argv0 compatibility path + // in that case. let output = match Command::new(system_bwrap_path).arg("--help").output() { Ok(output) => output, Err(_) => return false, @@ -147,12 +167,15 @@ exit 1 assert_eq!( preferred_bwrap_launcher_for_path(fake_bwrap_path), - BubblewrapLauncher::System(expected) + BubblewrapLauncher::System(SystemBwrapLauncher { + program: expected, + supports_argv0: true, + }) ); } #[test] - fn falls_back_to_vendored_when_system_bwrap_lacks_argv0() { + fn prefers_system_bwrap_when_system_bwrap_lacks_argv0() { let fake_bwrap = write_fake_bwrap( r#"#!/bin/sh if [ "$1" = "--help" ]; then @@ -166,7 +189,10 @@ exit 1 assert_eq!( preferred_bwrap_launcher_for_path(fake_bwrap_path), - BubblewrapLauncher::Vendored + BubblewrapLauncher::System(SystemBwrapLauncher { + program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"), + supports_argv0: false, + }) ); } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index b753460dcba7..0e5d4a585300 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -12,6 +12,7 @@ use crate::bwrap::BwrapOptions; use crate::bwrap::create_bwrap_command_args; use crate::landlock::apply_sandbox_policy_to_current_thread; use crate::launcher::exec_bwrap; +use crate::launcher::preferred_bwrap_supports_argv0; use crate::proxy_routing::activate_proxy_routes_in_netns; use crate::proxy_routing::prepare_host_proxy_route_spec; use codex_protocol::protocol::FileSystemSandboxPolicy; @@ -426,13 +427,14 @@ fn run_bwrap_with_proc_fallback( mount_proc, network_mode, }; - let bwrap_args = build_bwrap_argv( + let mut bwrap_args = build_bwrap_argv( inner, file_system_sandbox_policy, sandbox_policy_cwd, command_cwd, options, ); + apply_inner_command_argv0(&mut bwrap_args.args); exec_bwrap(bwrap_args.args, bwrap_args.preserved_files); } @@ -456,7 +458,7 @@ fn build_bwrap_argv( command_cwd: &Path, options: BwrapOptions, ) -> crate::bwrap::BwrapArgs { - let mut bwrap_args = create_bwrap_command_args( + let bwrap_args = create_bwrap_command_args( inner, file_system_sandbox_policy, sandbox_policy_cwd, @@ -465,16 +467,6 @@ fn build_bwrap_argv( ) .unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}")); - let command_separator_index = bwrap_args - .args - .iter() - .position(|arg| arg == "--") - .unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'")); - bwrap_args.args.splice( - command_separator_index..command_separator_index, - ["--argv0".to_string(), "codex-linux-sandbox".to_string()], - ); - let mut argv = vec!["bwrap".to_string()]; argv.extend(bwrap_args.args); crate::bwrap::BwrapArgs { @@ -483,6 +475,46 @@ fn build_bwrap_argv( } } +fn apply_inner_command_argv0(argv: &mut Vec) { + apply_inner_command_argv0_for_launcher( + argv, + preferred_bwrap_supports_argv0(), + current_process_argv0(), + ); +} + +fn apply_inner_command_argv0_for_launcher( + argv: &mut Vec, + supports_argv0: bool, + argv0_fallback_command: String, +) { + let command_separator_index = argv + .iter() + .position(|arg| arg == "--") + .unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'")); + + if supports_argv0 { + argv.splice( + command_separator_index..command_separator_index, + ["--argv0".to_string(), "codex-linux-sandbox".to_string()], + ); + return; + } + + let command_index = command_separator_index + 1; + let Some(command) = argv.get_mut(command_index) else { + panic!("bubblewrap argv is missing inner command after '--'"); + }; + *command = argv0_fallback_command; +} + +fn current_process_argv0() -> String { + match std::env::args_os().next() { + Some(argv0) => argv0.to_string_lossy().into_owned(), + None => panic!("failed to resolve current process argv[0]"), + } +} + fn preflight_proc_mount_support( sandbox_policy_cwd: &Path, command_cwd: &Path, diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index b42d7e552a55..035c0954a272 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -40,7 +40,7 @@ fn ignores_non_proc_mount_errors() { #[test] fn inserts_bwrap_argv0_before_command_separator() { let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let argv = build_bwrap_argv( + let mut argv = build_bwrap_argv( vec!["/bin/true".to_string()], &FileSystemSandboxPolicy::from(&sandbox_policy), Path::new("/"), @@ -51,6 +51,11 @@ fn inserts_bwrap_argv0_before_command_separator() { }, ) .args; + apply_inner_command_argv0_for_launcher( + &mut argv, + true, + "/tmp/codex-arg0-session/codex-linux-sandbox".to_string(), + ); assert_eq!( argv, vec![ @@ -74,6 +79,33 @@ fn inserts_bwrap_argv0_before_command_separator() { ); } +#[test] +fn rewrites_inner_command_path_when_bwrap_lacks_argv0() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let mut argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &FileSystemSandboxPolicy::from(&sandbox_policy), + Path::new("/"), + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::FullAccess, + }, + ) + .args; + apply_inner_command_argv0_for_launcher( + &mut argv, + false, + "/tmp/codex-arg0-session/codex-linux-sandbox".to_string(), + ); + + assert!(!argv.iter().any(|arg| arg == "--argv0")); + assert!( + argv.windows(2) + .any(|window| { window == ["--", "/tmp/codex-arg0-session/codex-linux-sandbox"] }) + ); +} + #[test] fn inserts_unshare_net_when_network_isolation_requested() { let sandbox_policy = SandboxPolicy::new_read_only_policy(); From e048d63a937aeb61d64444f368a38c045d13383b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 17:55:14 -0700 Subject: [PATCH 02/22] fix: preserve codex-linux-sandbox argv0 dispatch fallback --- codex-rs/arg0/src/lib.rs | 51 +++++++++++++++++++++++++++++------ codex-rs/core/src/landlock.rs | 17 ++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index e210783a35cc..74aef180bf27 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -9,7 +9,7 @@ use codex_utils_home_dir::find_codex_home; use std::os::unix::fs::symlink; use tempfile::TempDir; -const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; +pub const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; const APPLY_PATCH_ARG0: &str = "apply_patch"; const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch"; #[cfg(unix)] @@ -133,8 +133,9 @@ pub fn arg0_dispatch() -> Option { /// /// 1. Load `.env` values from `~/.codex/.env` before creating any threads. /// 2. Construct a Tokio multi-thread runtime. -/// 3. Derive the path to the current executable (so children can re-invoke the -/// sandbox) when running on Linux. +/// 3. Derive the `codex-linux-sandbox` helper path (falling back to the +/// current executable if needed) so children can re-invoke the sandbox when +/// running on Linux. /// 4. Execute the provided async `main_fn` inside that runtime, forwarding any /// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which /// contains the helper executable paths needed to construct @@ -159,11 +160,7 @@ where let current_exe = std::env::current_exe().ok(); let paths = Arg0DispatchPaths { codex_linux_sandbox_exe: if cfg!(target_os = "linux") { - current_exe.or_else(|| { - path_entry - .as_ref() - .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone()) - }) + linux_sandbox_exe_path(path_entry.as_ref(), current_exe) } else { None }, @@ -176,6 +173,18 @@ where }) } +fn linux_sandbox_exe_path( + path_entry: Option<&Arg0PathEntryGuard>, + current_exe: Option, +) -> Option { + // Prefer the `codex-linux-sandbox` alias when available so callers can + // re-exec through a path whose basename still triggers arg0 dispatch on + // bubblewrap builds that do not support `--argv0`. + path_entry + .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone()) + .or(current_exe) +} + fn build_runtime() -> anyhow::Result { let mut builder = tokio::runtime::Builder::new_multi_thread(); builder.enable_all(); @@ -397,11 +406,16 @@ fn try_lock_dir(dir: &Path) -> std::io::Result> { #[cfg(test)] mod tests { + use super::Arg0DispatchPaths; + use super::Arg0PathEntryGuard; use super::LOCK_FILENAME; use super::janitor_cleanup; + use super::linux_sandbox_exe_path; use std::fs; use std::fs::File; use std::path::Path; + use std::path::PathBuf; + use tempfile::TempDir; fn create_lock(dir: &Path) -> std::io::Result { let lock_path = dir.join(LOCK_FILENAME); @@ -413,6 +427,27 @@ mod tests { .open(lock_path) } + #[test] + fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let lock_file = create_lock(temp_dir.path())?; + let alias_path = temp_dir.path().join("codex-linux-sandbox"); + let path_entry = Arg0PathEntryGuard::new( + temp_dir, + lock_file, + Arg0DispatchPaths { + codex_linux_sandbox_exe: Some(alias_path.clone()), + main_execve_wrapper_exe: None, + }, + ); + + assert_eq!( + linux_sandbox_exe_path(Some(&path_entry), Some(PathBuf::from("/usr/bin/codex"))), + Some(alias_path), + ); + Ok(()) + } + #[test] fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> { let root = tempfile::tempdir()?; diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 14cfceea2fb7..262c5ac7f2c9 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -2,6 +2,7 @@ use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; +use codex_arg0::LINUX_SANDBOX_ARG0; use codex_network_proxy::NetworkProxy; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -48,12 +49,18 @@ where use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), ); - let arg0 = codex_linux_sandbox_exe - .as_ref() - .to_string_lossy() - .into_owned(); + let codex_linux_sandbox_exe = codex_linux_sandbox_exe.as_ref(); + let arg0 = if codex_linux_sandbox_exe + .file_name() + .and_then(|name| name.to_str()) + == Some(LINUX_SANDBOX_ARG0) + { + codex_linux_sandbox_exe.to_string_lossy().into_owned() + } else { + LINUX_SANDBOX_ARG0.to_string() + }; spawn_child_async(SpawnChildRequest { - program: codex_linux_sandbox_exe.as_ref().to_path_buf(), + program: codex_linux_sandbox_exe.to_path_buf(), args, arg0: Some(&arg0), cwd: command_cwd, From dcc22b4395aa9f84fcfae8df73552cdf3dc0f2f2 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 18:49:29 -0700 Subject: [PATCH 03/22] test: trim redundant system bwrap warning coverage --- codex-rs/arg0/src/lib.rs | 2 +- codex-rs/core/src/config/config_tests.rs | 17 ----------------- codex-rs/core/src/landlock.rs | 3 ++- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 74aef180bf27..329c1372745c 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -9,7 +9,7 @@ use codex_utils_home_dir::find_codex_home; use std::os::unix::fs::symlink; use tempfile::TempDir; -pub const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; +const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; const APPLY_PATCH_ARG0: &str = "apply_patch"; const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch"; #[cfg(unix)] diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6b0d8de58a82..cb4faa040256 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5608,23 +5608,6 @@ exit 1 assert_eq!(system_bwrap_warning_for_path(fake_bwrap_path), None); } -#[cfg(target_os = "linux")] -#[test] -fn system_bwrap_warning_skips_supported_system_bwrap() { - let fake_bwrap = write_fake_bwrap( - r#"#!/bin/sh -if [ "$1" = "--help" ]; then - echo ' --argv0 PROGRAM' - exit 0 -fi -exit 1 -"#, - ); - let fake_bwrap_path: &Path = fake_bwrap.as_ref(); - - assert_eq!(system_bwrap_warning_for_path(fake_bwrap_path), None); -} - #[cfg(not(target_os = "linux"))] #[test] fn system_bwrap_warning_is_disabled_off_linux() { diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 262c5ac7f2c9..b26783faf0df 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -2,7 +2,6 @@ use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; -use codex_arg0::LINUX_SANDBOX_ARG0; use codex_network_proxy::NetworkProxy; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -13,6 +12,8 @@ use std::path::Path; use std::path::PathBuf; use tokio::process::Child; +const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; + /// Spawn a shell tool command under the Linux sandbox helper /// (codex-linux-sandbox), which defaults to bubblewrap for filesystem /// isolation plus seccomp for network restrictions. From 2502f966d06031e351e5c7dd47663731669620dc Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 20:15:28 -0700 Subject: [PATCH 04/22] fix(linux-sandbox): preserve inner command in old bwrap fallback --- codex-rs/linux-sandbox/src/linux_run_main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 0e5d4a585300..922b31d64092 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -488,6 +488,14 @@ fn apply_inner_command_argv0_for_launcher( supports_argv0: bool, argv0_fallback_command: String, ) { + if !supports_argv0 && let Ok(current_exe) = std::env::current_exe() { + let current_exe = current_exe.to_string_lossy(); + if let Some(command) = argv.iter_mut().find(|arg| arg.as_str() == current_exe) { + *command = argv0_fallback_command; + return; + } + } + let command_separator_index = argv .iter() .position(|arg| arg == "--") From 89bff52c68a464ead159c3f45d17b373ddec5aeb Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 21:23:17 -0700 Subject: [PATCH 05/22] fix(linux-sandbox): rewrite helper argv0 at bwrap command boundary --- codex-rs/linux-sandbox/src/linux_run_main.rs | 25 +---- .../linux-sandbox/src/linux_run_main_tests.rs | 99 +++++++++++++++++-- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 922b31d64092..a31ad6301914 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -232,10 +232,6 @@ enum ResolveSandboxPoliciesError { PartialSplitPolicies, SplitPoliciesRequireDirectRuntimeEnforcement(String), FailedToDeriveLegacyPolicy(String), - MismatchedLegacyPolicy { - provided: SandboxPolicy, - derived: SandboxPolicy, - }, MissingConfiguration, } @@ -260,12 +256,6 @@ impl fmt::Display for ResolveSandboxPoliciesError { "failed to derive legacy sandbox policy from split policies: {err}" ) } - Self::MismatchedLegacyPolicy { provided, derived } => { - write!( - f, - "legacy sandbox policy must match split sandbox policies: provided={provided:?}, derived={derived:?}" - ) - } Self::MissingConfiguration => write!(f, "missing sandbox policy configuration"), } } @@ -311,9 +301,10 @@ fn resolve_sandbox_policies( &derived_legacy_policy, sandbox_policy_cwd, ) { - return Err(ResolveSandboxPoliciesError::MismatchedLegacyPolicy { - provided: sandbox_policy, - derived: derived_legacy_policy, + return Ok(EffectiveSandboxPolicies { + sandbox_policy: derived_legacy_policy, + file_system_sandbox_policy, + network_sandbox_policy, }); } Ok(EffectiveSandboxPolicies { @@ -488,14 +479,6 @@ fn apply_inner_command_argv0_for_launcher( supports_argv0: bool, argv0_fallback_command: String, ) { - if !supports_argv0 && let Ok(current_exe) = std::env::current_exe() { - let current_exe = current_exe.to_string_lossy(); - if let Some(command) = argv.iter_mut().find(|arg| arg.as_str() == current_exe) { - *command = argv0_fallback_command; - return; - } - } - let command_separator_index = argv .iter() .position(|arg| arg == "--") diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 035c0954a272..6dfc9767f88d 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -106,6 +106,46 @@ fn rewrites_inner_command_path_when_bwrap_lacks_argv0() { ); } +#[test] +fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appears_later() { + let nested_current_exe = std::env::current_exe() + .expect("current exe") + .to_string_lossy() + .into_owned(); + let mut argv = vec![ + "bwrap".to_string(), + "--".to_string(), + "/tmp/helper-symlink".to_string(), + "--sandbox-policy-cwd".to_string(), + "/tmp/cwd".to_string(), + "--".to_string(), + nested_current_exe.clone(), + "--codex-run-as-apply-patch".to_string(), + "patch".to_string(), + ]; + + apply_inner_command_argv0_for_launcher( + &mut argv, + false, + "/tmp/argv0-fallback-helper".to_string(), + ); + + assert_eq!( + argv, + vec![ + "bwrap".to_string(), + "--".to_string(), + "/tmp/argv0-fallback-helper".to_string(), + "--sandbox-policy-cwd".to_string(), + "/tmp/cwd".to_string(), + "--".to_string(), + nested_current_exe, + "--codex-run-as-apply-patch".to_string(), + "patch".to_string(), + ] + ); +} + #[test] fn inserts_unshare_net_when_network_isolation_requested() { let sandbox_policy = SandboxPolicy::new_read_only_policy(); @@ -337,21 +377,17 @@ fn resolve_sandbox_policies_rejects_partial_split_policies() { } #[test] -fn resolve_sandbox_policies_rejects_mismatched_legacy_and_split_inputs() { - let err = resolve_sandbox_policies( +fn resolve_sandbox_policies_uses_split_derived_legacy_policy_for_mismatched_legacy_and_split_inputs() + { + let resolved = resolve_sandbox_policies( Path::new("/tmp"), Some(SandboxPolicy::new_read_only_policy()), Some(FileSystemSandboxPolicy::unrestricted()), Some(NetworkSandboxPolicy::Enabled), ) - .expect_err("mismatched legacy and split policies should fail"); - assert!( - matches!( - err, - ResolveSandboxPoliciesError::MismatchedLegacyPolicy { .. } - ), - "{err}" - ); + .expect("mismatched legacy and split policies should resolve from split policy"); + + assert_eq!(resolved.sandbox_policy, SandboxPolicy::DangerFullAccess); } #[test] @@ -428,6 +464,49 @@ fn resolve_sandbox_policies_accepts_semantically_equivalent_workspace_write_inpu ); } +#[test] +fn resolve_sandbox_policies_uses_split_derived_legacy_policy_when_provided_legacy_is_stale() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let workspace = temp_dir.path().join("workspace"); + std::fs::create_dir_all(&workspace).expect("create workspace"); + let workspace = AbsolutePathBuf::from_absolute_path(&workspace).expect("absolute workspace"); + + let provided_legacy_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![workspace.clone()], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + let derived_legacy_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&derived_legacy_policy, &workspace); + + let resolved = resolve_sandbox_policies( + &workspace, + Some(provided_legacy_policy), + Some(file_system_sandbox_policy.clone()), + Some(NetworkSandboxPolicy::Restricted), + ) + .expect("stale provided legacy policy should resolve from split policy"); + + assert_eq!(resolved.sandbox_policy, derived_legacy_policy); + assert_eq!( + resolved.file_system_sandbox_policy, + file_system_sandbox_policy + ); + assert_eq!( + resolved.network_sandbox_policy, + NetworkSandboxPolicy::Restricted + ); +} + #[test] fn apply_seccomp_then_exec_with_legacy_landlock_panics() { let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, true)); From a17aa0eb7b1de2356e6f1416e5b4f527cf7dc038 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 24 Mar 2026 21:46:52 -0700 Subject: [PATCH 06/22] fix(linux-sandbox): restore legacy policy mismatch rejection --- codex-rs/linux-sandbox/src/linux_run_main.rs | 17 ++++-- .../linux-sandbox/src/linux_run_main_tests.rs | 58 ++++--------------- 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index a31ad6301914..0e5d4a585300 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -232,6 +232,10 @@ enum ResolveSandboxPoliciesError { PartialSplitPolicies, SplitPoliciesRequireDirectRuntimeEnforcement(String), FailedToDeriveLegacyPolicy(String), + MismatchedLegacyPolicy { + provided: SandboxPolicy, + derived: SandboxPolicy, + }, MissingConfiguration, } @@ -256,6 +260,12 @@ impl fmt::Display for ResolveSandboxPoliciesError { "failed to derive legacy sandbox policy from split policies: {err}" ) } + Self::MismatchedLegacyPolicy { provided, derived } => { + write!( + f, + "legacy sandbox policy must match split sandbox policies: provided={provided:?}, derived={derived:?}" + ) + } Self::MissingConfiguration => write!(f, "missing sandbox policy configuration"), } } @@ -301,10 +311,9 @@ fn resolve_sandbox_policies( &derived_legacy_policy, sandbox_policy_cwd, ) { - return Ok(EffectiveSandboxPolicies { - sandbox_policy: derived_legacy_policy, - file_system_sandbox_policy, - network_sandbox_policy, + return Err(ResolveSandboxPoliciesError::MismatchedLegacyPolicy { + provided: sandbox_policy, + derived: derived_legacy_policy, }); } Ok(EffectiveSandboxPolicies { diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 6dfc9767f88d..d8b84ffbaf73 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -377,17 +377,22 @@ fn resolve_sandbox_policies_rejects_partial_split_policies() { } #[test] -fn resolve_sandbox_policies_uses_split_derived_legacy_policy_for_mismatched_legacy_and_split_inputs() - { - let resolved = resolve_sandbox_policies( +fn resolve_sandbox_policies_rejects_mismatched_legacy_and_split_inputs() { + let err = resolve_sandbox_policies( Path::new("/tmp"), Some(SandboxPolicy::new_read_only_policy()), Some(FileSystemSandboxPolicy::unrestricted()), Some(NetworkSandboxPolicy::Enabled), ) - .expect("mismatched legacy and split policies should resolve from split policy"); + .expect_err("mismatched legacy and split policies should fail"); - assert_eq!(resolved.sandbox_policy, SandboxPolicy::DangerFullAccess); + assert!( + matches!( + err, + ResolveSandboxPoliciesError::MismatchedLegacyPolicy { .. } + ), + "{err}" + ); } #[test] @@ -464,49 +469,6 @@ fn resolve_sandbox_policies_accepts_semantically_equivalent_workspace_write_inpu ); } -#[test] -fn resolve_sandbox_policies_uses_split_derived_legacy_policy_when_provided_legacy_is_stale() { - let temp_dir = tempfile::TempDir::new().expect("tempdir"); - let workspace = temp_dir.path().join("workspace"); - std::fs::create_dir_all(&workspace).expect("create workspace"); - let workspace = AbsolutePathBuf::from_absolute_path(&workspace).expect("absolute workspace"); - - let provided_legacy_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![workspace.clone()], - read_only_access: ReadOnlyAccess::FullAccess, - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; - let derived_legacy_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::FullAccess, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&derived_legacy_policy, &workspace); - - let resolved = resolve_sandbox_policies( - &workspace, - Some(provided_legacy_policy), - Some(file_system_sandbox_policy.clone()), - Some(NetworkSandboxPolicy::Restricted), - ) - .expect("stale provided legacy policy should resolve from split policy"); - - assert_eq!(resolved.sandbox_policy, derived_legacy_policy); - assert_eq!( - resolved.file_system_sandbox_policy, - file_system_sandbox_policy - ); - assert_eq!( - resolved.network_sandbox_policy, - NetworkSandboxPolicy::Restricted - ); -} - #[test] fn apply_seccomp_then_exec_with_legacy_landlock_panics() { let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, true)); From c04464bed6fc397e250df1922c7c654a65afac80 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 00:33:37 -0700 Subject: [PATCH 07/22] fix(core): run apply_patch through codex binary --- .../core/src/tools/handlers/apply_patch.rs | 2 -- .../core/src/tools/runtimes/apply_patch.rs | 20 +++++-------------- .../src/tools/runtimes/apply_patch_tests.rs | 1 - 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index bbe3572c264c..3b6d8810fd68 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -207,7 +207,6 @@ impl ToolHandler for ApplyPatchHandler { permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms: None, - codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); @@ -311,7 +310,6 @@ pub(crate) async fn intercept_apply_patch( permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms, - codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index d1a6efe001e3..63bd4ab991ca 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -44,7 +44,6 @@ pub struct ApplyPatchRequest { pub additional_permissions: Option, pub permissions_preapproved: bool, pub timeout_ms: Option, - pub codex_exe: Option, } #[derive(Default)] @@ -72,20 +71,11 @@ impl ApplyPatchRuntime { req: &ApplyPatchRequest, _codex_home: &std::path::Path, ) -> Result { - let exe = if let Some(path) = &req.codex_exe { - path.clone() - } else { - #[cfg(target_os = "windows")] - { - codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe") - } - #[cfg(not(target_os = "windows"))] - { - std::env::current_exe().map_err(|e| { - ToolError::Rejected(format!("failed to determine codex exe: {e}")) - })? - } - }; + #[cfg(target_os = "windows")] + let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); + #[cfg(not(target_os = "windows"))] + let exe = std::env::current_exe() + .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?; Ok(SandboxCommand { program: exe.to_string_lossy().to_string(), args: vec![ diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index d2812b5ecfe9..688c93a3ac69 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -52,7 +52,6 @@ fn guardian_review_request_includes_patch_context() { additional_permissions: None, permissions_preapproved: false, timeout_ms: None, - codex_exe: None, }; let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1"); From 614c5591d0b880fabe70af111b7cdbe73cf08246 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 10:08:06 -0700 Subject: [PATCH 08/22] fix(core): preserve codex binary for macOS apply_patch --- codex-rs/core/src/tools/handlers/apply_patch.rs | 10 ++++++++++ codex-rs/core/src/tools/runtimes/apply_patch.rs | 10 +++++++++- codex-rs/core/src/tools/runtimes/apply_patch_tests.rs | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index edb94fb4f0fc..c2c0f6e3e69b 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -195,6 +195,10 @@ impl ToolHandler for ApplyPatchHandler { ); emitter.begin(event_ctx).await; + #[cfg(target_os = "linux")] + let codex_exe = None; + #[cfg(not(target_os = "linux"))] + let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, file_paths, @@ -205,6 +209,7 @@ impl ToolHandler for ApplyPatchHandler { permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms: None, + codex_exe, }; let mut orchestrator = ToolOrchestrator::new(); @@ -297,6 +302,10 @@ pub(crate) async fn intercept_apply_patch( ); emitter.begin(event_ctx).await; + #[cfg(target_os = "linux")] + let codex_exe = None; + #[cfg(not(target_os = "linux"))] + let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, file_paths: approval_keys, @@ -307,6 +316,7 @@ pub(crate) async fn intercept_apply_patch( permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms, + codex_exe, }; let mut orchestrator = ToolOrchestrator::new(); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 85b16713a7e2..150f5cc48b52 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -42,6 +42,7 @@ pub struct ApplyPatchRequest { pub additional_permissions: Option, pub permissions_preapproved: bool, pub timeout_ms: Option, + pub codex_exe: Option, } #[derive(Default)] @@ -71,9 +72,16 @@ impl ApplyPatchRuntime { ) -> Result { #[cfg(target_os = "windows")] let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "linux")] let exe = std::env::current_exe() .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?; + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] + let exe = if let Some(path) = &req.codex_exe { + path.clone() + } else { + std::env::current_exe() + .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))? + }; Ok(SandboxCommand { program: exe.to_string_lossy().to_string(), args: vec![ diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 4020b6415539..79e35a9c2ddf 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -51,6 +51,7 @@ fn guardian_review_request_includes_patch_context() { additional_permissions: None, permissions_preapproved: false, timeout_ms: None, + codex_exe: None, }; let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1"); From b09e9ad220effe3d98f0aa82aaca218580cbd11d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 10:32:19 -0700 Subject: [PATCH 09/22] fix(core): cfg-gate macOS apply_patch codex path --- codex-rs/core/src/tools/handlers/apply_patch.rs | 10 ++++------ codex-rs/core/src/tools/runtimes/apply_patch.rs | 1 + codex-rs/core/src/tools/runtimes/apply_patch_tests.rs | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index c2c0f6e3e69b..e313c39b90c4 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -195,9 +195,7 @@ impl ToolHandler for ApplyPatchHandler { ); emitter.begin(event_ctx).await; - #[cfg(target_os = "linux")] - let codex_exe = None; - #[cfg(not(target_os = "linux"))] + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, @@ -209,6 +207,7 @@ impl ToolHandler for ApplyPatchHandler { permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms: None, + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] codex_exe, }; @@ -302,9 +301,7 @@ pub(crate) async fn intercept_apply_patch( ); emitter.begin(event_ctx).await; - #[cfg(target_os = "linux")] - let codex_exe = None; - #[cfg(not(target_os = "linux"))] + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, @@ -316,6 +313,7 @@ pub(crate) async fn intercept_apply_patch( permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms, + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] codex_exe, }; diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 150f5cc48b52..2c74544b0bee 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -42,6 +42,7 @@ pub struct ApplyPatchRequest { pub additional_permissions: Option, pub permissions_preapproved: bool, pub timeout_ms: Option, + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] pub codex_exe: Option, } diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 79e35a9c2ddf..fb4b79a0cc5b 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -51,6 +51,7 @@ fn guardian_review_request_includes_patch_context() { additional_permissions: None, permissions_preapproved: false, timeout_ms: None, + #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] codex_exe: None, }; From 29950a77c2d9256b4a602c00cc8a68c9db766d83 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 12:23:46 -0700 Subject: [PATCH 10/22] fix(core): drop macOS apply_patch cfg split --- codex-rs/arg0/src/lib.rs | 6 +++--- codex-rs/core/src/config/config_tests.rs | 6 +++++- .../core/src/tools/handlers/apply_patch.rs | 10 ++-------- .../core/src/tools/runtimes/apply_patch.rs | 19 ++++++++++--------- .../src/tools/runtimes/apply_patch_tests.rs | 1 - 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 329c1372745c..676d4461a2f2 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -151,7 +151,7 @@ where // Retain the TempDir so it exists for the lifetime of the invocation of // this executable. Admittedly, we could invoke `keep()` on it, but it // would be nice to avoid leaving temporary directories behind, if possible. - let path_entry = arg0_dispatch(); + let path_entry_guard = arg0_dispatch(); // Regular invocation – create a Tokio runtime and execute the provided // async entry-point. @@ -160,11 +160,11 @@ where let current_exe = std::env::current_exe().ok(); let paths = Arg0DispatchPaths { codex_linux_sandbox_exe: if cfg!(target_os = "linux") { - linux_sandbox_exe_path(path_entry.as_ref(), current_exe) + linux_sandbox_exe_path(path_entry_guard.as_ref(), current_exe) } else { None }, - main_execve_wrapper_exe: path_entry + main_execve_wrapper_exe: path_entry_guard .as_ref() .and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()), }; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 8f4761a1fc8c..050aa145f729 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5627,7 +5627,11 @@ exit 1 ); let fake_bwrap_path: &Path = fake_bwrap.as_ref(); - assert_eq!(system_bwrap_warning_for_path(fake_bwrap_path), None); + assert_eq!( + system_bwrap_warning_for_path(fake_bwrap_path), + None, + "Do not warn even if bwrap does not support `--argv0`", + ); } #[cfg(not(target_os = "linux"))] diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index e313c39b90c4..62ebde48b85a 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -195,8 +195,6 @@ impl ToolHandler for ApplyPatchHandler { ); emitter.begin(event_ctx).await; - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] - let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, file_paths, @@ -207,8 +205,7 @@ impl ToolHandler for ApplyPatchHandler { permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms: None, - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] - codex_exe, + codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); @@ -301,8 +298,6 @@ pub(crate) async fn intercept_apply_patch( ); emitter.begin(event_ctx).await; - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] - let codex_exe = turn.codex_linux_sandbox_exe.clone(); let req = ApplyPatchRequest { action: apply.action, file_paths: approval_keys, @@ -313,8 +308,7 @@ pub(crate) async fn intercept_apply_patch( permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms, - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] - codex_exe, + codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 2c74544b0bee..7f5686e47d93 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -42,7 +42,6 @@ pub struct ApplyPatchRequest { pub additional_permissions: Option, pub permissions_preapproved: bool, pub timeout_ms: Option, - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] pub codex_exe: Option, } @@ -71,17 +70,19 @@ impl ApplyPatchRuntime { req: &ApplyPatchRequest, _codex_home: &std::path::Path, ) -> Result { - #[cfg(target_os = "windows")] - let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); - #[cfg(target_os = "linux")] - let exe = std::env::current_exe() - .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?; - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] let exe = if let Some(path) = &req.codex_exe { path.clone() } else { - std::env::current_exe() - .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))? + #[cfg(target_os = "windows")] + { + codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe") + } + #[cfg(not(target_os = "windows"))] + { + std::env::current_exe().map_err(|e| { + ToolError::Rejected(format!("failed to determine codex exe: {e}")) + })? + } }; Ok(SandboxCommand { program: exe.to_string_lossy().to_string(), diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index fb4b79a0cc5b..79e35a9c2ddf 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -51,7 +51,6 @@ fn guardian_review_request_includes_patch_context() { additional_permissions: None, permissions_preapproved: false, timeout_ms: None, - #[cfg(all(not(target_os = "windows"), not(target_os = "linux")))] codex_exe: None, }; From 70df627486c0c76416d5b2dfbea57094b876c2c5 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 12:26:37 -0700 Subject: [PATCH 11/22] style(arg0): rename path entry guard parameter --- codex-rs/arg0/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 676d4461a2f2..4be476733317 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -174,13 +174,13 @@ where } fn linux_sandbox_exe_path( - path_entry: Option<&Arg0PathEntryGuard>, + path_entry_guard: Option<&Arg0PathEntryGuard>, current_exe: Option, ) -> Option { // Prefer the `codex-linux-sandbox` alias when available so callers can // re-exec through a path whose basename still triggers arg0 dispatch on // bubblewrap builds that do not support `--argv0`. - path_entry + path_entry_guard .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone()) .or(current_exe) } From 3e14bf61a2dded22cd916689f35c183cf900e760 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 13:25:15 -0700 Subject: [PATCH 12/22] fix(core): resolve current exe for apply_patch --- .../core/src/tools/handlers/apply_patch.rs | 2 - .../core/src/tools/runtimes/apply_patch.rs | 20 +++------- .../src/tools/runtimes/apply_patch_tests.rs | 39 ++++++++++++++++++- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 62ebde48b85a..edb94fb4f0fc 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -205,7 +205,6 @@ impl ToolHandler for ApplyPatchHandler { permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms: None, - codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); @@ -308,7 +307,6 @@ pub(crate) async fn intercept_apply_patch( permissions_preapproved: effective_additional_permissions .permissions_preapproved, timeout_ms, - codex_exe: turn.codex_linux_sandbox_exe.clone(), }; let mut orchestrator = ToolOrchestrator::new(); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 7f5686e47d93..85b16713a7e2 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -42,7 +42,6 @@ pub struct ApplyPatchRequest { pub additional_permissions: Option, pub permissions_preapproved: bool, pub timeout_ms: Option, - pub codex_exe: Option, } #[derive(Default)] @@ -70,20 +69,11 @@ impl ApplyPatchRuntime { req: &ApplyPatchRequest, _codex_home: &std::path::Path, ) -> Result { - let exe = if let Some(path) = &req.codex_exe { - path.clone() - } else { - #[cfg(target_os = "windows")] - { - codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe") - } - #[cfg(not(target_os = "windows"))] - { - std::env::current_exe().map_err(|e| { - ToolError::Rejected(format!("failed to determine codex exe: {e}")) - })? - } - }; + #[cfg(target_os = "windows")] + let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); + #[cfg(not(target_os = "windows"))] + let exe = std::env::current_exe() + .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?; Ok(SandboxCommand { program: exe.to_string_lossy().to_string(), args: vec![ diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 79e35a9c2ddf..849a7d887cc2 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -2,6 +2,7 @@ use super::*; use codex_protocol::protocol::GranularApprovalConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; +use std::path::Path; #[test] fn wants_no_sandbox_approval_granular_respects_sandbox_flag() { @@ -51,7 +52,6 @@ fn guardian_review_request_includes_patch_context() { additional_permissions: None, permissions_preapproved: false, timeout_ms: None, - codex_exe: None, }; let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1"); @@ -67,3 +67,40 @@ fn guardian_review_request_includes_patch_context() { } ); } + +#[cfg(not(target_os = "windows"))] +#[test] +fn build_sandbox_command_uses_current_exe_for_apply_patch() { + let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); + let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); + let request = ApplyPatchRequest { + action, + file_paths: vec![ + AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"), + ], + changes: HashMap::from([( + path, + FileChange::Add { + content: "hello".to_string(), + }, + )]), + exec_approval_requirement: ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: None, + }, + additional_permissions: None, + permissions_preapproved: false, + timeout_ms: None, + }; + + let command = ApplyPatchRuntime::build_sandbox_command(&request, Path::new("/tmp")) + .expect("build sandbox command"); + + assert_eq!( + command.program, + std::env::current_exe() + .expect("current exe") + .to_string_lossy() + .to_string() + ); +} From e869060cee68b8212a2944d80768d1eae3d953d3 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 13:45:46 -0700 Subject: [PATCH 13/22] refactor(core): share linux sandbox arg0 constant --- codex-rs/apply-patch/src/lib.rs | 4 ++++ codex-rs/arg0/src/lib.rs | 8 ++++---- codex-rs/core/src/landlock.rs | 9 +++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 3041c4b2cdd4..4ac3d22dfb6f 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -25,6 +25,10 @@ use crate::invocation::ExtractHeredocError; /// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool. pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md"); +/// Basename used when the Codex executable self-invokes as the Linux sandbox +/// helper. +pub const CODEX_LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; + /// Special argv[1] flag used when the Codex executable self-invokes to run the /// internal `apply_patch` path. /// diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 4be476733317..a2a464db4ccb 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -4,12 +4,12 @@ use std::path::Path; use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; +use codex_apply_patch::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; use tempfile::TempDir; -const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; const APPLY_PATCH_ARG0: &str = "apply_patch"; const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch"; #[cfg(unix)] @@ -79,7 +79,7 @@ pub fn arg0_dispatch() -> Option { } } - if exe_name == LINUX_SANDBOX_ARG0 { + if exe_name == CODEX_LINUX_SANDBOX_ARG0 { // Safety: [`run_main`] never returns. codex_linux_sandbox::run_main(); } else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 { @@ -285,7 +285,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result std::io::Result Date: Wed, 25 Mar 2026 14:27:21 -0700 Subject: [PATCH 14/22] fix(core): use shared linux sandbox arg0 constant --- codex-rs/Cargo.lock | 4 ++-- codex-rs/apply-patch/src/lib.rs | 4 ---- codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 2 +- codex-rs/core/src/landlock.rs | 4 +++- codex-rs/core/src/tools/runtimes/apply_patch_tests.rs | 1 + codex-rs/linux-sandbox/Cargo.toml | 1 + codex-rs/linux-sandbox/src/linux_run_main.rs | 3 ++- codex-rs/sandboxing/src/landlock.rs | 5 +++++ 9 files changed, 16 insertions(+), 9 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index df44a7f83b27..874950e85681 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1591,6 +1591,7 @@ dependencies = [ "anyhow", "codex-apply-patch", "codex-linux-sandbox", + "codex-sandboxing", "codex-shell-escalation", "codex-utils-home-dir", "dotenvy", @@ -2219,6 +2220,7 @@ dependencies = [ "clap", "codex-core", "codex-protocol", + "codex-sandboxing", "codex-utils-absolute-path", "landlock", "libc", @@ -2418,8 +2420,6 @@ version = "0.0.0" dependencies = [ "codex-utils-absolute-path", "codex-utils-plugins", - "serde", - "serde_json", "thiserror 2.0.18", ] diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 4ac3d22dfb6f..3041c4b2cdd4 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -25,10 +25,6 @@ use crate::invocation::ExtractHeredocError; /// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool. pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md"); -/// Basename used when the Codex executable self-invokes as the Linux sandbox -/// helper. -pub const CODEX_LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; - /// Special argv[1] flag used when the Codex executable self-invokes to run the /// internal `apply_patch` path. /// diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index abe7277d9499..cd409fedd8c9 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -15,6 +15,7 @@ workspace = true anyhow = { workspace = true } codex-apply-patch = { workspace = true } codex-linux-sandbox = { workspace = true } +codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-home-dir = { workspace = true } dotenvy = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index a2a464db4ccb..4d27cd864fdb 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; -use codex_apply_patch::CODEX_LINUX_SANDBOX_ARG0; +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] use std::os::unix::fs::symlink; diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index fe0078b14793..ef55a207e259 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -2,10 +2,10 @@ use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; -use codex_apply_patch::CODEX_LINUX_SANDBOX_ARG0; use codex_network_proxy::NetworkProxy; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_sandboxing::landlock::allow_network_for_proxy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; use std::collections::HashMap; @@ -57,6 +57,8 @@ where .and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) { + // Old bubblewrap builds without `--argv0` need a real helper path whose + // basename still dispatches to the Linux sandbox entrypoint. codex_linux_sandbox_exe.to_string_lossy().into_owned() } else { CODEX_LINUX_SANDBOX_ARG0.to_string() diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 849a7d887cc2..40c6af73bd13 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -2,6 +2,7 @@ use super::*; use codex_protocol::protocol::GranularApprovalConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; +#[cfg(not(target_os = "windows"))] use std::path::Path; #[test] diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 88da2d7479cb..c0bfc7aca29a 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -19,6 +19,7 @@ workspace = true clap = { workspace = true, features = ["derive"] } codex-core = { workspace = true } codex-protocol = { workspace = true } +codex-sandboxing = { workspace = true } codex-utils-absolute-path = { workspace = true } landlock = { workspace = true } libc = { workspace = true } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 0e5d4a585300..8f39837de1b9 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -18,6 +18,7 @@ use crate::proxy_routing::prepare_host_proxy_route_spec; use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; #[derive(Debug, Parser)] /// CLI surface for the Linux sandbox helper. @@ -496,7 +497,7 @@ fn apply_inner_command_argv0_for_launcher( if supports_argv0 { argv.splice( command_separator_index..command_separator_index, - ["--argv0".to_string(), "codex-linux-sandbox".to_string()], + ["--argv0".to_string(), CODEX_LINUX_SANDBOX_ARG0.to_string()], ); return; } diff --git a/codex-rs/sandboxing/src/landlock.rs b/codex-rs/sandboxing/src/landlock.rs index 8636f9ed0466..d948d23d1fce 100644 --- a/codex-rs/sandboxing/src/landlock.rs +++ b/codex-rs/sandboxing/src/landlock.rs @@ -2,6 +2,11 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use std::path::Path; + +/// Basename used when the Codex executable self-invokes as the Linux sandbox +/// helper. +pub const CODEX_LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox"; + pub fn allow_network_for_proxy(enforce_managed_network: bool) -> bool { // When managed network requirements are active, request proxy-only // networking from the Linux sandbox helper. Without managed requirements, From e03f62b550dffb435a254ed9b42ddae75ef17480 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 25 Mar 2026 16:59:17 -0700 Subject: [PATCH 15/22] fix test --- .../core/src/tools/runtimes/apply_patch.rs | 21 ++++++--- .../src/tools/runtimes/apply_patch_tests.rs | 45 ++++++++++++++++++- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 85b16713a7e2..df7c45ecf7fe 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -68,12 +68,17 @@ impl ApplyPatchRuntime { fn build_sandbox_command( req: &ApplyPatchRequest, _codex_home: &std::path::Path, + configured_codex_exe: Option<&PathBuf>, ) -> Result { #[cfg(target_os = "windows")] let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); #[cfg(not(target_os = "windows"))] - let exe = std::env::current_exe() - .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))?; + let exe = if let Some(path) = configured_codex_exe { + path.clone() + } else { + std::env::current_exe() + .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))? + }; Ok(SandboxCommand { program: exe.to_string_lossy().to_string(), args: vec![ @@ -124,13 +129,13 @@ impl Approvable for ApplyPatchRuntime { let approval_keys = self.approval_keys(req); let changes = req.changes.clone(); Box::pin(async move { + if req.permissions_preapproved && retry_reason.is_none() { + return ReviewDecision::Approved; + } if routes_approval_to_guardian(turn) { let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id); return review_approval_request(session, turn, action, retry_reason).await; } - if req.permissions_preapproved && retry_reason.is_none() { - return ReviewDecision::Approved; - } if let Some(reason) = retry_reason { let rx_approve = session .request_patch_approval( @@ -190,7 +195,11 @@ impl ToolRuntime for ApplyPatchRuntime { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result { - let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?; + let command = Self::build_sandbox_command( + req, + &ctx.turn.config.codex_home, + ctx.turn.codex_linux_sandbox_exe.as_ref(), + )?; let options = ExecOptions { expiration: req.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 40c6af73bd13..05df4f225d25 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -4,6 +4,8 @@ use pretty_assertions::assert_eq; use std::collections::HashMap; #[cfg(not(target_os = "windows"))] use std::path::Path; +#[cfg(not(target_os = "windows"))] +use std::path::PathBuf; #[test] fn wants_no_sandbox_approval_granular_respects_sandbox_flag() { @@ -71,7 +73,46 @@ fn guardian_review_request_includes_patch_context() { #[cfg(not(target_os = "windows"))] #[test] -fn build_sandbox_command_uses_current_exe_for_apply_patch() { +fn build_sandbox_command_prefers_configured_codex_exe_for_apply_patch() { + let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); + let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); + let request = ApplyPatchRequest { + action, + file_paths: vec![ + AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"), + ], + changes: HashMap::from([( + path, + FileChange::Add { + content: "hello".to_string(), + }, + )]), + exec_approval_requirement: ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: None, + }, + additional_permissions: None, + permissions_preapproved: false, + timeout_ms: None, + }; + let configured_codex_exe = PathBuf::from("/tmp/codex"); + + let command = ApplyPatchRuntime::build_sandbox_command( + &request, + Path::new("/tmp"), + Some(&configured_codex_exe), + ) + .expect("build sandbox command"); + + assert_eq!( + command.program, + configured_codex_exe.to_string_lossy().to_string() + ); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn build_sandbox_command_falls_back_to_current_exe_for_apply_patch() { let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); let request = ApplyPatchRequest { @@ -94,7 +135,7 @@ fn build_sandbox_command_uses_current_exe_for_apply_patch() { timeout_ms: None, }; - let command = ApplyPatchRuntime::build_sandbox_command(&request, Path::new("/tmp")) + let command = ApplyPatchRuntime::build_sandbox_command(&request, Path::new("/tmp"), None) .expect("build sandbox command"); assert_eq!( From 151a09e12a17d13df31b179f7a1c45d810170728 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 25 Mar 2026 17:44:22 -0700 Subject: [PATCH 16/22] more platform-specific behavior --- .../core/src/tools/runtimes/apply_patch.rs | 33 ++++++++++++------- .../src/tools/runtimes/apply_patch_tests.rs | 14 +++----- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index df7c45ecf7fe..772a3639b866 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -65,21 +65,33 @@ impl ApplyPatchRuntime { } } + #[cfg(target_os = "windows")] + fn build_sandbox_command( + req: &ApplyPatchRequest, + codex_home: &std::path::Path, + ) -> Result { + Ok(Self::build_sandbox_command_with_program( + req, + codex_windows_sandbox::resolve_current_exe_for_launch(codex_home, "codex.exe"), + )) + } + + #[cfg(not(target_os = "windows"))] fn build_sandbox_command( req: &ApplyPatchRequest, - _codex_home: &std::path::Path, configured_codex_exe: Option<&PathBuf>, ) -> Result { - #[cfg(target_os = "windows")] - let exe = codex_windows_sandbox::resolve_current_exe_for_launch(_codex_home, "codex.exe"); - #[cfg(not(target_os = "windows"))] let exe = if let Some(path) = configured_codex_exe { path.clone() } else { std::env::current_exe() .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))? }; - Ok(SandboxCommand { + Ok(Self::build_sandbox_command_with_program(req, exe)) + } + + fn build_sandbox_command_with_program(req: &ApplyPatchRequest, exe: PathBuf) -> SandboxCommand { + SandboxCommand { program: exe.to_string_lossy().to_string(), args: vec![ CODEX_CORE_APPLY_PATCH_ARG1.to_string(), @@ -89,7 +101,7 @@ impl ApplyPatchRuntime { // Run apply_patch with a minimal environment for determinism and to avoid leaks. env: HashMap::new(), additional_permissions: req.additional_permissions.clone(), - }) + } } fn stdout_stream(ctx: &ToolCtx) -> Option { @@ -195,11 +207,10 @@ impl ToolRuntime for ApplyPatchRuntime { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result { - let command = Self::build_sandbox_command( - req, - &ctx.turn.config.codex_home, - ctx.turn.codex_linux_sandbox_exe.as_ref(), - )?; + #[cfg(target_os = "windows")] + let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?; + #[cfg(not(target_os = "windows"))] + let command = Self::build_sandbox_command(req, ctx.turn.codex_linux_sandbox_exe.as_ref())?; let options = ExecOptions { expiration: req.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 05df4f225d25..3b5355206416 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -3,8 +3,6 @@ use codex_protocol::protocol::GranularApprovalConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; #[cfg(not(target_os = "windows"))] -use std::path::Path; -#[cfg(not(target_os = "windows"))] use std::path::PathBuf; #[test] @@ -97,12 +95,8 @@ fn build_sandbox_command_prefers_configured_codex_exe_for_apply_patch() { }; let configured_codex_exe = PathBuf::from("/tmp/codex"); - let command = ApplyPatchRuntime::build_sandbox_command( - &request, - Path::new("/tmp"), - Some(&configured_codex_exe), - ) - .expect("build sandbox command"); + let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&configured_codex_exe)) + .expect("build sandbox command"); assert_eq!( command.program, @@ -135,8 +129,8 @@ fn build_sandbox_command_falls_back_to_current_exe_for_apply_patch() { timeout_ms: None, }; - let command = ApplyPatchRuntime::build_sandbox_command(&request, Path::new("/tmp"), None) - .expect("build sandbox command"); + let command = + ApplyPatchRuntime::build_sandbox_command(&request, None).expect("build sandbox command"); assert_eq!( command.program, From 1605672ac568f94fb9bcec8292403c3e4fabd83d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 25 Mar 2026 18:30:56 -0700 Subject: [PATCH 17/22] Fix apply_patch self-exec paths in tests and sandbox manager --- codex-rs/core/tests/common/lib.rs | 8 ++-- codex-rs/core/tests/common/test_codex.rs | 18 ++++++--- codex-rs/exec/tests/suite/sandbox.rs | 2 +- codex-rs/sandboxing/src/manager.rs | 14 ++++++- codex-rs/sandboxing/src/manager_tests.rs | 51 ++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 484bd189ef18..8c03971ad74d 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -208,10 +208,6 @@ fn default_test_overrides() -> ConfigOverrides { #[cfg(target_os = "linux")] pub fn find_codex_linux_sandbox_exe() -> Result { - if let Ok(path) = std::env::current_exe() { - return Ok(path); - } - if let Some(path) = TEST_ARG0_PATH_ENTRY .get() .and_then(Option::as_ref) @@ -220,6 +216,10 @@ pub fn find_codex_linux_sandbox_exe() -> Result { return Ok(path); } + if let Ok(path) = std::env::current_exe() { + return Ok(path); + } + codex_utils_cargo_bin::cargo_bin("codex-linux-sandbox") } diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 7b0d41278636..1c45a4fe80a4 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -565,14 +565,20 @@ impl TestCodexBuilder { } if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") { config.codex_linux_sandbox_exe = Some(path); + } else if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex-exec") { + // `codex-exec` also supports `--codex-run-as-apply-patch`, so use it + // when the multitool binary is not available in test builds. + config.codex_linux_sandbox_exe = Some(path); } else if let Ok(exe) = std::env::current_exe() - && let Some(path) = exe - .parent() - .and_then(|parent| parent.parent()) - .map(|parent| parent.join("codex")) - && path.is_file() + && let Some(bin_dir) = exe.parent().and_then(|parent| parent.parent()) { - config.codex_linux_sandbox_exe = Some(path); + let codex = bin_dir.join("codex"); + let codex_exec = bin_dir.join("codex-exec"); + if codex.is_file() { + config.codex_linux_sandbox_exe = Some(codex); + } else if codex_exec.is_file() { + config.codex_linux_sandbox_exe = Some(codex_exec); + } } let mut mutators = vec![]; diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index e6dd37927b54..744a9998ff0d 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -43,7 +43,7 @@ async fn spawn_command_under_sandbox( env: HashMap, ) -> std::io::Result { use codex_core::landlock::spawn_command_under_linux_sandbox; - let codex_linux_sandbox_exe = codex_utils_cargo_bin::cargo_bin("codex-exec") + let codex_linux_sandbox_exe = core_test_support::find_codex_linux_sandbox_exe() .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?; spawn_command_under_linux_sandbox( codex_linux_sandbox_exe, diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index f55f53cb1330..3d286eb64c47 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -1,3 +1,4 @@ +use crate::landlock::CODEX_LINUX_SANDBOX_ARG0; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; use crate::policy_transforms::EffectiveSandboxPermissions; @@ -247,7 +248,10 @@ impl SandboxManager { 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, Some("codex-linux-sandbox".to_string())) + ( + full_command, + Some(linux_sandbox_arg0_override(exe.as_path())), + ) } #[cfg(target_os = "windows")] SandboxType::WindowsRestrictedToken => (argv, None), @@ -271,6 +275,14 @@ impl SandboxManager { } } +fn linux_sandbox_arg0_override(exe: &Path) -> String { + if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) { + exe.to_string_lossy().into_owned() + } else { + CODEX_LINUX_SANDBOX_ARG0.to_string() + } +} + #[cfg(test)] #[path = "manager_tests.rs"] mod tests; diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 17921b191432..690aec89d542 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -249,3 +249,54 @@ fn transform_additional_permissions_preserves_denied_entries() { NetworkSandboxPolicy::Restricted ); } + +#[cfg(target_os = "linux")] +fn transform_linux_seccomp_request( + codex_linux_sandbox_exe: &std::path::PathBuf, +) -> super::SandboxExecRequest { + let manager = SandboxManager::new(); + let cwd = std::env::current_dir().expect("current dir"); + manager + .transform(SandboxTransformRequest { + command: SandboxCommand { + program: "true".to_string(), + args: Vec::new(), + cwd: cwd.clone(), + env: HashMap::new(), + additional_permissions: None, + }, + policy: &SandboxPolicy::DangerFullAccess, + file_system_policy: &FileSystemSandboxPolicy::unrestricted(), + network_policy: NetworkSandboxPolicy::Enabled, + sandbox: SandboxType::LinuxSeccomp, + enforce_managed_network: false, + network: None, + sandbox_policy_cwd: cwd.as_path(), + codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe), + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + }) + .expect("transform") +} + +#[cfg(target_os = "linux")] +#[test] +fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() { + let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex-linux-sandbox"); + let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe); + + assert_eq!( + exec_request.arg0, + Some(codex_linux_sandbox_exe.to_string_lossy().into_owned()) + ); +} + +#[cfg(target_os = "linux")] +#[test] +fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path() { + let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex"); + let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe); + + assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string())); +} From 72a0d34c5d799466426d978b488127cc727d132c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 25 Mar 2026 19:05:49 -0700 Subject: [PATCH 18/22] Handle apply_patch self-exec from Bazel test env --- .../core/src/tools/runtimes/apply_patch.rs | 120 +++++++++++++++++- .../src/tools/runtimes/apply_patch_tests.rs | 38 ++++++ 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 772a3639b866..2a8a635a4c5b 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -28,11 +28,31 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxablePreference; +#[cfg(not(target_os = "windows"))] +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; +#[cfg(not(target_os = "windows"))] +use std::fs::File; +#[cfg(not(target_os = "windows"))] +use std::io::BufRead; +#[cfg(not(target_os = "windows"))] +use std::io::BufReader; +#[cfg(not(target_os = "windows"))] +use std::path::Path; use std::path::PathBuf; +#[cfg(not(target_os = "windows"))] +const APPLY_PATCH_SELF_EXEC_BIN_CANDIDATES: &[&str] = &[ + "codex", + "codex-exec", + "codex-app-server", + "codex-mcp-server", + "codex-tui", + "codex-tui-app-server", +]; + #[derive(Debug)] pub struct ApplyPatchRequest { pub action: ApplyPatchAction, @@ -81,15 +101,103 @@ impl ApplyPatchRuntime { req: &ApplyPatchRequest, configured_codex_exe: Option<&PathBuf>, ) -> Result { - let exe = if let Some(path) = configured_codex_exe { - path.clone() - } else { - std::env::current_exe() - .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))? - }; + let exe = Self::resolve_apply_patch_program(configured_codex_exe)?; Ok(Self::build_sandbox_command_with_program(req, exe)) } + #[cfg(not(target_os = "windows"))] + fn resolve_apply_patch_program( + configured_codex_exe: Option<&PathBuf>, + ) -> Result { + if let Some(path) = + configured_codex_exe.filter(|path| !Self::is_linux_sandbox_helper_path(path)) + { + return Ok(path.clone()); + } + + if let Some(path) = Self::resolve_apply_patch_program_from_test_env() { + return Ok(path); + } + + std::env::current_exe() + .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}"))) + } + + #[cfg(not(target_os = "windows"))] + fn is_linux_sandbox_helper_path(path: &Path) -> bool { + path.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) + } + + #[cfg(not(target_os = "windows"))] + fn resolve_apply_patch_program_from_test_env() -> Option { + APPLY_PATCH_SELF_EXEC_BIN_CANDIDATES + .iter() + .find_map(|name| Self::resolve_test_binary(name)) + } + + #[cfg(not(target_os = "windows"))] + fn resolve_test_binary(name: &str) -> Option { + Self::cargo_bin_env_keys(name) + .into_iter() + .filter_map(|key| std::env::var_os(key).map(PathBuf::from)) + .find_map(|path| Self::resolve_test_binary_path(path)) + } + + #[cfg(not(target_os = "windows"))] + fn cargo_bin_env_keys(name: &str) -> Vec { + let mut env_keys = vec![format!("CARGO_BIN_EXE_{name}")]; + let underscored_name = name.replace('-', "_"); + if underscored_name != name { + env_keys.push(format!("CARGO_BIN_EXE_{underscored_name}")); + } + + env_keys + } + + #[cfg(not(target_os = "windows"))] + fn resolve_test_binary_path(path: PathBuf) -> Option { + if path.is_absolute() && path.exists() { + return Some(path); + } + + Self::resolve_runfile_path(&path) + } + + #[cfg(not(target_os = "windows"))] + fn resolve_runfile_path(path: &Path) -> Option { + if let Some(runfiles_dir) = std::env::var_os("RUNFILES_DIR") { + let resolved = PathBuf::from(runfiles_dir).join(path); + if resolved.exists() { + return Some(resolved); + } + } + + let manifest = std::env::var_os("RUNFILES_MANIFEST_FILE")?; + Self::lookup_runfiles_manifest(Path::new(&manifest), path) + } + + #[cfg(not(target_os = "windows"))] + fn lookup_runfiles_manifest(manifest: &Path, path: &Path) -> Option { + let path = path.to_string_lossy(); + let reader = BufReader::new(File::open(manifest).ok()?); + for line in reader.lines() { + let Ok(line) = line else { + continue; + }; + let Some((runfile, resolved_path)) = line.split_once(' ') else { + continue; + }; + if runfile == path { + let resolved_path = PathBuf::from(resolved_path); + if resolved_path.exists() { + return Some(resolved_path); + } + } + } + + None + } + fn build_sandbox_command_with_program(req: &ApplyPatchRequest, exe: PathBuf) -> SandboxCommand { SandboxCommand { program: exe.to_string_lossy().to_string(), diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 3b5355206416..6c7a028c4433 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -140,3 +140,41 @@ fn build_sandbox_command_falls_back_to_current_exe_for_apply_patch() { .to_string() ); } + +#[cfg(not(target_os = "windows"))] +#[test] +fn build_sandbox_command_ignores_linux_sandbox_helper_alias_for_apply_patch() { + let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); + let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); + let request = ApplyPatchRequest { + action, + file_paths: vec![ + AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"), + ], + changes: HashMap::from([( + path, + FileChange::Add { + content: "hello".to_string(), + }, + )]), + exec_approval_requirement: ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: None, + }, + additional_permissions: None, + permissions_preapproved: false, + timeout_ms: None, + }; + let configured_codex_exe = PathBuf::from("/tmp/codex-linux-sandbox"); + + let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&configured_codex_exe)) + .expect("build sandbox command"); + + assert_eq!( + command.program, + std::env::current_exe() + .expect("current exe") + .to_string_lossy() + .to_string() + ); +} From d0354b292fd3364468ee8c9ea518866792dc09ee Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 25 Mar 2026 19:26:10 -0700 Subject: [PATCH 19/22] Fix apply_patch clippy closure --- codex-rs/core/src/tools/runtimes/apply_patch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 2a8a635a4c5b..609269937601 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -140,7 +140,7 @@ impl ApplyPatchRuntime { Self::cargo_bin_env_keys(name) .into_iter() .filter_map(|key| std::env::var_os(key).map(PathBuf::from)) - .find_map(|path| Self::resolve_test_binary_path(path)) + .find_map(Self::resolve_test_binary_path) } #[cfg(not(target_os = "windows"))] From 0cfa95439a1b34c85d8ddb4b76a9679395338bb2 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 20:38:04 -0700 Subject: [PATCH 20/22] fix(core): add explicit codex self exe for apply_patch --- .../app-server/src/codex_message_processor.rs | 1 + codex-rs/arg0/src/lib.rs | 11 +- codex-rs/core/src/codex.rs | 4 + codex-rs/core/src/config/config_tests.rs | 4 + codex-rs/core/src/config/mod.rs | 9 +- .../core/src/tools/runtimes/apply_patch.rs | 113 +----------------- .../src/tools/runtimes/apply_patch_tests.rs | 46 +------ codex-rs/core/tests/common/test_codex.rs | 8 +- codex-rs/core/tests/suite/apply_patch_cli.rs | 49 ++++++++ codex-rs/exec/src/lib.rs | 1 + codex-rs/mcp-server/src/codex_tool_config.rs | 1 + codex-rs/tui/src/lib.rs | 1 + codex-rs/tui_app_server/src/lib.rs | 1 + 13 files changed, 91 insertions(+), 158 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4687ec5ba3d5..620a85a095dd 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -537,6 +537,7 @@ impl CodexMessageProcessor { data: None, })?; apply_runtime_feature_enablement(&mut config, &self.current_runtime_feature_enablement()); + config.codex_self_exe = self.arg0_paths.codex_self_exe.clone(); config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone(); Ok(config) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 4d27cd864fdb..54bdf278be08 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -19,6 +19,7 @@ const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Arg0DispatchPaths { + pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, pub main_execve_wrapper_exe: Option, } @@ -133,9 +134,10 @@ pub fn arg0_dispatch() -> Option { /// /// 1. Load `.env` values from `~/.codex/.env` before creating any threads. /// 2. Construct a Tokio multi-thread runtime. -/// 3. Derive the `codex-linux-sandbox` helper path (falling back to the -/// current executable if needed) so children can re-invoke the sandbox when -/// running on Linux. +/// 3. Capture the current executable path and derive the +/// `codex-linux-sandbox` helper path (falling back to the current +/// executable if needed) so children can re-invoke the sandbox when running +/// on Linux. /// 4. Execute the provided async `main_fn` inside that runtime, forwarding any /// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which /// contains the helper executable paths needed to construct @@ -159,6 +161,7 @@ where runtime.block_on(async move { let current_exe = std::env::current_exe().ok(); let paths = Arg0DispatchPaths { + codex_self_exe: current_exe.clone(), codex_linux_sandbox_exe: if cfg!(target_os = "linux") { linux_sandbox_exe_path(path_entry_guard.as_ref(), current_exe) } else { @@ -335,6 +338,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result, + pub(crate) codex_self_exe: Option, pub(crate) codex_linux_sandbox_exe: Option, pub(crate) tool_call_gate: Arc, pub(crate) truncation_policy: TruncationPolicy, @@ -970,6 +971,7 @@ impl TurnContext { features, ghost_snapshot: self.ghost_snapshot.clone(), final_output_json_schema: self.final_output_json_schema.clone(), + codex_self_exe: self.codex_self_exe.clone(), codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), truncation_policy, @@ -1427,6 +1429,7 @@ impl Session { features: per_turn_config.features.clone(), ghost_snapshot: per_turn_config.ghost_snapshot.clone(), final_output_json_schema: None, + codex_self_exe: per_turn_config.codex_self_exe.clone(), codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), truncation_policy: model_info.truncation_policy.into(), @@ -5474,6 +5477,7 @@ async fn spawn_review_thread( shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), cwd: parent_turn_context.cwd.clone(), final_output_json_schema: None, + codex_self_exe: parent_turn_context.codex_self_exe.clone(), codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), js_repl: Arc::clone(&sess.js_repl), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 050aa145f729..d4828c2731f2 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4320,6 +4320,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { history: History::default(), ephemeral: false, file_opener: UriBasedFileOpener::VsCode, + codex_self_exe: None, codex_linux_sandbox_exe: None, main_execve_wrapper_exe: None, js_repl_node_path: None, @@ -4463,6 +4464,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { history: History::default(), ephemeral: false, file_opener: UriBasedFileOpener::VsCode, + codex_self_exe: None, codex_linux_sandbox_exe: None, main_execve_wrapper_exe: None, js_repl_node_path: None, @@ -4604,6 +4606,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { history: History::default(), ephemeral: false, file_opener: UriBasedFileOpener::VsCode, + codex_self_exe: None, codex_linux_sandbox_exe: None, main_execve_wrapper_exe: None, js_repl_node_path: None, @@ -4731,6 +4734,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { history: History::default(), ephemeral: false, file_opener: UriBasedFileOpener::VsCode, + codex_self_exe: None, codex_linux_sandbox_exe: None, main_execve_wrapper_exe: None, js_repl_node_path: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index abaa4386b635..a791307528ca 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -455,6 +455,10 @@ pub struct Config { /// output will be hyperlinked using the specified URI scheme. pub file_opener: UriBasedFileOpener, + /// Path to the current Codex executable. This cannot be set in the config + /// file: it must be set in code via [`ConfigOverrides`]. + pub codex_self_exe: Option, + /// Path to the `codex-linux-sandbox` executable. This must be set if /// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this /// cannot be set in the config file: it must be set in code via @@ -743,7 +747,7 @@ impl Config { /// designed to use [AskForApproval::Never] exclusively. /// /// Further, [ConfigOverrides] contains some options that are not supported - /// in [ConfigToml], such as `cwd`, `codex_linux_sandbox_exe`, and + /// in [ConfigToml], such as `cwd`, `codex_self_exe`, `codex_linux_sandbox_exe`, and /// `main_execve_wrapper_exe`. pub async fn load_with_cli_overrides_and_harness_overrides( cli_overrides: Vec<(String, TomlValue)>, @@ -1825,6 +1829,7 @@ pub struct ConfigOverrides { pub model_provider: Option, pub service_tier: Option>, pub config_profile: Option, + pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, pub main_execve_wrapper_exe: Option, pub js_repl_node_path: Option, @@ -2023,6 +2028,7 @@ impl Config { model_provider, service_tier: service_tier_override, config_profile: config_profile_key, + codex_self_exe, codex_linux_sandbox_exe, main_execve_wrapper_exe, js_repl_node_path: js_repl_node_path_override, @@ -2629,6 +2635,7 @@ impl Config { history, ephemeral: ephemeral.unwrap_or_default(), file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), + codex_self_exe, codex_linux_sandbox_exe, main_execve_wrapper_exe, js_repl_node_path, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 609269937601..c476d47738b0 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -28,31 +28,11 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxablePreference; -#[cfg(not(target_os = "windows"))] -use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; -#[cfg(not(target_os = "windows"))] -use std::fs::File; -#[cfg(not(target_os = "windows"))] -use std::io::BufRead; -#[cfg(not(target_os = "windows"))] -use std::io::BufReader; -#[cfg(not(target_os = "windows"))] -use std::path::Path; use std::path::PathBuf; -#[cfg(not(target_os = "windows"))] -const APPLY_PATCH_SELF_EXEC_BIN_CANDIDATES: &[&str] = &[ - "codex", - "codex-exec", - "codex-app-server", - "codex-mcp-server", - "codex-tui", - "codex-tui-app-server", -]; - #[derive(Debug)] pub struct ApplyPatchRequest { pub action: ApplyPatchAction, @@ -99,105 +79,22 @@ impl ApplyPatchRuntime { #[cfg(not(target_os = "windows"))] fn build_sandbox_command( req: &ApplyPatchRequest, - configured_codex_exe: Option<&PathBuf>, + codex_self_exe: Option<&PathBuf>, ) -> Result { - let exe = Self::resolve_apply_patch_program(configured_codex_exe)?; + let exe = Self::resolve_apply_patch_program(codex_self_exe)?; Ok(Self::build_sandbox_command_with_program(req, exe)) } #[cfg(not(target_os = "windows"))] - fn resolve_apply_patch_program( - configured_codex_exe: Option<&PathBuf>, - ) -> Result { - if let Some(path) = - configured_codex_exe.filter(|path| !Self::is_linux_sandbox_helper_path(path)) - { + fn resolve_apply_patch_program(codex_self_exe: Option<&PathBuf>) -> Result { + if let Some(path) = codex_self_exe { return Ok(path.clone()); } - if let Some(path) = Self::resolve_apply_patch_program_from_test_env() { - return Ok(path); - } - std::env::current_exe() .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}"))) } - #[cfg(not(target_os = "windows"))] - fn is_linux_sandbox_helper_path(path: &Path) -> bool { - path.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) - } - - #[cfg(not(target_os = "windows"))] - fn resolve_apply_patch_program_from_test_env() -> Option { - APPLY_PATCH_SELF_EXEC_BIN_CANDIDATES - .iter() - .find_map(|name| Self::resolve_test_binary(name)) - } - - #[cfg(not(target_os = "windows"))] - fn resolve_test_binary(name: &str) -> Option { - Self::cargo_bin_env_keys(name) - .into_iter() - .filter_map(|key| std::env::var_os(key).map(PathBuf::from)) - .find_map(Self::resolve_test_binary_path) - } - - #[cfg(not(target_os = "windows"))] - fn cargo_bin_env_keys(name: &str) -> Vec { - let mut env_keys = vec![format!("CARGO_BIN_EXE_{name}")]; - let underscored_name = name.replace('-', "_"); - if underscored_name != name { - env_keys.push(format!("CARGO_BIN_EXE_{underscored_name}")); - } - - env_keys - } - - #[cfg(not(target_os = "windows"))] - fn resolve_test_binary_path(path: PathBuf) -> Option { - if path.is_absolute() && path.exists() { - return Some(path); - } - - Self::resolve_runfile_path(&path) - } - - #[cfg(not(target_os = "windows"))] - fn resolve_runfile_path(path: &Path) -> Option { - if let Some(runfiles_dir) = std::env::var_os("RUNFILES_DIR") { - let resolved = PathBuf::from(runfiles_dir).join(path); - if resolved.exists() { - return Some(resolved); - } - } - - let manifest = std::env::var_os("RUNFILES_MANIFEST_FILE")?; - Self::lookup_runfiles_manifest(Path::new(&manifest), path) - } - - #[cfg(not(target_os = "windows"))] - fn lookup_runfiles_manifest(manifest: &Path, path: &Path) -> Option { - let path = path.to_string_lossy(); - let reader = BufReader::new(File::open(manifest).ok()?); - for line in reader.lines() { - let Ok(line) = line else { - continue; - }; - let Some((runfile, resolved_path)) = line.split_once(' ') else { - continue; - }; - if runfile == path { - let resolved_path = PathBuf::from(resolved_path); - if resolved_path.exists() { - return Some(resolved_path); - } - } - } - - None - } - fn build_sandbox_command_with_program(req: &ApplyPatchRequest, exe: PathBuf) -> SandboxCommand { SandboxCommand { program: exe.to_string_lossy().to_string(), @@ -318,7 +215,7 @@ impl ToolRuntime for ApplyPatchRuntime { #[cfg(target_os = "windows")] let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?; #[cfg(not(target_os = "windows"))] - let command = Self::build_sandbox_command(req, ctx.turn.codex_linux_sandbox_exe.as_ref())?; + let command = Self::build_sandbox_command(req, ctx.turn.codex_self_exe.as_ref())?; let options = ExecOptions { expiration: req.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 6c7a028c4433..e38bf5bb0510 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -71,7 +71,7 @@ fn guardian_review_request_includes_patch_context() { #[cfg(not(target_os = "windows"))] #[test] -fn build_sandbox_command_prefers_configured_codex_exe_for_apply_patch() { +fn build_sandbox_command_prefers_configured_codex_self_exe_for_apply_patch() { let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); let request = ApplyPatchRequest { @@ -93,14 +93,14 @@ fn build_sandbox_command_prefers_configured_codex_exe_for_apply_patch() { permissions_preapproved: false, timeout_ms: None, }; - let configured_codex_exe = PathBuf::from("/tmp/codex"); + let codex_self_exe = PathBuf::from("/tmp/codex"); - let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&configured_codex_exe)) + let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&codex_self_exe)) .expect("build sandbox command"); assert_eq!( command.program, - configured_codex_exe.to_string_lossy().to_string() + codex_self_exe.to_string_lossy().to_string() ); } @@ -140,41 +140,3 @@ fn build_sandbox_command_falls_back_to_current_exe_for_apply_patch() { .to_string() ); } - -#[cfg(not(target_os = "windows"))] -#[test] -fn build_sandbox_command_ignores_linux_sandbox_helper_alias_for_apply_patch() { - let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt"); - let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string()); - let request = ApplyPatchRequest { - action, - file_paths: vec![ - AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"), - ], - changes: HashMap::from([( - path, - FileChange::Add { - content: "hello".to_string(), - }, - )]), - exec_approval_requirement: ExecApprovalRequirement::NeedsApproval { - reason: None, - proposed_execpolicy_amendment: None, - }, - additional_permissions: None, - permissions_preapproved: false, - timeout_ms: None, - }; - let configured_codex_exe = PathBuf::from("/tmp/codex-linux-sandbox"); - - let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&configured_codex_exe)) - .expect("build sandbox command"); - - assert_eq!( - command.program, - std::env::current_exe() - .expect("current exe") - .to_string_lossy() - .to_string() - ); -} diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 1c45a4fe80a4..5d87a81dce75 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -564,20 +564,20 @@ impl TestCodexBuilder { hook(home.path()); } if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") { - config.codex_linux_sandbox_exe = Some(path); + config.codex_self_exe = Some(path); } else if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex-exec") { // `codex-exec` also supports `--codex-run-as-apply-patch`, so use it // when the multitool binary is not available in test builds. - config.codex_linux_sandbox_exe = Some(path); + config.codex_self_exe = Some(path); } else if let Ok(exe) = std::env::current_exe() && let Some(bin_dir) = exe.parent().and_then(|parent| parent.parent()) { let codex = bin_dir.join("codex"); let codex_exec = bin_dir.join("codex-exec"); if codex.is_file() { - config.codex_linux_sandbox_exe = Some(codex); + config.codex_self_exe = Some(codex); } else if codex_exec.is_file() { - config.codex_linux_sandbox_exe = Some(codex_exec); + config.codex_self_exe = Some(codex_exec); } } diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index d0cefcb61692..15fd3b7d36b7 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -19,6 +19,8 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; +#[cfg(target_os = "linux")] +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use core_test_support::assert_regex_match; use core_test_support::responses::ev_apply_patch_function_call; use core_test_support::responses::ev_assistant_message; @@ -87,6 +89,53 @@ fn apply_patch_responses( ] } +#[cfg(target_os = "linux")] +#[large_stack_test] +async fn apply_patch_cli_uses_codex_self_exe_with_linux_sandbox_helper_alias() -> Result<()> { + skip_if_no_network!(Ok(())); + + let harness = apply_patch_harness().await?; + let codex_linux_sandbox_exe = harness + .test() + .config + .codex_linux_sandbox_exe + .as_ref() + .expect("linux test config should include codex-linux-sandbox helper"); + assert_eq!( + codex_linux_sandbox_exe + .file_name() + .and_then(|name| name.to_str()), + Some(CODEX_LINUX_SANDBOX_ARG0), + ); + + let patch = "*** Begin Patch\n*** Add File: helper-alias.txt\n+hello\n*** End Patch"; + let call_id = "apply-helper-alias"; + mount_apply_patch( + &harness, + call_id, + patch, + "done", + ApplyPatchModelOutput::Function, + ) + .await; + + harness.submit("please apply helper alias patch").await?; + + let out = harness + .apply_patch_output(call_id, ApplyPatchModelOutput::Function) + .await; + assert_regex_match( + r"(?s)^Exit code: 0.*Success\. Updated the following files:\nA helper-alias\.txt\n?$", + &out, + ); + assert_eq!( + fs::read_to_string(harness.path("helper-alias.txt"))?, + "hello\n" + ); + + Ok(()) +} + #[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index c4f106b46441..0cc5ad28cabe 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -327,6 +327,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result cwd: resolved_cwd, model_provider: model_provider.clone(), service_tier: None, + codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), js_repl_node_path: None, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index f3f66b555970..f83fd4fd5a1f 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -176,6 +176,7 @@ impl CodexToolCallParam { cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), sandbox_mode: sandbox.map(Into::into), + codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 43182b524059..e291480d0bfb 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -418,6 +418,7 @@ pub async fn run_main( cwd, model_provider: model_provider_override.clone(), config_profile: cli.config_profile.clone(), + codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), show_raw_agent_reasoning: cli.oss.then_some(true), diff --git a/codex-rs/tui_app_server/src/lib.rs b/codex-rs/tui_app_server/src/lib.rs index 4a36eb58c6f6..f01564360a11 100644 --- a/codex-rs/tui_app_server/src/lib.rs +++ b/codex-rs/tui_app_server/src/lib.rs @@ -740,6 +740,7 @@ pub async fn run_main( cwd, model_provider: model_provider_override.clone(), config_profile: cli.config_profile.clone(), + codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), show_raw_agent_reasoning: cli.oss.then_some(true), From f1946925c44744974fa4ed24fd418b748b50d6a7 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 21:21:02 -0700 Subject: [PATCH 21/22] test(core): skip arg0 alias setup for helper re-execs --- codex-rs/core/tests/suite/mod.rs | 20 +++++++++-- codex-rs/linux-sandbox/src/launcher.rs | 47 +++++++------------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index d540b01bbfc4..7ebe5c91f422 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -1,8 +1,11 @@ // Aggregates all former standalone integration tests as modules. use std::ffi::OsString; +use std::path::Path; +use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_arg0::Arg0PathEntryGuard; use codex_arg0::arg0_dispatch; +use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use ctor::ctor; use tempfile::TempDir; @@ -19,7 +22,18 @@ const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; // based on the arg0. // NOTE: this doesn't work on ARM #[ctor] -pub static CODEX_ALIASES_TEMP_DIR: TestCodexAliasesGuard = unsafe { +pub static CODEX_ALIASES_TEMP_DIR: Option = unsafe { + let mut args = std::env::args_os(); + let argv0 = args.next().unwrap_or_default(); + let exe_name = Path::new(&argv0) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""); + let argv1 = args.next().unwrap_or_default(); + if exe_name == CODEX_LINUX_SANDBOX_ARG0 || argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { + return None; + } + #[allow(clippy::unwrap_used)] let codex_home = tempfile::Builder::new() .prefix("codex-core-tests") @@ -47,11 +61,11 @@ pub static CODEX_ALIASES_TEMP_DIR: TestCodexAliasesGuard = unsafe { }, } - TestCodexAliasesGuard { + Some(TestCodexAliasesGuard { _codex_home: codex_home, _arg0: arg0, _previous_codex_home: previous_codex_home, - } + }) }; #[cfg(not(target_os = "windows"))] diff --git a/codex-rs/linux-sandbox/src/launcher.rs b/codex-rs/linux-sandbox/src/launcher.rs index 0df667f4233a..e68eea27252b 100644 --- a/codex-rs/linux-sandbox/src/launcher.rs +++ b/codex-rs/linux-sandbox/src/launcher.rs @@ -41,6 +41,13 @@ fn preferred_bwrap_launcher() -> BubblewrapLauncher { } fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLauncher { + preferred_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_supports_argv0) +} + +fn preferred_bwrap_launcher_for_path_with_probe( + system_bwrap_path: &Path, + system_bwrap_supports_argv0: impl FnOnce(&Path) -> bool, +) -> BubblewrapLauncher { if !system_bwrap_path.is_file() { return BubblewrapLauncher::Vendored; } @@ -146,27 +153,16 @@ fn clear_cloexec(fd: libc::c_int) { mod tests { use super::*; use pretty_assertions::assert_eq; - use std::fs; - use std::os::unix::fs::PermissionsExt; use tempfile::NamedTempFile; - use tempfile::TempPath; #[test] fn prefers_system_bwrap_when_help_lists_argv0() { - let fake_bwrap = write_fake_bwrap( - r#"#!/bin/sh -if [ "$1" = "--help" ]; then - echo ' --argv0 PROGRAM' - exit 0 -fi -exit 1 -"#, - ); - let fake_bwrap_path: &Path = fake_bwrap.as_ref(); + let fake_bwrap = NamedTempFile::new().expect("temp file"); + let fake_bwrap_path = fake_bwrap.path(); let expected = AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"); assert_eq!( - preferred_bwrap_launcher_for_path(fake_bwrap_path), + preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| true), BubblewrapLauncher::System(SystemBwrapLauncher { program: expected, supports_argv0: true, @@ -176,19 +172,11 @@ exit 1 #[test] fn prefers_system_bwrap_when_system_bwrap_lacks_argv0() { - let fake_bwrap = write_fake_bwrap( - r#"#!/bin/sh -if [ "$1" = "--help" ]; then - echo 'usage: bwrap [OPTION...] COMMAND' - exit 0 -fi -exit 1 -"#, - ); - let fake_bwrap_path: &Path = fake_bwrap.as_ref(); + let fake_bwrap = NamedTempFile::new().expect("temp file"); + let fake_bwrap_path = fake_bwrap.path(); assert_eq!( - preferred_bwrap_launcher_for_path(fake_bwrap_path), + preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| false), BubblewrapLauncher::System(SystemBwrapLauncher { program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"), supports_argv0: false, @@ -233,13 +221,4 @@ exit 1 } flags } - - fn write_fake_bwrap(contents: &str) -> TempPath { - // Linux rejects exec-ing a file that is still open for writing. - let path = NamedTempFile::new().expect("temp file").into_temp_path(); - fs::write(&path, contents).expect("write fake bwrap"); - let permissions = fs::Permissions::from_mode(0o755); - fs::set_permissions(&path, permissions).expect("chmod fake bwrap"); - path - } } From 62336e078458bedf9f9006f8951bdebe51f0fed7 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 25 Mar 2026 22:27:56 -0700 Subject: [PATCH 22/22] test(core): narrow ctor unsafe scope --- codex-rs/arg0/src/lib.rs | 5 +++++ codex-rs/core/tests/suite/mod.rs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 54bdf278be08..efad6c2481e9 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -19,6 +19,11 @@ const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Arg0DispatchPaths { + /// Stable path to the current Codex executable for child re-execs. + /// + /// Prefer this over [`std::env::current_exe()`] in code that may run under + /// a test harness, where `current_exe()` can point at the harness binary + /// instead of the real Codex CLI. pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, pub main_execve_wrapper_exe: Option, diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 7ebe5c91f422..5bbf233f1e4a 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -22,7 +22,7 @@ const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; // based on the arg0. // NOTE: this doesn't work on ARM #[ctor] -pub static CODEX_ALIASES_TEMP_DIR: Option = unsafe { +pub static CODEX_ALIASES_TEMP_DIR: Option = { let mut args = std::env::args_os(); let argv0 = args.next().unwrap_or_default(); let exe_name = Path::new(&argv0) @@ -30,6 +30,8 @@ pub static CODEX_ALIASES_TEMP_DIR: Option = unsafe { .and_then(|name| name.to_str()) .unwrap_or(""); let argv1 = args.next().unwrap_or_default(); + // Helper re-execs inherit this ctor too, but they may run inside a sandbox + // where creating another CODEX_HOME tempdir under /tmp is not allowed. if exe_name == CODEX_LINUX_SANDBOX_ARG0 || argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { return None; }