From cfe518b9daeb617b58a0eee65ff2847d239a7071 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 13:18:21 -0800 Subject: [PATCH 01/23] feat(sandbox): enforce proxy-aware network routing --- codex-rs/core/src/landlock.rs | 30 ++- codex-rs/core/src/sandboxing/mod.rs | 12 +- codex-rs/core/src/seatbelt.rs | 232 +++++++++++++++++- .../core/src/seatbelt_network_policy.sbpl | 3 +- codex-rs/linux-sandbox/README.md | 2 + codex-rs/linux-sandbox/src/bwrap.rs | 13 +- codex-rs/linux-sandbox/src/landlock.rs | 11 +- codex-rs/linux-sandbox/src/linux_run_main.rs | 76 +++++- codex-rs/network-proxy/src/lib.rs | 4 + codex-rs/network-proxy/src/proxy.rs | 148 ++++++++++- 10 files changed, 490 insertions(+), 41 deletions(-) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index ea27f77f75e..f76ea00532c 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,6 +1,7 @@ use crate::protocol::SandboxPolicy; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; +use codex_network_proxy::PROXY_URL_ENV_KEYS; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -33,6 +34,7 @@ where sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, + !sandbox_policy.has_full_network_access() && has_proxy_env_vars(&env), ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async( @@ -56,6 +58,7 @@ pub(crate) fn create_linux_sandbox_command_args( sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, use_bwrap_sandbox: bool, + allow_network_for_proxy: bool, ) -> Vec { #[expect(clippy::expect_used)] let sandbox_policy_cwd = sandbox_policy_cwd @@ -76,6 +79,9 @@ pub(crate) fn create_linux_sandbox_command_args( if use_bwrap_sandbox { linux_cmd.push("--use-bwrap-sandbox".to_string()); } + if allow_network_for_proxy { + linux_cmd.push("--allow-network-for-proxy".to_string()); + } // Separator so that command arguments starting with `-` are not parsed as // options of the helper itself. @@ -87,6 +93,12 @@ pub(crate) fn create_linux_sandbox_command_args( linux_cmd } +fn has_proxy_env_vars(env: &HashMap) -> bool { + PROXY_URL_ENV_KEYS + .iter() + .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) +} + #[cfg(test)] mod tests { use super::*; @@ -98,16 +110,30 @@ mod tests { let cwd = Path::new("/tmp"); let policy = SandboxPolicy::ReadOnly; - let with_bwrap = create_linux_sandbox_command_args(command.clone(), &policy, cwd, true); + let with_bwrap = + create_linux_sandbox_command_args(command.clone(), &policy, cwd, true, false); assert_eq!( with_bwrap.contains(&"--use-bwrap-sandbox".to_string()), true ); - let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false); + let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false, false); assert_eq!( without_bwrap.contains(&"--use-bwrap-sandbox".to_string()), false ); } + + #[test] + fn proxy_flag_is_included_when_requested() { + let command = vec!["/bin/true".to_string()]; + let cwd = Path::new("/tmp"); + let policy = SandboxPolicy::ReadOnly; + + let args = create_linux_sandbox_command_args(command, &policy, cwd, true, true); + assert_eq!( + args.contains(&"--allow-network-for-proxy".to_string()), + true + ); + } } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 0ffede3019d..be48039b234 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -22,6 +22,7 @@ use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_protocol::config_types::WindowsSandboxLevel; pub use codex_protocol::models::SandboxPermissions; use std::collections::HashMap; @@ -148,7 +149,7 @@ impl SandboxManager { let mut seatbelt_env = HashMap::new(); seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); let mut args = - create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd); + create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd, &env); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); full_command.append(&mut args); @@ -159,11 +160,14 @@ impl SandboxManager { SandboxType::LinuxSeccomp => { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; + let allow_network_for_proxy = + !policy.has_full_network_access() && has_proxy_env_vars(&env); let mut args = create_linux_sandbox_command_args( command.clone(), policy, sandbox_policy_cwd, use_linux_sandbox_bwrap, + allow_network_for_proxy, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); @@ -204,6 +208,12 @@ impl SandboxManager { } } +fn has_proxy_env_vars(env: &HashMap) -> bool { + PROXY_URL_ENV_KEYS + .iter() + .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) +} + pub async fn execute_env( env: ExecEnv, policy: &SandboxPolicy, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index a15ebb177bd..ededb6a0647 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -1,10 +1,13 @@ #![cfg(target_os = "macos")] +use codex_network_proxy::PROXY_URL_ENV_KEYS; +use std::collections::BTreeSet; use std::collections::HashMap; use std::ffi::CStr; use std::path::Path; use std::path::PathBuf; use tokio::process::Child; +use url::Url; use crate::protocol::SandboxPolicy; use crate::spawn::CODEX_SANDBOX_ENV_VAR; @@ -28,7 +31,7 @@ pub async fn spawn_command_under_seatbelt( stdio_policy: StdioPolicy, mut env: HashMap, ) -> std::io::Result { - let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd); + let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, &env); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); spawn_child_async( @@ -43,10 +46,93 @@ pub async fn spawn_command_under_seatbelt( .await } +fn is_loopback_host(host: &str) -> bool { + host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1" +} + +fn proxy_scheme_default_port(scheme: &str) -> u16 { + match scheme { + "https" => 443, + "socks5" | "socks5h" | "socks4" | "socks4a" => 1080, + _ => 80, + } +} + +fn proxy_loopback_ports_from_env(env: &HashMap) -> Vec { + let mut ports = BTreeSet::new(); + for key in PROXY_URL_ENV_KEYS { + let Some(proxy_url) = env.get(*key) else { + continue; + }; + let trimmed = proxy_url.trim(); + if trimmed.is_empty() { + continue; + } + + let candidate = if trimmed.contains("://") { + trimmed.to_string() + } else { + format!("http://{trimmed}") + }; + let Ok(parsed) = Url::parse(&candidate) else { + continue; + }; + let Some(host) = parsed.host_str() else { + continue; + }; + if !is_loopback_host(host) { + continue; + } + + let scheme = parsed.scheme().to_ascii_lowercase(); + let port = parsed + .port() + .unwrap_or_else(|| proxy_scheme_default_port(scheme.as_str())); + ports.insert(port); + } + ports.into_iter().collect() +} + +fn has_proxy_env_vars(env: &HashMap) -> bool { + PROXY_URL_ENV_KEYS + .iter() + .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) +} + +fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, env: &HashMap) -> String { + let proxy_ports = proxy_loopback_ports_from_env(env); + if !proxy_ports.is_empty() { + let mut policy = + String::from("; allow outbound access only to configured loopback proxy endpoints\n"); + for port in proxy_ports { + policy.push_str(&format!( + "(allow network-outbound (remote ip \"localhost:{port}\"))\n" + )); + } + return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}"); + } + + if has_proxy_env_vars(env) { + // Proxy configuration is present but we could not infer any valid loopback endpoints. + // Fail closed to avoid silently widening network access in proxy-enforced sessions. + return String::new(); + } + + if sandbox_policy.has_full_network_access() { + // No proxy env is configured: retain the existing full-network behavior. + format!( + "(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}" + ) + } else { + String::new() + } +} + pub(crate) fn create_seatbelt_command_args( command: Vec, sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, + env: &HashMap, ) -> Vec { let (file_write_policy, file_write_dir_params) = { if sandbox_policy.has_full_disk_write_access() { @@ -112,11 +198,7 @@ pub(crate) fn create_seatbelt_command_args( }; // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. - let network_policy = if sandbox_policy.has_full_network_access() { - MACOS_SEATBELT_NETWORK_POLICY - } else { - "" - }; + let network_policy = dynamic_network_policy(sandbox_policy, env); let full_policy = format!( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" @@ -168,6 +250,7 @@ mod tests { use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use pretty_assertions::assert_eq; + use std::collections::HashMap; use std::fs; use std::path::Path; use std::path::PathBuf; @@ -184,6 +267,122 @@ mod tests { ); } + #[test] + fn create_seatbelt_args_routes_network_through_proxy_ports() { + let tmp = TempDir::new().expect("tempdir"); + let cwd = tmp.path().join("cwd"); + fs::create_dir_all(&cwd).expect("create cwd"); + let command = vec!["/bin/echo".to_string(), "ok".to_string()]; + + let mut env = HashMap::new(); + env.insert( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43128".to_string(), + ); + env.insert( + "ALL_PROXY".to_string(), + "socks5h://127.0.0.1:48081".to_string(), + ); + + let args = create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, &env); + let policy = args + .get(1) + .expect("seatbelt args should include policy at index 1"); + + assert!( + policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"), + "expected HTTP proxy port allow rule in policy:\n{policy}" + ); + assert!( + policy.contains("(allow network-outbound (remote ip \"localhost:48081\"))"), + "expected SOCKS proxy port allow rule in policy:\n{policy}" + ); + assert!( + !policy.contains("\n(allow network-outbound)\n"), + "policy should not include blanket outbound allowance when proxy ports are present:\n{policy}" + ); + } + + #[test] + fn create_seatbelt_args_fails_closed_for_invalid_proxy_env() { + let tmp = TempDir::new().expect("tempdir"); + let cwd = tmp.path().join("cwd"); + fs::create_dir_all(&cwd).expect("create cwd"); + let command = vec!["/bin/echo".to_string(), "ok".to_string()]; + + let mut env = HashMap::new(); + env.insert( + "HTTP_PROXY".to_string(), + "not-a-valid-proxy-url".to_string(), + ); + + let args = create_seatbelt_command_args( + command, + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + &cwd, + &env, + ); + let policy = args + .get(1) + .expect("seatbelt args should include policy at index 1"); + + assert!( + !policy.contains("\n(allow network-outbound)\n"), + "policy should not include blanket outbound allowance when proxy env is invalid:\n{policy}" + ); + assert!( + !policy.contains("(allow network-outbound (remote ip \"localhost:"), + "policy should not include proxy port allowance when proxy env is invalid:\n{policy}" + ); + } + + #[test] + fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { + let tmp = TempDir::new().expect("tempdir"); + let cwd = tmp.path().join("cwd"); + fs::create_dir_all(&cwd).expect("create cwd"); + let command = vec!["/bin/echo".to_string(), "ok".to_string()]; + + let mut env = HashMap::new(); + env.insert( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43128".to_string(), + ); + + let args = create_seatbelt_command_args( + command, + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + &cwd, + &env, + ); + let policy = args + .get(1) + .expect("seatbelt args should include policy at index 1"); + + assert!( + policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"), + "expected proxy endpoint allow rule in policy:\n{policy}" + ); + assert!( + !policy.contains("\n(allow network-outbound)\n"), + "policy should not include blanket outbound allowance when proxy is configured:\n{policy}" + ); + assert!( + !policy.contains("\n(allow network-inbound)\n"), + "policy should not include blanket inbound allowance when proxy is configured:\n{policy}" + ); + } + #[test] fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { // Create a temporary workspace with two writable roots: one containing @@ -227,7 +426,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd); + let args = + create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, &HashMap::new()); // Build the expected policy text using a raw string for readability. // Note that the policy includes: @@ -315,7 +515,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let write_hooks_file_args = create_seatbelt_command_args(shell_command_git, &policy, &cwd); + let write_hooks_file_args = + create_seatbelt_command_args(shell_command_git, &policy, &cwd, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_hooks_file_args) .current_dir(&cwd) @@ -346,7 +547,7 @@ mod tests { .map(std::string::ToString::to_string) .collect(); let write_allowed_file_args = - create_seatbelt_command_args(shell_command_allowed, &policy, &cwd); + create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_allowed_file_args) .current_dir(&cwd) @@ -406,7 +607,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args(shell_command, &policy, &cwd); + let args = create_seatbelt_command_args(shell_command, &policy, &cwd, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&args) @@ -436,7 +637,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let gitdir_args = create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd); + let gitdir_args = + create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&gitdir_args) .current_dir(&cwd) @@ -492,8 +694,12 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = - create_seatbelt_command_args(shell_command.clone(), &policy, vulnerable_root.as_path()); + let args = create_seatbelt_command_args( + shell_command.clone(), + &policy, + vulnerable_root.as_path(), + &HashMap::new(), + ); let tmpdir_env_var = std::env::var("TMPDIR") .ok() diff --git a/codex-rs/core/src/seatbelt_network_policy.sbpl b/codex-rs/core/src/seatbelt_network_policy.sbpl index 2a72f95fd32..3cf0b9c8a0e 100644 --- a/codex-rs/core/src/seatbelt_network_policy.sbpl +++ b/codex-rs/core/src/seatbelt_network_policy.sbpl @@ -1,8 +1,7 @@ ; when network access is enabled, these policies are added after those in seatbelt_base_policy.sbpl +; proxy-specific allow rules are injected by codex-core based on environment. ; Ref https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/network.sb;drc=f8f264d5e4e7509c913f4c60c2639d15905a07e4 -(allow network-outbound) -(allow network-inbound) (allow system-socket) (allow mach-lookup diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index f14fc5f13d8..0abd641708a 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -26,6 +26,8 @@ into this binary. writable roots are blocked by mounting `/dev/null` on the symlink or first missing component. - When enabled, the helper isolates the PID namespace via `--unshare-pid`. +- When enabled and network is restricted without proxy routing, the helper also + isolates the network namespace via `--unshare-net`. - When enabled, it mounts a fresh `/proc` via `--proc /proc` by default, but you can skip this in restrictive container environments with `--no-proc`. diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 4a60c65dd3b..a834f41f17a 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -26,11 +26,19 @@ pub(crate) struct BwrapOptions { /// This is the secure default, but some restrictive container environments /// deny `--proc /proc` even when PID namespaces are available. pub mount_proc: bool, + /// Whether to isolate networking with a dedicated network namespace. + /// + /// When true, bubblewrap adds `--unshare-net`, which removes access to the + /// host network stack and leaves only loopback inside the sandbox. + pub isolate_network: bool, } impl Default for BwrapOptions { fn default() -> Self { - Self { mount_proc: true } + Self { + mount_proc: true, + isolate_network: false, + } } } @@ -65,6 +73,9 @@ fn create_bwrap_flags( args.extend(create_filesystem_args(sandbox_policy, cwd)?); // Isolate the PID namespace. args.push("--unshare-pid".to_string()); + if options.isolate_network { + args.push("--unshare-net".to_string()); + } // Mount a fresh /proc unless the caller explicitly disables it. if options.mount_proc { args.push("--proc".to_string()); diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index d49491233cd..ee49b142d69 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -35,25 +35,30 @@ use seccompiler::apply_filter; /// /// This function is responsible for: /// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and -/// - installing the network seccomp filter when network access is disabled. +/// - installing the network seccomp filter when network access is disabled and +/// proxy-based routing is not enabled. /// /// Filesystem restrictions are intentionally handled by bubblewrap. pub(crate) fn apply_sandbox_policy_to_current_thread( sandbox_policy: &SandboxPolicy, cwd: &Path, apply_landlock_fs: bool, + allow_network_for_proxy: bool, ) -> Result<()> { + let install_network_seccomp = + !sandbox_policy.has_full_network_access() && !allow_network_for_proxy; + // `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents // setuid privilege elevation. Many `bwrap` deployments rely on setuid, so // we avoid this unless we need seccomp or we are explicitly using the // legacy Landlock filesystem pipeline. - if !sandbox_policy.has_full_network_access() + if install_network_seccomp || (apply_landlock_fs && !sandbox_policy.has_full_disk_write_access()) { set_no_new_privs()?; } - if !sandbox_policy.has_full_network_access() { + if install_network_seccomp { install_network_seccomp_filter_on_current_thread()?; } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index f5f0d9887aa..887403059df 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -40,6 +40,11 @@ pub struct LandlockCommand { #[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)] pub apply_seccomp_then_exec: bool, + /// Internal: permit network syscalls in restricted policies when we rely + /// on proxy environment variables for egress routing. + #[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)] + pub allow_network_for_proxy: bool, + /// When set, skip mounting a fresh `/proc` even though PID isolation is /// still enabled. This is primarily intended for restrictive container /// environments that deny `--proc /proc`. @@ -64,6 +69,7 @@ pub fn run_main() -> ! { sandbox_policy, use_bwrap_sandbox, apply_seccomp_then_exec, + allow_network_for_proxy, no_proc, command, } = LandlockCommand::parse(); @@ -75,18 +81,24 @@ pub fn run_main() -> ! { // Inner stage: apply seccomp/no_new_privs after bubblewrap has already // established the filesystem view. if apply_seccomp_then_exec { - if let Err(e) = - apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false) - { + if let Err(e) = apply_sandbox_policy_to_current_thread( + &sandbox_policy, + &sandbox_policy_cwd, + false, + allow_network_for_proxy, + ) { panic!("error applying Linux sandbox restrictions: {e:?}"); } exec_or_panic(command); } if sandbox_policy.has_full_disk_write_access() { - if let Err(e) = - apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false) - { + if let Err(e) = apply_sandbox_policy_to_current_thread( + &sandbox_policy, + &sandbox_policy_cwd, + false, + allow_network_for_proxy, + ) { panic!("error applying Linux sandbox restrictions: {e:?}"); } exec_or_panic(command); @@ -100,15 +112,25 @@ pub fn run_main() -> ! { &sandbox_policy_cwd, &sandbox_policy, use_bwrap_sandbox, + allow_network_for_proxy, command, ); - run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc); + run_bwrap_with_proc_fallback( + &sandbox_policy_cwd, + &sandbox_policy, + inner, + !no_proc, + allow_network_for_proxy, + ); } // Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled. - if let Err(e) = - apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, true) - { + if let Err(e) = apply_sandbox_policy_to_current_thread( + &sandbox_policy, + &sandbox_policy_cwd, + true, + allow_network_for_proxy, + ) { panic!("error applying legacy Linux sandbox restrictions: {e:?}"); } exec_or_panic(command); @@ -119,6 +141,7 @@ fn run_bwrap_with_proc_fallback( sandbox_policy: &codex_core::protocol::SandboxPolicy, inner: Vec, mount_proc: bool, + allow_network_for_proxy: bool, ) -> ! { let mut mount_proc = mount_proc; @@ -127,7 +150,10 @@ fn run_bwrap_with_proc_fallback( mount_proc = false; } - let options = BwrapOptions { mount_proc }; + let options = BwrapOptions { + mount_proc, + isolate_network: !sandbox_policy.has_full_network_access() && !allow_network_for_proxy, + }; let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options); exec_vendored_bwrap(argv); } @@ -164,7 +190,10 @@ fn preflight_proc_mount_support( preflight_command, sandbox_policy, sandbox_policy_cwd, - BwrapOptions { mount_proc: true }, + BwrapOptions { + mount_proc: true, + isolate_network: false, + }, ); let stderr = run_bwrap_in_child_capture_stderr(preflight_argv); !is_proc_mount_failure(stderr.as_str()) @@ -268,6 +297,7 @@ fn build_inner_seccomp_command( sandbox_policy_cwd: &Path, sandbox_policy: &codex_core::protocol::SandboxPolicy, use_bwrap_sandbox: bool, + allow_network_for_proxy: bool, command: Vec, ) -> Vec { let current_exe = match std::env::current_exe() { @@ -290,6 +320,9 @@ fn build_inner_seccomp_command( inner.push("--use-bwrap-sandbox".to_string()); inner.push("--apply-seccomp-then-exec".to_string()); } + if allow_network_for_proxy { + inner.push("--allow-network-for-proxy".to_string()); + } inner.push("--".to_string()); inner.extend(command); inner @@ -342,7 +375,10 @@ mod tests { vec!["/bin/true".to_string()], &SandboxPolicy::ReadOnly, Path::new("/"), - BwrapOptions { mount_proc: true }, + BwrapOptions { + mount_proc: true, + isolate_network: false, + }, ); assert_eq!( argv, @@ -366,4 +402,18 @@ mod tests { ] ); } + + #[test] + fn inserts_unshare_net_when_network_isolation_requested() { + let argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &SandboxPolicy::ReadOnly, + Path::new("/"), + BwrapOptions { + mount_proc: true, + isolate_network: true, + }, + ); + assert_eq!(argv.contains(&"--unshare-net".to_string()), true); + } } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index a974ce13b73..078af3d1661 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -20,10 +20,14 @@ pub use network_policy::NetworkPolicyDecider; pub use network_policy::NetworkPolicyRequest; pub use network_policy::NetworkPolicyRequestArgs; pub use network_policy::NetworkProtocol; +pub use proxy::ALL_PROXY_ENV_KEYS; pub use proxy::Args; +pub use proxy::DEFAULT_NO_PROXY_VALUE; +pub use proxy::NO_PROXY_ENV_KEYS; pub use proxy::NetworkProxy; pub use proxy::NetworkProxyBuilder; pub use proxy::NetworkProxyHandle; +pub use proxy::PROXY_URL_ENV_KEYS; pub use runtime::ConfigReloader; pub use runtime::ConfigState; pub use runtime::NetworkProxyState; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 46936dd5306..caa14c2954f 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -176,6 +176,7 @@ impl NetworkProxyBuilder { state, http_addr, socks_addr, + socks_enabled: current_cfg.network.enable_socks5, admin_addr, reserved_listeners, policy_decider: self.policy_decider, @@ -202,6 +203,7 @@ pub struct NetworkProxy { state: Arc, http_addr: SocketAddr, socks_addr: SocketAddr, + socks_enabled: bool, admin_addr: SocketAddr, reserved_listeners: Option>, policy_decider: Option>, @@ -229,18 +231,101 @@ impl PartialEq for NetworkProxy { impl Eq for NetworkProxy {} +pub const PROXY_URL_ENV_KEYS: &[&str] = &[ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "FTP_PROXY", + "GRPC_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "ftp_proxy", + "grpc_proxy", + "YARN_HTTP_PROXY", + "YARN_HTTPS_PROXY", + "npm_config_http_proxy", + "npm_config_https_proxy", + "npm_config_proxy", + "NPM_CONFIG_HTTP_PROXY", + "NPM_CONFIG_HTTPS_PROXY", + "NPM_CONFIG_PROXY", + "BUNDLE_HTTP_PROXY", + "BUNDLE_HTTPS_PROXY", + "PIP_PROXY", + "DOCKER_HTTP_PROXY", + "DOCKER_HTTPS_PROXY", +]; + +pub const ALL_PROXY_ENV_KEYS: &[&str] = &[ + "ALL_PROXY", + "all_proxy", + "FTP_PROXY", + "ftp_proxy", + "GRPC_PROXY", + "grpc_proxy", +]; + +pub const NO_PROXY_ENV_KEYS: &[&str] = &[ + "NO_PROXY", + "no_proxy", + "npm_config_noproxy", + "NPM_CONFIG_NOPROXY", + "YARN_NO_PROXY", + "BUNDLE_NO_PROXY", +]; + +pub const DEFAULT_NO_PROXY_VALUE: &str = "localhost,127.0.0.1,::1"; + +fn apply_proxy_env_overrides( + env: &mut HashMap, + http_addr: SocketAddr, + socks_addr: SocketAddr, + socks_enabled: bool, +) { + let http_proxy_url = format!("http://{http_addr}"); + let all_proxy_url = if socks_enabled { + format!("socks5h://{socks_addr}") + } else { + http_proxy_url.clone() + }; + + for key in PROXY_URL_ENV_KEYS { + env.insert((*key).to_string(), http_proxy_url.clone()); + } + for key in ALL_PROXY_ENV_KEYS { + env.insert((*key).to_string(), all_proxy_url.clone()); + } + for key in NO_PROXY_ENV_KEYS { + env.insert((*key).to_string(), DEFAULT_NO_PROXY_VALUE.to_string()); + } + + env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string()); + + env.insert("CLOUDSDK_PROXY_TYPE".to_string(), "https".to_string()); + env.insert( + "CLOUDSDK_PROXY_ADDRESS".to_string(), + http_addr.ip().to_string(), + ); + env.insert( + "CLOUDSDK_PROXY_PORT".to_string(), + http_addr.port().to_string(), + ); + + if socks_enabled { + env.insert("RSYNC_PROXY".to_string(), socks_addr.to_string()); + } +} + impl NetworkProxy { pub fn builder() -> NetworkProxyBuilder { NetworkProxyBuilder::default() } pub fn apply_to_env(&self, env: &mut HashMap) { - // Enforce proxying for all child processes when configured. We always override to ensure - // the proxy is actually used even if the caller passed conflicting environment variables. - let proxy_url = format!("http://{}", self.http_addr); - for key in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] { - env.insert(key.to_string(), proxy_url.clone()); - } + // Enforce proxying for child processes. We intentionally override existing values so + // command-level environment cannot bypass the managed proxy endpoint. + apply_proxy_env_overrides(env, self.http_addr, self.socks_addr, self.socks_enabled); } pub async fn run(&self) -> Result { @@ -406,6 +491,9 @@ mod tests { use super::*; use crate::config::NetworkProxySettings; use crate::state::network_proxy_state_for_policy; + use pretty_assertions::assert_eq; + use std::net::IpAddr; + use std::net::Ipv4Addr; #[tokio::test] async fn managed_proxy_builder_uses_loopback_ephemeral_ports() { @@ -462,4 +550,52 @@ mod tests { "127.0.0.1:48080".parse::().unwrap() ); } + + #[test] + fn apply_proxy_env_overrides_sets_common_tool_vars() { + let mut env = HashMap::new(); + apply_proxy_env_overrides( + &mut env, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), + true, + ); + + assert_eq!( + env.get("HTTP_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("npm_config_proxy"), + Some(&"http://127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("ALL_PROXY"), + Some(&"socks5h://127.0.0.1:8081".to_string()) + ); + assert_eq!( + env.get("NO_PROXY"), + Some(&DEFAULT_NO_PROXY_VALUE.to_string()) + ); + assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string())); + assert_eq!(env.get("CLOUDSDK_PROXY_PORT"), Some(&"3128".to_string())); + assert_eq!(env.get("RSYNC_PROXY"), Some(&"127.0.0.1:8081".to_string())); + } + + #[test] + fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks() { + let mut env = HashMap::new(); + apply_proxy_env_overrides( + &mut env, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), + false, + ); + + assert_eq!( + env.get("ALL_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); + assert_eq!(env.get("RSYNC_PROXY"), None); + } } From f8f1486006263365f0258bf29d49f0c34689640c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 20:44:34 -0800 Subject: [PATCH 02/23] feat(network-proxy): tune proxy env defaults for socks and local networks --- codex-rs/network-proxy/src/proxy.rs | 101 ++++++++++++++++++---------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index caa14c2954f..8fc3abb8735 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -236,12 +236,10 @@ pub const PROXY_URL_ENV_KEYS: &[&str] = &[ "HTTPS_PROXY", "ALL_PROXY", "FTP_PROXY", - "GRPC_PROXY", "http_proxy", "https_proxy", "all_proxy", "ftp_proxy", - "grpc_proxy", "YARN_HTTP_PROXY", "YARN_HTTPS_PROXY", "npm_config_http_proxy", @@ -257,14 +255,9 @@ pub const PROXY_URL_ENV_KEYS: &[&str] = &[ "DOCKER_HTTPS_PROXY", ]; -pub const ALL_PROXY_ENV_KEYS: &[&str] = &[ - "ALL_PROXY", - "all_proxy", - "FTP_PROXY", - "ftp_proxy", - "GRPC_PROXY", - "grpc_proxy", -]; +pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; + +const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"]; pub const NO_PROXY_ENV_KEYS: &[&str] = &[ "NO_PROXY", @@ -275,7 +268,17 @@ pub const NO_PROXY_ENV_KEYS: &[&str] = &[ "BUNDLE_NO_PROXY", ]; -pub const DEFAULT_NO_PROXY_VALUE: &str = "localhost,127.0.0.1,::1"; +pub const DEFAULT_NO_PROXY_VALUE: &str = concat!( + "localhost,127.0.0.1,::1,", + "*.local,.local,", + "169.254.0.0/16,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" +); + +fn set_env_keys(env: &mut HashMap, keys: &[&str], value: &str) { + for key in keys { + env.insert((*key).to_string(), value.to_string()); + } +} fn apply_proxy_env_overrides( env: &mut HashMap, @@ -284,36 +287,49 @@ fn apply_proxy_env_overrides( socks_enabled: bool, ) { let http_proxy_url = format!("http://{http_addr}"); - let all_proxy_url = if socks_enabled { - format!("socks5h://{socks_addr}") - } else { - http_proxy_url.clone() - }; + let socks_proxy_url = format!("socks5h://{socks_addr}"); + + // HTTP-based clients are best served by explicit HTTP proxy URLs. + set_env_keys( + env, + &[ + "HTTP_PROXY", + "HTTPS_PROXY", + "http_proxy", + "https_proxy", + "YARN_HTTP_PROXY", + "YARN_HTTPS_PROXY", + "npm_config_http_proxy", + "npm_config_https_proxy", + "npm_config_proxy", + "NPM_CONFIG_HTTP_PROXY", + "NPM_CONFIG_HTTPS_PROXY", + "NPM_CONFIG_PROXY", + "BUNDLE_HTTP_PROXY", + "BUNDLE_HTTPS_PROXY", + "PIP_PROXY", + "DOCKER_HTTP_PROXY", + "DOCKER_HTTPS_PROXY", + ], + &http_proxy_url, + ); - for key in PROXY_URL_ENV_KEYS { - env.insert((*key).to_string(), http_proxy_url.clone()); - } - for key in ALL_PROXY_ENV_KEYS { - env.insert((*key).to_string(), all_proxy_url.clone()); - } - for key in NO_PROXY_ENV_KEYS { - env.insert((*key).to_string(), DEFAULT_NO_PROXY_VALUE.to_string()); - } + // Keep local/private targets direct so local IPC and metadata endpoints avoid the proxy. + set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE); env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string()); - env.insert("CLOUDSDK_PROXY_TYPE".to_string(), "https".to_string()); - env.insert( - "CLOUDSDK_PROXY_ADDRESS".to_string(), - http_addr.ip().to_string(), - ); - env.insert( - "CLOUDSDK_PROXY_PORT".to_string(), - http_addr.port().to_string(), - ); - if socks_enabled { + set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url); + set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); env.insert("RSYNC_PROXY".to_string(), socks_addr.to_string()); + #[cfg(target_os = "macos")] + env.insert( + "GIT_SSH_COMMAND".to_string(), + format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"), + ); + } else { + set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url); } } @@ -573,13 +589,24 @@ mod tests { env.get("ALL_PROXY"), Some(&"socks5h://127.0.0.1:8081".to_string()) ); + assert_eq!( + env.get("FTP_PROXY"), + Some(&"socks5h://127.0.0.1:8081".to_string()) + ); assert_eq!( env.get("NO_PROXY"), Some(&DEFAULT_NO_PROXY_VALUE.to_string()) ); assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string())); - assert_eq!(env.get("CLOUDSDK_PROXY_PORT"), Some(&"3128".to_string())); assert_eq!(env.get("RSYNC_PROXY"), Some(&"127.0.0.1:8081".to_string())); + assert_eq!(env.get("GRPC_PROXY"), None); + #[cfg(target_os = "macos")] + assert_eq!( + env.get("GIT_SSH_COMMAND"), + Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string()) + ); + #[cfg(not(target_os = "macos"))] + assert_eq!(env.get("GIT_SSH_COMMAND"), None); } #[test] @@ -597,5 +624,7 @@ mod tests { Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!(env.get("RSYNC_PROXY"), None); + assert_eq!(env.get("FTP_PROXY"), None); + assert_eq!(env.get("GRPC_PROXY"), None); } } From 77cc4ce526b948702df1e308b5f244fbcebe0e29 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 20:56:28 -0800 Subject: [PATCH 03/23] fix(network-proxy): preserve existing git ssh wrappers with socks --- codex-rs/network-proxy/src/proxy.rs | 31 +++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 8fc3abb8735..c805f6c364e 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -324,10 +324,12 @@ fn apply_proxy_env_overrides( set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); env.insert("RSYNC_PROXY".to_string(), socks_addr.to_string()); #[cfg(target_os = "macos")] - env.insert( - "GIT_SSH_COMMAND".to_string(), - format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"), - ); + { + // Preserve existing SSH wrappers (for example: Secretive/Teleport setups) + // and only provide a SOCKS ProxyCommand fallback when one is not present. + env.entry("GIT_SSH_COMMAND".to_string()) + .or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'")); + } } else { set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url); } @@ -627,4 +629,25 @@ mod tests { assert_eq!(env.get("FTP_PROXY"), None); assert_eq!(env.get("GRPC_PROXY"), None); } + + #[cfg(target_os = "macos")] + #[test] + fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() { + let mut env = HashMap::new(); + env.insert( + "GIT_SSH_COMMAND".to_string(), + "ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string(), + ); + apply_proxy_env_overrides( + &mut env, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), + true, + ); + + assert_eq!( + env.get("GIT_SSH_COMMAND"), + Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string()) + ); + } } From ae7b7941c5bccc04177574f21600b04b2d8130cc Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 20:56:54 -0800 Subject: [PATCH 04/23] fix(network-proxy): stop exporting rsync proxy env --- codex-rs/network-proxy/src/proxy.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index c805f6c364e..e872a55b5fd 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -322,7 +322,6 @@ fn apply_proxy_env_overrides( if socks_enabled { set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url); set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); - env.insert("RSYNC_PROXY".to_string(), socks_addr.to_string()); #[cfg(target_os = "macos")] { // Preserve existing SSH wrappers (for example: Secretive/Teleport setups) @@ -600,7 +599,7 @@ mod tests { Some(&DEFAULT_NO_PROXY_VALUE.to_string()) ); assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string())); - assert_eq!(env.get("RSYNC_PROXY"), Some(&"127.0.0.1:8081".to_string())); + assert_eq!(env.get("RSYNC_PROXY"), None); assert_eq!(env.get("GRPC_PROXY"), None); #[cfg(target_os = "macos")] assert_eq!( From de13798dcc93658bed97e1a2ea31c23ebc1436f0 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 21:21:20 -0800 Subject: [PATCH 05/23] fix(seatbelt): constrain system-socket allowance --- codex-rs/core/src/seatbelt_network_policy.sbpl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/seatbelt_network_policy.sbpl b/codex-rs/core/src/seatbelt_network_policy.sbpl index 3cf0b9c8a0e..a0801d093b5 100644 --- a/codex-rs/core/src/seatbelt_network_policy.sbpl +++ b/codex-rs/core/src/seatbelt_network_policy.sbpl @@ -2,7 +2,13 @@ ; proxy-specific allow rules are injected by codex-core based on environment. ; Ref https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/network.sb;drc=f8f264d5e4e7509c913f4c60c2639d15905a07e4 -(allow system-socket) +; allow only safe AF_SYSTEM sockets used for local platform services. +(allow system-socket + (require-all + (socket-domain AF_SYSTEM) + (socket-protocol 2) + ) +) (allow mach-lookup ; Used to look up the _CS_DARWIN_USER_CACHE_DIR in the sandbox. From 3c9cf8d79a7ca41bdd0354daad583efa745cde8d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 22:25:08 -0800 Subject: [PATCH 06/23] feat(core): thread proxy env through spawn request --- codex-rs/core/src/exec.rs | 22 ++++--- codex-rs/core/src/landlock.rs | 10 ++-- codex-rs/core/src/seatbelt.rs | 70 +++++++++++++++++++++-- codex-rs/core/src/spawn.rs | 36 +++++++++--- codex-rs/core/src/tools/runtimes/shell.rs | 7 ++- codex-rs/network-proxy/src/lib.rs | 1 + codex-rs/network-proxy/src/proxy.rs | 26 ++++++++- 7 files changed, 141 insertions(+), 31 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 2172caba8a4..44aa78cc76d 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -29,6 +29,7 @@ use crate::sandboxing::CommandSpec; use crate::sandboxing::ExecEnv; use crate::sandboxing::SandboxManager; use crate::sandboxing::SandboxPermissions; +use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use crate::text_encoding::bytes_to_string_smart; @@ -225,7 +226,7 @@ pub(crate) async fn execute_exec_env( let ExecEnv { command, cwd, - mut env, + env, expiration, sandbox, windows_sandbox_level, @@ -234,10 +235,6 @@ pub(crate) async fn execute_exec_env( arg0, } = env; - if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); - } - let params = ExecParams { command, cwd, @@ -694,7 +691,7 @@ async fn exec( command, cwd, env, - network: _, + network, arg0, expiration, windows_sandbox_level: _, @@ -708,15 +705,16 @@ async fn exec( )) })?; let arg0_ref = arg0.as_deref(); - let child = spawn_child_async( - PathBuf::from(program), - args.into(), - arg0_ref, + let child = spawn_child_async(SpawnChildRequest { + program: PathBuf::from(program), + args: args.into(), + arg0: arg0_ref, cwd, sandbox_policy, - StdioPolicy::RedirectForShellTool, + network: network.as_ref(), + stdio_policy: StdioPolicy::RedirectForShellTool, env, - ) + }) .await?; consume_truncated_output(child, expiration, stdout_stream).await } diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index f76ea00532c..ba092ff5984 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,4 +1,5 @@ use crate::protocol::SandboxPolicy; +use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use codex_network_proxy::PROXY_URL_ENV_KEYS; @@ -37,15 +38,16 @@ where !sandbox_policy.has_full_network_access() && has_proxy_env_vars(&env), ); let arg0 = Some("codex-linux-sandbox"); - spawn_child_async( - codex_linux_sandbox_exe.as_ref().to_path_buf(), + spawn_child_async(SpawnChildRequest { + program: codex_linux_sandbox_exe.as_ref().to_path_buf(), args, arg0, - command_cwd, + cwd: command_cwd, sandbox_policy, + network: None, stdio_policy, env, - ) + }) .await } diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index ededb6a0647..1fcbd5f8b24 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -1,5 +1,6 @@ #![cfg(target_os = "macos")] +use codex_network_proxy::ALLOW_LOCAL_BINDING_ENV_KEY; use codex_network_proxy::PROXY_URL_ENV_KEYS; use std::collections::BTreeSet; use std::collections::HashMap; @@ -11,6 +12,7 @@ use url::Url; use crate::protocol::SandboxPolicy; use crate::spawn::CODEX_SANDBOX_ENV_VAR; +use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; @@ -34,15 +36,16 @@ pub async fn spawn_command_under_seatbelt( let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, &env); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - spawn_child_async( - PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE), + spawn_child_async(SpawnChildRequest { + program: PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE), args, arg0, - command_cwd, + cwd: command_cwd, sandbox_policy, + network: None, stdio_policy, env, - ) + }) .await } @@ -99,11 +102,24 @@ fn has_proxy_env_vars(env: &HashMap) -> bool { .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) } +fn local_binding_enabled(env: &HashMap) -> bool { + env.get(ALLOW_LOCAL_BINDING_ENV_KEY).is_some_and(|value| { + let trimmed = value.trim(); + trimmed == "1" || trimmed.eq_ignore_ascii_case("true") + }) +} + fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, env: &HashMap) -> String { let proxy_ports = proxy_loopback_ports_from_env(env); if !proxy_ports.is_empty() { let mut policy = String::from("; allow outbound access only to configured loopback proxy endpoints\n"); + if local_binding_enabled(env) { + policy.push_str("; allow localhost-only binding and loopback traffic\n"); + policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n"); + policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n"); + policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n"); + } for port in proxy_ports { policy.push_str(&format!( "(allow network-outbound (remote ip \"localhost:{port}\"))\n" @@ -244,6 +260,7 @@ fn macos_dir_params() -> Vec<(String, PathBuf)> { #[cfg(test)] mod tests { + use super::ALLOW_LOCAL_BINDING_ENV_KEY; use super::MACOS_SEATBELT_BASE_POLICY; use super::create_seatbelt_command_args; use super::macos_dir_params; @@ -301,6 +318,51 @@ mod tests { !policy.contains("\n(allow network-outbound)\n"), "policy should not include blanket outbound allowance when proxy ports are present:\n{policy}" ); + assert!( + !policy.contains("(allow network-bind (local ip \"localhost:*\"))"), + "policy should not allow loopback binding unless explicitly enabled:\n{policy}" + ); + assert!( + !policy.contains("(allow network-inbound (local ip \"localhost:*\"))"), + "policy should not allow loopback inbound unless explicitly enabled:\n{policy}" + ); + } + + #[test] + fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { + let tmp = TempDir::new().expect("tempdir"); + let cwd = tmp.path().join("cwd"); + fs::create_dir_all(&cwd).expect("create cwd"); + let command = vec!["/bin/echo".to_string(), "ok".to_string()]; + + let mut env = HashMap::new(); + env.insert( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43128".to_string(), + ); + env.insert(ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), "1".to_string()); + + let args = create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, &env); + let policy = args + .get(1) + .expect("seatbelt args should include policy at index 1"); + + assert!( + policy.contains("(allow network-bind (local ip \"localhost:*\"))"), + "policy should allow loopback binding when explicitly enabled:\n{policy}" + ); + assert!( + policy.contains("(allow network-inbound (local ip \"localhost:*\"))"), + "policy should allow loopback inbound when explicitly enabled:\n{policy}" + ); + assert!( + policy.contains("(allow network-outbound (remote ip \"localhost:*\"))"), + "policy should allow loopback outbound when explicitly enabled:\n{policy}" + ); + assert!( + !policy.contains("\n(allow network-outbound)\n"), + "policy should keep proxy-routed behavior without blanket outbound allowance:\n{policy}" + ); } #[test] diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index b2a507fda7a..67e6ace0447 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -1,3 +1,4 @@ +use codex_network_proxy::NetworkProxy; use std::collections::HashMap; use std::path::PathBuf; use std::process::Stdio; @@ -35,15 +36,29 @@ pub enum StdioPolicy { /// For now, we take `SandboxPolicy` as a parameter to spawn_child() because /// we need to determine whether to set the /// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable. -pub(crate) async fn spawn_child_async( - program: PathBuf, - args: Vec, - #[cfg_attr(not(unix), allow(unused_variables))] arg0: Option<&str>, - cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - stdio_policy: StdioPolicy, - env: HashMap, -) -> std::io::Result { +pub(crate) struct SpawnChildRequest<'a> { + pub program: PathBuf, + pub args: Vec, + pub arg0: Option<&'a str>, + pub cwd: PathBuf, + pub sandbox_policy: &'a SandboxPolicy, + pub network: Option<&'a NetworkProxy>, + pub stdio_policy: StdioPolicy, + pub env: HashMap, +} + +pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io::Result { + let SpawnChildRequest { + program, + args, + arg0, + cwd, + sandbox_policy, + network, + stdio_policy, + mut env, + } = request; + trace!( "spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {sandbox_policy:?} {stdio_policy:?} {env:?}" ); @@ -53,6 +68,9 @@ pub(crate) async fn spawn_child_async( cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from)); cmd.args(args); cmd.current_dir(cwd); + if let Some(network) = network { + network.apply_to_env(&mut env); + } cmd.env_clear(); cmd.envs(env); diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 106092ca0f9..fe8e2e57aa8 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -158,10 +158,15 @@ impl ToolRuntime for ShellRuntime { command }; + let mut env = req.env.clone(); + if let Some(network) = req.network.as_ref() { + network.apply_to_env(&mut env); + } + let spec = build_command_spec( &command, &req.cwd, - &req.env, + &env, req.timeout_ms.into(), req.sandbox_permissions, req.justification.clone(), diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 078af3d1661..060dc1e35d4 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -21,6 +21,7 @@ pub use network_policy::NetworkPolicyRequest; pub use network_policy::NetworkPolicyRequestArgs; pub use network_policy::NetworkProtocol; pub use proxy::ALL_PROXY_ENV_KEYS; +pub use proxy::ALLOW_LOCAL_BINDING_ENV_KEY; pub use proxy::Args; pub use proxy::DEFAULT_NO_PROXY_VALUE; pub use proxy::NO_PROXY_ENV_KEYS; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index e872a55b5fd..1c136eaf04a 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -177,6 +177,7 @@ impl NetworkProxyBuilder { http_addr, socks_addr, socks_enabled: current_cfg.network.enable_socks5, + allow_local_binding: current_cfg.network.allow_local_binding, admin_addr, reserved_listeners, policy_decider: self.policy_decider, @@ -204,6 +205,7 @@ pub struct NetworkProxy { http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, + allow_local_binding: bool, admin_addr: SocketAddr, reserved_listeners: Option>, policy_decider: Option>, @@ -225,6 +227,7 @@ impl PartialEq for NetworkProxy { fn eq(&self, other: &Self) -> bool { self.http_addr == other.http_addr && self.socks_addr == other.socks_addr + && self.allow_local_binding == other.allow_local_binding && self.admin_addr == other.admin_addr } } @@ -256,6 +259,7 @@ pub const PROXY_URL_ENV_KEYS: &[&str] = &[ ]; pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; +pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"]; @@ -285,9 +289,18 @@ fn apply_proxy_env_overrides( http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, + allow_local_binding: bool, ) { let http_proxy_url = format!("http://{http_addr}"); let socks_proxy_url = format!("socks5h://{socks_addr}"); + env.insert( + ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), + if allow_local_binding { + "1".to_string() + } else { + "0".to_string() + }, + ); // HTTP-based clients are best served by explicit HTTP proxy URLs. set_env_keys( @@ -342,7 +355,13 @@ impl NetworkProxy { pub fn apply_to_env(&self, env: &mut HashMap) { // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. - apply_proxy_env_overrides(env, self.http_addr, self.socks_addr, self.socks_enabled); + apply_proxy_env_overrides( + env, + self.http_addr, + self.socks_addr, + self.socks_enabled, + self.allow_local_binding, + ); } pub async fn run(&self) -> Result { @@ -576,6 +595,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, + false, ); assert_eq!( @@ -598,6 +618,7 @@ mod tests { env.get("NO_PROXY"), Some(&DEFAULT_NO_PROXY_VALUE.to_string()) ); + assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"0".to_string())); assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string())); assert_eq!(env.get("RSYNC_PROXY"), None); assert_eq!(env.get("GRPC_PROXY"), None); @@ -618,12 +639,14 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), false, + true, ); assert_eq!( env.get("ALL_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); + assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string())); assert_eq!(env.get("RSYNC_PROXY"), None); assert_eq!(env.get("FTP_PROXY"), None); assert_eq!(env.get("GRPC_PROXY"), None); @@ -642,6 +665,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, + false, ); assert_eq!( From c477c93d7c03b392179a8d5c4baae838d7fa70f2 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 22:41:00 -0800 Subject: [PATCH 07/23] fix(sandbox): dedupe proxy gating and align debug env --- codex-rs/cli/src/debug_sandbox.rs | 5 ++++- codex-rs/core/src/landlock.rs | 9 ++++++++- codex-rs/core/src/sandboxing/mod.rs | 13 +++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 30352cbb831..d7c0b2be43b 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -130,7 +130,10 @@ async fn run_command_under_sandbox( let sandbox_policy_cwd = cwd.clone(); let stdio_policy = StdioPolicy::Inherit; - let env = create_env(&config.shell_environment_policy, None); + let mut env = create_env(&config.shell_environment_policy, None); + if let Some(network) = config.network.as_ref() { + network.apply_to_env(&mut env); + } // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio. if let SandboxType::Windows = sandbox_type { diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index ba092ff5984..ad78f679809 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -35,7 +35,7 @@ where sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, - !sandbox_policy.has_full_network_access() && has_proxy_env_vars(&env), + allow_network_for_proxy(sandbox_policy, &env), ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -51,6 +51,13 @@ where .await } +pub(crate) fn allow_network_for_proxy( + sandbox_policy: &SandboxPolicy, + env: &HashMap, +) -> bool { + !sandbox_policy.has_full_network_access() && has_proxy_env_vars(env) +} + /// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. /// /// The helper performs the actual sandboxing (bubblewrap + seccomp) after diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index be48039b234..9f101820733 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -11,6 +11,7 @@ use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_env; +use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args; use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] @@ -22,7 +23,6 @@ use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; -use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_protocol::config_types::WindowsSandboxLevel; pub use codex_protocol::models::SandboxPermissions; use std::collections::HashMap; @@ -160,14 +160,13 @@ impl SandboxManager { SandboxType::LinuxSeccomp => { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let allow_network_for_proxy = - !policy.has_full_network_access() && has_proxy_env_vars(&env); + let allow_proxy_network = allow_network_for_proxy(policy, &env); let mut args = create_linux_sandbox_command_args( command.clone(), policy, sandbox_policy_cwd, use_linux_sandbox_bwrap, - allow_network_for_proxy, + allow_proxy_network, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); @@ -208,12 +207,6 @@ impl SandboxManager { } } -fn has_proxy_env_vars(env: &HashMap) -> bool { - PROXY_URL_ENV_KEYS - .iter() - .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) -} - pub async fn execute_env( env: ExecEnv, policy: &SandboxPolicy, From e72c7d11f38e9c9bd86b24a1acdcfccb9800f28b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 22:50:19 -0800 Subject: [PATCH 08/23] refactor(network): canonicalize proxy env key handling --- codex-rs/core/src/landlock.rs | 10 ++---- codex-rs/core/src/seatbelt.rs | 12 +++---- codex-rs/network-proxy/src/lib.rs | 2 ++ codex-rs/network-proxy/src/proxy.rs | 49 ++++++++++++++++++++++++----- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index ad78f679809..f5ed95f14a2 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -2,7 +2,7 @@ use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; -use codex_network_proxy::PROXY_URL_ENV_KEYS; +use codex_network_proxy::has_proxy_url_env_vars; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -55,7 +55,7 @@ pub(crate) fn allow_network_for_proxy( sandbox_policy: &SandboxPolicy, env: &HashMap, ) -> bool { - !sandbox_policy.has_full_network_access() && has_proxy_env_vars(env) + !sandbox_policy.has_full_network_access() && has_proxy_url_env_vars(env) } /// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. @@ -102,12 +102,6 @@ pub(crate) fn create_linux_sandbox_command_args( linux_cmd } -fn has_proxy_env_vars(env: &HashMap) -> bool { - PROXY_URL_ENV_KEYS - .iter() - .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) -} - #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 1fcbd5f8b24..56d05222b83 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -2,6 +2,8 @@ use codex_network_proxy::ALLOW_LOCAL_BINDING_ENV_KEY; use codex_network_proxy::PROXY_URL_ENV_KEYS; +use codex_network_proxy::has_proxy_url_env_vars; +use codex_network_proxy::proxy_url_env_value; use std::collections::BTreeSet; use std::collections::HashMap; use std::ffi::CStr; @@ -64,7 +66,7 @@ fn proxy_scheme_default_port(scheme: &str) -> u16 { fn proxy_loopback_ports_from_env(env: &HashMap) -> Vec { let mut ports = BTreeSet::new(); for key in PROXY_URL_ENV_KEYS { - let Some(proxy_url) = env.get(*key) else { + let Some(proxy_url) = proxy_url_env_value(env, key) else { continue; }; let trimmed = proxy_url.trim(); @@ -96,12 +98,6 @@ fn proxy_loopback_ports_from_env(env: &HashMap) -> Vec { ports.into_iter().collect() } -fn has_proxy_env_vars(env: &HashMap) -> bool { - PROXY_URL_ENV_KEYS - .iter() - .any(|key| env.get(*key).is_some_and(|value| !value.trim().is_empty())) -} - fn local_binding_enabled(env: &HashMap) -> bool { env.get(ALLOW_LOCAL_BINDING_ENV_KEY).is_some_and(|value| { let trimmed = value.trim(); @@ -128,7 +124,7 @@ fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, env: &HashMap( + env: &'a HashMap, + canonical_key: &str, +) -> Option<&'a str> { + if let Some(value) = env.get(canonical_key) { + return Some(value.as_str()); + } + let lower_key = canonical_key.to_ascii_lowercase(); + env.get(lower_key.as_str()).map(String::as_str) +} + +pub fn has_proxy_url_env_vars(env: &HashMap) -> bool { + PROXY_URL_ENV_KEYS + .iter() + .any(|key| proxy_url_env_value(env, key).is_some_and(|value| !value.trim().is_empty())) +} + fn set_env_keys(env: &mut HashMap, keys: &[&str], value: &str) { for key in keys { env.insert((*key).to_string(), value.to_string()); @@ -587,6 +597,31 @@ mod tests { ); } + #[test] + fn proxy_url_env_value_resolves_lowercase_aliases() { + let mut env = HashMap::new(); + env.insert( + "http_proxy".to_string(), + "http://127.0.0.1:3128".to_string(), + ); + + assert_eq!( + proxy_url_env_value(&env, "HTTP_PROXY"), + Some("http://127.0.0.1:3128") + ); + } + + #[test] + fn has_proxy_url_env_vars_detects_lowercase_aliases() { + let mut env = HashMap::new(); + env.insert( + "all_proxy".to_string(), + "socks5h://127.0.0.1:8081".to_string(), + ); + + assert_eq!(has_proxy_url_env_vars(&env), true); + } + #[test] fn apply_proxy_env_overrides_sets_common_tool_vars() { let mut env = HashMap::new(); From 982ff820e284123f86b20d830f9017b7440aeb77 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sun, 8 Feb 2026 23:14:27 -0800 Subject: [PATCH 09/23] test(network): trim removed proxy var assertions --- codex-rs/network-proxy/src/proxy.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 447a9fe9f7d..6388854b43a 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -655,8 +655,6 @@ mod tests { ); assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"0".to_string())); assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string())); - assert_eq!(env.get("RSYNC_PROXY"), None); - assert_eq!(env.get("GRPC_PROXY"), None); #[cfg(target_os = "macos")] assert_eq!( env.get("GIT_SSH_COMMAND"), @@ -682,9 +680,6 @@ mod tests { Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string())); - assert_eq!(env.get("RSYNC_PROXY"), None); - assert_eq!(env.get("FTP_PROXY"), None); - assert_eq!(env.get("GRPC_PROXY"), None); } #[cfg(target_os = "macos")] From e6238a43b6be8f9b8e113ad42ea0e905aafca98d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 14:28:36 -0800 Subject: [PATCH 10/23] fix(linux-sandbox): keep restricted network isolation with proxy env --- codex-rs/core/src/landlock.rs | 24 ++++++++++++++++---- codex-rs/linux-sandbox/src/landlock.rs | 8 +++---- codex-rs/linux-sandbox/src/linux_run_main.rs | 16 ++++--------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index f5ed95f14a2..e345afb0a20 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_network_proxy::has_proxy_url_env_vars; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -52,10 +51,13 @@ where } pub(crate) fn allow_network_for_proxy( - sandbox_policy: &SandboxPolicy, - env: &HashMap, + _sandbox_policy: &SandboxPolicy, + _env: &HashMap, ) -> bool { - !sandbox_policy.has_full_network_access() && has_proxy_url_env_vars(env) + // Proxy environment variables are advisory only. In restricted policies we + // keep network sandboxing fail-closed unless we have explicit proxy-only + // enforcement in the sandbox runtime. + false } /// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. @@ -139,4 +141,18 @@ mod tests { true ); } + + #[test] + fn proxy_env_does_not_bypass_restricted_network_policy() { + let mut env = HashMap::new(); + env.insert( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:8080".to_string(), + ); + + assert_eq!( + allow_network_for_proxy(&SandboxPolicy::ReadOnly, &env), + false + ); + } } diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index ee49b142d69..a4ac62426a8 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -35,18 +35,16 @@ use seccompiler::apply_filter; /// /// This function is responsible for: /// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and -/// - installing the network seccomp filter when network access is disabled and -/// proxy-based routing is not enabled. +/// - installing the network seccomp filter when network access is disabled. /// /// Filesystem restrictions are intentionally handled by bubblewrap. pub(crate) fn apply_sandbox_policy_to_current_thread( sandbox_policy: &SandboxPolicy, cwd: &Path, apply_landlock_fs: bool, - allow_network_for_proxy: bool, + _allow_network_for_proxy: bool, ) -> Result<()> { - let install_network_seccomp = - !sandbox_policy.has_full_network_access() && !allow_network_for_proxy; + let install_network_seccomp = !sandbox_policy.has_full_network_access(); // `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents // setuid privilege elevation. Many `bwrap` deployments rely on setuid, so diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 887403059df..4ae7d91b778 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -40,8 +40,9 @@ pub struct LandlockCommand { #[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)] pub apply_seccomp_then_exec: bool, - /// Internal: permit network syscalls in restricted policies when we rely - /// on proxy environment variables for egress routing. + /// Internal compatibility flag. + /// + /// Proxy environment variables do not disable restricted-network sandboxing. #[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)] pub allow_network_for_proxy: bool, @@ -115,13 +116,7 @@ pub fn run_main() -> ! { allow_network_for_proxy, command, ); - run_bwrap_with_proc_fallback( - &sandbox_policy_cwd, - &sandbox_policy, - inner, - !no_proc, - allow_network_for_proxy, - ); + run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc); } // Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled. @@ -141,7 +136,6 @@ fn run_bwrap_with_proc_fallback( sandbox_policy: &codex_core::protocol::SandboxPolicy, inner: Vec, mount_proc: bool, - allow_network_for_proxy: bool, ) -> ! { let mut mount_proc = mount_proc; @@ -152,7 +146,7 @@ fn run_bwrap_with_proc_fallback( let options = BwrapOptions { mount_proc, - isolate_network: !sandbox_policy.has_full_network_access() && !allow_network_for_proxy, + isolate_network: !sandbox_policy.has_full_network_access(), }; let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options); exec_vendored_bwrap(argv); From c4764ec0111d74f1ced9da36610b12fed17c06f4 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 14:49:21 -0800 Subject: [PATCH 11/23] refactor(sandbox): thread network context through seatbelt and landlock --- codex-rs/cli/src/debug_sandbox.rs | 2 + codex-rs/core/src/exec.rs | 1 + codex-rs/core/src/landlock.rs | 18 ++--- codex-rs/core/src/sandboxing/mod.rs | 13 +++- codex-rs/core/src/seatbelt.rs | 77 +++++++++++++++---- .../core/src/tools/runtimes/apply_patch.rs | 2 +- codex-rs/core/src/tools/runtimes/shell.rs | 2 +- .../core/src/tools/runtimes/unified_exec.rs | 2 +- codex-rs/core/src/tools/sandboxing.rs | 3 + codex-rs/core/tests/suite/seatbelt.rs | 3 + codex-rs/exec/tests/suite/sandbox.rs | 2 + 11 files changed, 91 insertions(+), 34 deletions(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index d7c0b2be43b..308e835837f 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -225,6 +225,7 @@ async fn run_command_under_sandbox( config.sandbox_policy.get(), sandbox_policy_cwd.as_path(), stdio_policy, + None, env, ) .await? @@ -244,6 +245,7 @@ async fn run_command_under_sandbox( sandbox_policy_cwd.as_path(), use_bwrap_sandbox, stdio_policy, + None, env, ) .await? diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 44aa78cc76d..4555b3715f0 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -206,6 +206,7 @@ pub async fn process_exec_tool_call( spec, policy: sandbox_policy, sandbox: sandbox_type, + network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), use_linux_sandbox_bwrap, diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index e345afb0a20..bea057e4f45 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_network_proxy::NetworkProxy; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -24,6 +25,7 @@ pub async fn spawn_command_under_linux_sandbox

( sandbox_policy_cwd: &Path, use_bwrap_sandbox: bool, stdio_policy: StdioPolicy, + network: Option<&NetworkProxy>, env: HashMap, ) -> std::io::Result where @@ -34,7 +36,7 @@ where sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, - allow_network_for_proxy(sandbox_policy, &env), + allow_network_for_proxy(sandbox_policy, network), ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -43,7 +45,7 @@ where arg0, cwd: command_cwd, sandbox_policy, - network: None, + network, stdio_policy, env, }) @@ -52,7 +54,7 @@ where pub(crate) fn allow_network_for_proxy( _sandbox_policy: &SandboxPolicy, - _env: &HashMap, + _network: Option<&NetworkProxy>, ) -> bool { // Proxy environment variables are advisory only. In restricted policies we // keep network sandboxing fail-closed unless we have explicit proxy-only @@ -143,15 +145,9 @@ mod tests { } #[test] - fn proxy_env_does_not_bypass_restricted_network_policy() { - let mut env = HashMap::new(); - env.insert( - "HTTP_PROXY".to_string(), - "http://127.0.0.1:8080".to_string(), - ); - + fn proxy_network_does_not_bypass_restricted_network_policy() { assert_eq!( - allow_network_for_proxy(&SandboxPolicy::ReadOnly, &env), + allow_network_for_proxy(&SandboxPolicy::ReadOnly, None), false ); } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 9f101820733..85e85ab494b 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -60,6 +60,7 @@ pub(crate) struct SandboxTransformRequest<'a> { pub spec: CommandSpec, pub policy: &'a SandboxPolicy, pub sandbox: SandboxType, + pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, pub use_linux_sandbox_bwrap: bool, @@ -125,6 +126,7 @@ impl SandboxManager { mut spec, policy, sandbox, + network, sandbox_policy_cwd, codex_linux_sandbox_exe, use_linux_sandbox_bwrap, @@ -148,8 +150,13 @@ impl SandboxManager { SandboxType::MacosSeatbelt => { let mut seatbelt_env = HashMap::new(); seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - let mut args = - create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd, &env); + let mut args = create_seatbelt_command_args( + command.clone(), + policy, + sandbox_policy_cwd, + network, + &env, + ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); full_command.append(&mut args); @@ -160,7 +167,7 @@ impl SandboxManager { SandboxType::LinuxSeccomp => { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let allow_proxy_network = allow_network_for_proxy(policy, &env); + let allow_proxy_network = allow_network_for_proxy(policy, network); let mut args = create_linux_sandbox_command_args( command.clone(), policy, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 56d05222b83..48152049a39 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -1,6 +1,7 @@ #![cfg(target_os = "macos")] use codex_network_proxy::ALLOW_LOCAL_BINDING_ENV_KEY; +use codex_network_proxy::NetworkProxy; use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_network_proxy::has_proxy_url_env_vars; use codex_network_proxy::proxy_url_env_value; @@ -33,9 +34,11 @@ pub async fn spawn_command_under_seatbelt( sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, stdio_policy: StdioPolicy, + network: Option<&NetworkProxy>, mut env: HashMap, ) -> std::io::Result { - let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, &env); + let args = + create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, network, &env); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); spawn_child_async(SpawnChildRequest { @@ -44,7 +47,7 @@ pub async fn spawn_command_under_seatbelt( arg0, cwd: command_cwd, sandbox_policy, - network: None, + network, stdio_policy, env, }) @@ -105,12 +108,30 @@ fn local_binding_enabled(env: &HashMap) -> bool { }) } -fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, env: &HashMap) -> String { - let proxy_ports = proxy_loopback_ports_from_env(env); +fn proxy_env_from_network(network: &NetworkProxy) -> HashMap { + let mut env = HashMap::new(); + network.apply_to_env(&mut env); + env +} + +fn dynamic_network_policy( + sandbox_policy: &SandboxPolicy, + network: Option<&NetworkProxy>, + env: &HashMap, +) -> String { + let proxy_env; + let policy_env = if let Some(network) = network { + proxy_env = proxy_env_from_network(network); + &proxy_env + } else { + env + }; + + let proxy_ports = proxy_loopback_ports_from_env(policy_env); if !proxy_ports.is_empty() { let mut policy = String::from("; allow outbound access only to configured loopback proxy endpoints\n"); - if local_binding_enabled(env) { + if local_binding_enabled(policy_env) { policy.push_str("; allow localhost-only binding and loopback traffic\n"); policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n"); policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n"); @@ -124,7 +145,7 @@ fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, env: &HashMap, sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, + network: Option<&NetworkProxy>, env: &HashMap, ) -> Vec { let (file_write_policy, file_write_dir_params) = { @@ -210,7 +232,7 @@ pub(crate) fn create_seatbelt_command_args( }; // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. - let network_policy = dynamic_network_policy(sandbox_policy, env); + let network_policy = dynamic_network_policy(sandbox_policy, network, env); let full_policy = format!( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" @@ -297,7 +319,8 @@ mod tests { "socks5h://127.0.0.1:48081".to_string(), ); - let args = create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, &env); + let args = + create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, None, &env); let policy = args .get(1) .expect("seatbelt args should include policy at index 1"); @@ -338,7 +361,8 @@ mod tests { ); env.insert(ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), "1".to_string()); - let args = create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, &env); + let args = + create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, None, &env); let policy = args .get(1) .expect("seatbelt args should include policy at index 1"); @@ -383,6 +407,7 @@ mod tests { exclude_slash_tmp: false, }, &cwd, + None, &env, ); let policy = args @@ -421,6 +446,7 @@ mod tests { exclude_slash_tmp: false, }, &cwd, + None, &env, ); let policy = args @@ -484,8 +510,13 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = - create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, &HashMap::new()); + let args = create_seatbelt_command_args( + shell_command.clone(), + &policy, + &cwd, + None, + &HashMap::new(), + ); // Build the expected policy text using a raw string for readability. // Note that the policy includes: @@ -574,7 +605,7 @@ mod tests { .map(std::string::ToString::to_string) .collect(); let write_hooks_file_args = - create_seatbelt_command_args(shell_command_git, &policy, &cwd, &HashMap::new()); + create_seatbelt_command_args(shell_command_git, &policy, &cwd, None, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_hooks_file_args) .current_dir(&cwd) @@ -604,8 +635,13 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let write_allowed_file_args = - create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, &HashMap::new()); + let write_allowed_file_args = create_seatbelt_command_args( + shell_command_allowed, + &policy, + &cwd, + None, + &HashMap::new(), + ); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_allowed_file_args) .current_dir(&cwd) @@ -665,7 +701,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args(shell_command, &policy, &cwd, &HashMap::new()); + let args = + create_seatbelt_command_args(shell_command, &policy, &cwd, None, &HashMap::new()); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&args) @@ -695,8 +732,13 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let gitdir_args = - create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, &HashMap::new()); + let gitdir_args = create_seatbelt_command_args( + shell_command_gitdir, + &policy, + &cwd, + None, + &HashMap::new(), + ); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&gitdir_args) .current_dir(&cwd) @@ -756,6 +798,7 @@ mod tests { shell_command.clone(), &policy, vulnerable_root.as_path(), + None, &HashMap::new(), ); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index e87b40661b2..f1eb0c6d075 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -151,7 +151,7 @@ impl ToolRuntime for ApplyPatchRuntime { ) -> Result { let spec = Self::build_command_spec(req)?; let env = attempt - .env_for(spec) + .env_for(spec, None) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, attempt.policy, None, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index fe8e2e57aa8..7c27e6d7b2d 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -172,7 +172,7 @@ impl ToolRuntime for ShellRuntime { req.justification.clone(), )?; let env = attempt - .env_for(spec) + .env_for(spec, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env( env, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index a36ee68ed9e..d286adb2178 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -176,7 +176,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt ) .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; let exec_env = attempt - .env_for(spec) + .env_for(spec, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; self.manager .open_session_with_exec_env(&exec_env, req.tty) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index d50e5925300..06af8a5ce0c 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -12,6 +12,7 @@ use crate::sandboxing::CommandSpec; use crate::sandboxing::SandboxManager; use crate::sandboxing::SandboxTransformError; use crate::state::SessionServices; +use codex_network_proxy::NetworkProxy; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; @@ -282,12 +283,14 @@ impl<'a> SandboxAttempt<'a> { pub fn env_for( &self, spec: CommandSpec, + network: Option<&NetworkProxy>, ) -> Result { self.manager .transform(crate::sandboxing::SandboxTransformRequest { spec, policy: self.policy, sandbox: self.sandbox, + network, sandbox_policy_cwd: self.sandbox_cwd, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap, diff --git a/codex-rs/core/tests/suite/seatbelt.rs b/codex-rs/core/tests/suite/seatbelt.rs index 286bc8791bf..614e87367a5 100644 --- a/codex-rs/core/tests/suite/seatbelt.rs +++ b/codex-rs/core/tests/suite/seatbelt.rs @@ -190,6 +190,7 @@ assert os.read(master, 4) == b"ping""# &policy, sandbox_cwd.as_path(), StdioPolicy::RedirectForShellTool, + None, HashMap::new(), ) .await @@ -242,6 +243,7 @@ async fn java_home_finds_runtime_under_seatbelt() { &policy, sandbox_cwd.as_path(), StdioPolicy::RedirectForShellTool, + None, env, ) .await @@ -298,6 +300,7 @@ async fn touch(path: &Path, policy: &SandboxPolicy) -> bool { policy, sandbox_cwd.as_path(), StdioPolicy::RedirectForShellTool, + None, HashMap::new(), ) .await diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index ab8d3868d97..45de9c22366 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -27,6 +27,7 @@ async fn spawn_command_under_sandbox( sandbox_policy, sandbox_cwd, stdio_policy, + None, env, ) .await @@ -52,6 +53,7 @@ async fn spawn_command_under_sandbox( sandbox_cwd, false, stdio_policy, + None, env, ) .await From 58c573d0648471658fc15d6b3e03d65239f834ff Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 14:54:34 -0800 Subject: [PATCH 12/23] refactor(seatbelt): simplify proxy policy input derivation --- codex-rs/core/src/seatbelt.rs | 48 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 48152049a39..f92e0ce7e82 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -108,36 +108,52 @@ fn local_binding_enabled(env: &HashMap) -> bool { }) } -fn proxy_env_from_network(network: &NetworkProxy) -> HashMap { - let mut env = HashMap::new(); - network.apply_to_env(&mut env); - env +#[derive(Debug, Default)] +struct ProxyPolicyInputs { + ports: Vec, + has_proxy_config: bool, + allow_local_binding: bool, } -fn dynamic_network_policy( - sandbox_policy: &SandboxPolicy, +fn proxy_policy_inputs( network: Option<&NetworkProxy>, env: &HashMap, -) -> String { - let proxy_env; - let policy_env = if let Some(network) = network { - proxy_env = proxy_env_from_network(network); - &proxy_env +) -> ProxyPolicyInputs { + let network_env; + let effective_env = if let Some(network) = network { + network_env = { + let mut env = HashMap::new(); + network.apply_to_env(&mut env); + env + }; + &network_env } else { env }; - let proxy_ports = proxy_loopback_ports_from_env(policy_env); - if !proxy_ports.is_empty() { + ProxyPolicyInputs { + ports: proxy_loopback_ports_from_env(effective_env), + has_proxy_config: has_proxy_url_env_vars(effective_env), + allow_local_binding: local_binding_enabled(effective_env), + } +} + +fn dynamic_network_policy( + sandbox_policy: &SandboxPolicy, + network: Option<&NetworkProxy>, + env: &HashMap, +) -> String { + let proxy = proxy_policy_inputs(network, env); + if !proxy.ports.is_empty() { let mut policy = String::from("; allow outbound access only to configured loopback proxy endpoints\n"); - if local_binding_enabled(policy_env) { + if proxy.allow_local_binding { policy.push_str("; allow localhost-only binding and loopback traffic\n"); policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n"); policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n"); policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n"); } - for port in proxy_ports { + for port in proxy.ports { policy.push_str(&format!( "(allow network-outbound (remote ip \"localhost:{port}\"))\n" )); @@ -145,7 +161,7 @@ fn dynamic_network_policy( return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}"); } - if has_proxy_url_env_vars(policy_env) { + if proxy.has_proxy_config { // Proxy configuration is present but we could not infer any valid loopback endpoints. // Fail closed to avoid silently widening network access in proxy-enforced sessions. return String::new(); From e2259a261f947e06d1942e37923ac92303a9c890 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 15:03:16 -0800 Subject: [PATCH 13/23] refactor(seatbelt): derive proxy policy from network context --- codex-rs/core/src/sandboxing/mod.rs | 1 - codex-rs/core/src/seatbelt.rs | 185 ++++++++-------------------- 2 files changed, 52 insertions(+), 134 deletions(-) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 85e85ab494b..fef75a75996 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -155,7 +155,6 @@ impl SandboxManager { policy, sandbox_policy_cwd, network, - &env, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index f92e0ce7e82..ee7f1572f15 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -37,8 +37,7 @@ pub async fn spawn_command_under_seatbelt( network: Option<&NetworkProxy>, mut env: HashMap, ) -> std::io::Result { - let args = - create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, network, &env); + let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, network); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); spawn_child_async(SpawnChildRequest { @@ -115,35 +114,21 @@ struct ProxyPolicyInputs { allow_local_binding: bool, } -fn proxy_policy_inputs( - network: Option<&NetworkProxy>, - env: &HashMap, -) -> ProxyPolicyInputs { - let network_env; - let effective_env = if let Some(network) = network { - network_env = { - let mut env = HashMap::new(); - network.apply_to_env(&mut env); - env +fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { + if let Some(network) = network { + let mut env = HashMap::new(); + network.apply_to_env(&mut env); + return ProxyPolicyInputs { + ports: proxy_loopback_ports_from_env(&env), + has_proxy_config: has_proxy_url_env_vars(&env), + allow_local_binding: local_binding_enabled(&env), }; - &network_env - } else { - env - }; - - ProxyPolicyInputs { - ports: proxy_loopback_ports_from_env(effective_env), - has_proxy_config: has_proxy_url_env_vars(effective_env), - allow_local_binding: local_binding_enabled(effective_env), } + + ProxyPolicyInputs::default() } -fn dynamic_network_policy( - sandbox_policy: &SandboxPolicy, - network: Option<&NetworkProxy>, - env: &HashMap, -) -> String { - let proxy = proxy_policy_inputs(network, env); +fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, proxy: &ProxyPolicyInputs) -> String { if !proxy.ports.is_empty() { let mut policy = String::from("; allow outbound access only to configured loopback proxy endpoints\n"); @@ -153,7 +138,7 @@ fn dynamic_network_policy( policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n"); policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n"); } - for port in proxy.ports { + for port in &proxy.ports { policy.push_str(&format!( "(allow network-outbound (remote ip \"localhost:{port}\"))\n" )); @@ -182,7 +167,6 @@ pub(crate) fn create_seatbelt_command_args( sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, network: Option<&NetworkProxy>, - env: &HashMap, ) -> Vec { let (file_write_policy, file_write_dir_params) = { if sandbox_policy.has_full_disk_write_access() { @@ -248,7 +232,8 @@ pub(crate) fn create_seatbelt_command_args( }; // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. - let network_policy = dynamic_network_policy(sandbox_policy, network, env); + let proxy = proxy_policy_inputs(network); + let network_policy = dynamic_network_policy(sandbox_policy, &proxy); let full_policy = format!( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" @@ -294,14 +279,14 @@ fn macos_dir_params() -> Vec<(String, PathBuf)> { #[cfg(test)] mod tests { - use super::ALLOW_LOCAL_BINDING_ENV_KEY; use super::MACOS_SEATBELT_BASE_POLICY; + use super::ProxyPolicyInputs; use super::create_seatbelt_command_args; + use super::dynamic_network_policy; use super::macos_dir_params; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use pretty_assertions::assert_eq; - use std::collections::HashMap; use std::fs; use std::path::Path; use std::path::PathBuf; @@ -320,27 +305,15 @@ mod tests { #[test] fn create_seatbelt_args_routes_network_through_proxy_ports() { - let tmp = TempDir::new().expect("tempdir"); - let cwd = tmp.path().join("cwd"); - fs::create_dir_all(&cwd).expect("create cwd"); - let command = vec!["/bin/echo".to_string(), "ok".to_string()]; - - let mut env = HashMap::new(); - env.insert( - "HTTP_PROXY".to_string(), - "http://127.0.0.1:43128".to_string(), - ); - env.insert( - "ALL_PROXY".to_string(), - "socks5h://127.0.0.1:48081".to_string(), + let policy = dynamic_network_policy( + &SandboxPolicy::ReadOnly, + &ProxyPolicyInputs { + ports: vec![43128, 48081], + has_proxy_config: true, + allow_local_binding: false, + }, ); - let args = - create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, None, &env); - let policy = args - .get(1) - .expect("seatbelt args should include policy at index 1"); - assert!( policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"), "expected HTTP proxy port allow rule in policy:\n{policy}" @@ -365,23 +338,14 @@ mod tests { #[test] fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { - let tmp = TempDir::new().expect("tempdir"); - let cwd = tmp.path().join("cwd"); - fs::create_dir_all(&cwd).expect("create cwd"); - let command = vec!["/bin/echo".to_string(), "ok".to_string()]; - - let mut env = HashMap::new(); - env.insert( - "HTTP_PROXY".to_string(), - "http://127.0.0.1:43128".to_string(), + let policy = dynamic_network_policy( + &SandboxPolicy::ReadOnly, + &ProxyPolicyInputs { + ports: vec![43128], + has_proxy_config: true, + allow_local_binding: true, + }, ); - env.insert(ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), "1".to_string()); - - let args = - create_seatbelt_command_args(command, &SandboxPolicy::ReadOnly, &cwd, None, &env); - let policy = args - .get(1) - .expect("seatbelt args should include policy at index 1"); assert!( policy.contains("(allow network-bind (local ip \"localhost:*\"))"), @@ -402,72 +366,46 @@ mod tests { } #[test] - fn create_seatbelt_args_fails_closed_for_invalid_proxy_env() { - let tmp = TempDir::new().expect("tempdir"); - let cwd = tmp.path().join("cwd"); - fs::create_dir_all(&cwd).expect("create cwd"); - let command = vec!["/bin/echo".to_string(), "ok".to_string()]; - - let mut env = HashMap::new(); - env.insert( - "HTTP_PROXY".to_string(), - "not-a-valid-proxy-url".to_string(), - ); - - let args = create_seatbelt_command_args( - command, + fn dynamic_network_policy_fails_closed_when_proxy_config_without_ports() { + let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, - &cwd, - None, - &env, + &ProxyPolicyInputs { + ports: vec![], + has_proxy_config: true, + allow_local_binding: false, + }, ); - let policy = args - .get(1) - .expect("seatbelt args should include policy at index 1"); assert!( !policy.contains("\n(allow network-outbound)\n"), - "policy should not include blanket outbound allowance when proxy env is invalid:\n{policy}" + "policy should not include blanket outbound allowance when proxy config is present without ports:\n{policy}" ); assert!( !policy.contains("(allow network-outbound (remote ip \"localhost:"), - "policy should not include proxy port allowance when proxy env is invalid:\n{policy}" + "policy should not include proxy port allowance when proxy config is present without ports:\n{policy}" ); } #[test] fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { - let tmp = TempDir::new().expect("tempdir"); - let cwd = tmp.path().join("cwd"); - fs::create_dir_all(&cwd).expect("create cwd"); - let command = vec!["/bin/echo".to_string(), "ok".to_string()]; - - let mut env = HashMap::new(); - env.insert( - "HTTP_PROXY".to_string(), - "http://127.0.0.1:43128".to_string(), - ); - - let args = create_seatbelt_command_args( - command, + let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, - &cwd, - None, - &env, + &ProxyPolicyInputs { + ports: vec![43128], + has_proxy_config: true, + allow_local_binding: false, + }, ); - let policy = args - .get(1) - .expect("seatbelt args should include policy at index 1"); assert!( policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"), @@ -526,13 +464,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args( - shell_command.clone(), - &policy, - &cwd, - None, - &HashMap::new(), - ); + let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, None); // Build the expected policy text using a raw string for readability. // Note that the policy includes: @@ -621,7 +553,7 @@ mod tests { .map(std::string::ToString::to_string) .collect(); let write_hooks_file_args = - create_seatbelt_command_args(shell_command_git, &policy, &cwd, None, &HashMap::new()); + create_seatbelt_command_args(shell_command_git, &policy, &cwd, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_hooks_file_args) .current_dir(&cwd) @@ -651,13 +583,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let write_allowed_file_args = create_seatbelt_command_args( - shell_command_allowed, - &policy, - &cwd, - None, - &HashMap::new(), - ); + let write_allowed_file_args = + create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_allowed_file_args) .current_dir(&cwd) @@ -717,8 +644,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = - create_seatbelt_command_args(shell_command, &policy, &cwd, None, &HashMap::new()); + let args = create_seatbelt_command_args(shell_command, &policy, &cwd, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&args) @@ -748,13 +674,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let gitdir_args = create_seatbelt_command_args( - shell_command_gitdir, - &policy, - &cwd, - None, - &HashMap::new(), - ); + let gitdir_args = create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&gitdir_args) .current_dir(&cwd) @@ -815,7 +735,6 @@ mod tests { &policy, vulnerable_root.as_path(), None, - &HashMap::new(), ); let tmpdir_env_var = std::env::var("TMPDIR") From 4d9f3f5b0934f7a188a8413368070045ee372688 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 15:06:43 -0800 Subject: [PATCH 14/23] refactor(core): decouple proxy gate from sandbox network mode --- codex-rs/core/src/landlock.rs | 12 +++--------- codex-rs/core/src/sandboxing/mod.rs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index bea057e4f45..deaf443da4e 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -36,7 +36,7 @@ where sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, - allow_network_for_proxy(sandbox_policy, network), + allow_network_for_proxy(network), ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -52,10 +52,7 @@ where .await } -pub(crate) fn allow_network_for_proxy( - _sandbox_policy: &SandboxPolicy, - _network: Option<&NetworkProxy>, -) -> bool { +pub(crate) fn allow_network_for_proxy(_network: Option<&NetworkProxy>) -> bool { // Proxy environment variables are advisory only. In restricted policies we // keep network sandboxing fail-closed unless we have explicit proxy-only // enforcement in the sandbox runtime. @@ -146,9 +143,6 @@ mod tests { #[test] fn proxy_network_does_not_bypass_restricted_network_policy() { - assert_eq!( - allow_network_for_proxy(&SandboxPolicy::ReadOnly, None), - false - ); + assert_eq!(allow_network_for_proxy(None), false); } } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index fef75a75996..14135e948eb 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -166,7 +166,7 @@ impl SandboxManager { SandboxType::LinuxSeccomp => { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let allow_proxy_network = allow_network_for_proxy(policy, network); + let allow_proxy_network = allow_network_for_proxy(network); let mut args = create_linux_sandbox_command_args( command.clone(), policy, From ec5b165f99698676f1394f9c5f36a07c8dc407a1 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 18:27:17 -0800 Subject: [PATCH 15/23] refactor(core): avoid duplicate shell proxy env wiring --- codex-rs/core/src/sandboxing/mod.rs | 2 ++ codex-rs/core/src/tools/runtimes/shell.rs | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 14135e948eb..067c493ad1f 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -60,6 +60,8 @@ pub(crate) struct SandboxTransformRequest<'a> { pub spec: CommandSpec, pub policy: &'a SandboxPolicy, pub sandbox: SandboxType, + // TODO(viyatb): Evaluate switching this to Option> + // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 7c27e6d7b2d..55552a7a7d4 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -158,10 +158,7 @@ impl ToolRuntime for ShellRuntime { command }; - let mut env = req.env.clone(); - if let Some(network) = req.network.as_ref() { - network.apply_to_env(&mut env); - } + let env = req.env.clone(); let spec = build_command_spec( &command, From fff4b726bdfe868f711ef0f533b30e67dbdae39b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 18:32:33 -0800 Subject: [PATCH 16/23] refactor(core): embed network in exec request structs --- codex-rs/core/src/exec.rs | 4 ++-- codex-rs/core/src/sandboxing/mod.rs | 5 +++-- codex-rs/core/src/tasks/user_shell.rs | 12 ++++-------- codex-rs/core/src/tools/handlers/unified_exec.rs | 1 + codex-rs/core/src/tools/runtimes/apply_patch.rs | 2 +- codex-rs/core/src/tools/runtimes/shell.rs | 11 +++-------- codex-rs/core/src/unified_exec/mod.rs | 3 +++ codex-rs/core/src/unified_exec/process_manager.rs | 2 +- 8 files changed, 18 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 4555b3715f0..329ed55b6ba 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -215,19 +215,19 @@ pub async fn process_exec_tool_call( .map_err(CodexErr::from)?; // Route through the sandboxing module for a single, unified execution path. - crate::sandboxing::execute_env(exec_env, sandbox_policy, network, stdout_stream).await + crate::sandboxing::execute_env(exec_env, sandbox_policy, stdout_stream).await } pub(crate) async fn execute_exec_env( env: ExecEnv, sandbox_policy: &SandboxPolicy, stdout_stream: Option, - network: Option, ) -> Result { let ExecEnv { command, cwd, env, + network, expiration, sandbox, windows_sandbox_level, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 067c493ad1f..bf8f63f2340 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -45,6 +45,7 @@ pub struct ExecEnv { pub command: Vec, pub cwd: PathBuf, pub env: HashMap, + pub network: Option, pub expiration: ExecExpiration, pub sandbox: SandboxType, pub windows_sandbox_level: WindowsSandboxLevel, @@ -201,6 +202,7 @@ impl SandboxManager { command, cwd: spec.cwd, env, + network: network.cloned(), expiration: spec.expiration, sandbox, windows_sandbox_level, @@ -218,8 +220,7 @@ impl SandboxManager { pub async fn execute_env( env: ExecEnv, policy: &SandboxPolicy, - network: Option, stdout_stream: Option, ) -> crate::error::Result { - execute_exec_env(env, policy, stdout_stream, network).await + execute_exec_env(env, policy, stdout_stream).await } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index c034e26f479..19e7a82826b 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -147,6 +147,7 @@ pub(crate) async fn execute_user_shell_command( &turn_context.shell_environment_policy, Some(session.conversation_id), ), + network: turn_context.config.network.clone(), // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), @@ -164,14 +165,9 @@ pub(crate) async fn execute_user_shell_command( }); let sandbox_policy = SandboxPolicy::DangerFullAccess; - let exec_result = execute_exec_env( - exec_env, - &sandbox_policy, - stdout_stream, - turn_context.config.network.clone(), - ) - .or_cancel(&cancellation_token) - .await; + let exec_result = execute_exec_env(exec_env, &sandbox_policy, stdout_stream) + .or_cancel(&cancellation_token) + .await; match exec_result { Err(CancelErr::Cancelled) => { diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 1eb51dd7334..0a88283fe96 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -192,6 +192,7 @@ impl ToolHandler for UnifiedExecHandler { yield_time_ms, max_output_tokens, workdir, + network: context.turn.config.network.clone(), tty, sandbox_permissions, justification, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index f1eb0c6d075..d16f9b836b3 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -153,7 +153,7 @@ impl ToolRuntime for ApplyPatchRuntime { let env = attempt .env_for(spec, None) .map_err(|err| ToolError::Codex(err.into()))?; - let out = execute_env(env, attempt.policy, None, Self::stdout_stream(ctx)) + let out = execute_env(env, attempt.policy, Self::stdout_stream(ctx)) .await .map_err(ToolError::Codex)?; Ok(out) diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 55552a7a7d4..cebaa99a697 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -171,14 +171,9 @@ impl ToolRuntime for ShellRuntime { let env = attempt .env_for(spec, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; - let out = execute_env( - env, - attempt.policy, - req.network.clone(), - Self::stdout_stream(ctx), - ) - .await - .map_err(ToolError::Codex)?; + let out = execute_env(env, attempt.policy, Self::stdout_stream(ctx)) + .await + .map_err(ToolError::Codex)?; Ok(out) } } diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 4c45f1cf421..e06f6d48a79 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -27,6 +27,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use codex_network_proxy::NetworkProxy; use rand::Rng; use rand::rng; use tokio::sync::Mutex; @@ -79,6 +80,7 @@ pub(crate) struct ExecCommandRequest { pub yield_time_ms: u64, pub max_output_tokens: Option, pub workdir: Option, + pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub justification: Option, @@ -203,6 +205,7 @@ mod tests { yield_time_ms, max_output_tokens: None, workdir: None, + network: None, tty: true, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index cee73fae256..dd39c82c16b 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -520,7 +520,7 @@ impl UnifiedExecProcessManager { command: request.command.clone(), cwd, env, - network: context.turn.config.network.clone(), + network: request.network.clone(), tty: request.tty, sandbox_permissions: request.sandbox_permissions, justification: request.justification.clone(), From df10db12eb13139745cbc1ca5edff768c17267dc Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 18:42:48 -0800 Subject: [PATCH 17/23] docs(linux-sandbox): clarify proxy compatibility flag behavior --- codex-rs/linux-sandbox/src/linux_run_main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 4ae7d91b778..846473ebc3d 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -42,7 +42,9 @@ pub struct LandlockCommand { /// Internal compatibility flag. /// - /// Proxy environment variables do not disable restricted-network sandboxing. + /// By default, proxy environment variables do not disable + /// restricted-network sandboxing. + /// If set, proxy configuration can disable restricted-network sandboxing. #[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)] pub allow_network_for_proxy: bool, From 62f0ac2d60377bec615c50a21ea78bb8f2b4b81a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 19:00:13 -0800 Subject: [PATCH 18/23] refactor(linux-sandbox): model bwrap network mode as tri-state --- codex-rs/linux-sandbox/src/bwrap.rs | 32 +++++++++++--- codex-rs/linux-sandbox/src/linux_run_main.rs | 45 ++++++++++++++++---- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index a834f41f17a..493ab975de2 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -26,22 +26,40 @@ pub(crate) struct BwrapOptions { /// This is the secure default, but some restrictive container environments /// deny `--proc /proc` even when PID namespaces are available. pub mount_proc: bool, - /// Whether to isolate networking with a dedicated network namespace. - /// - /// When true, bubblewrap adds `--unshare-net`, which removes access to the - /// host network stack and leaves only loopback inside the sandbox. - pub isolate_network: bool, + /// How networking should be configured inside the bubblewrap sandbox. + pub network_mode: BwrapNetworkMode, } impl Default for BwrapOptions { fn default() -> Self { Self { mount_proc: true, - isolate_network: false, + network_mode: BwrapNetworkMode::FullAccess, } } } +/// Network policy modes for bubblewrap. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum BwrapNetworkMode { + /// Keep access to the host network namespace. + #[default] + FullAccess, + /// Remove access to the host network namespace. + Isolated, + /// Intended proxy-only mode. + /// + /// Bubblewrap does not currently enforce proxy-only egress, so this is + /// treated as isolated for fail-closed behavior. + ProxyOnly, +} + +impl BwrapNetworkMode { + fn should_unshare_network(self) -> bool { + !matches!(self, Self::FullAccess) + } +} + /// Wrap a command with bubblewrap so the filesystem is read-only by default, /// with explicit writable roots and read-only subpaths layered afterward. /// @@ -73,7 +91,7 @@ fn create_bwrap_flags( args.extend(create_filesystem_args(sandbox_policy, cwd)?); // Isolate the PID namespace. args.push("--unshare-pid".to_string()); - if options.isolate_network { + if options.network_mode.should_unshare_network() { args.push("--unshare-net".to_string()); } // Mount a fresh /proc unless the caller explicitly disables it. diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 846473ebc3d..871d67710ec 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -6,6 +6,7 @@ use std::os::fd::FromRawFd; use std::path::Path; use std::path::PathBuf; +use crate::bwrap::BwrapNetworkMode; use crate::bwrap::BwrapOptions; use crate::bwrap::create_bwrap_command_args; use crate::landlock::apply_sandbox_policy_to_current_thread; @@ -42,9 +43,9 @@ pub struct LandlockCommand { /// Internal compatibility flag. /// - /// By default, proxy environment variables do not disable - /// restricted-network sandboxing. - /// If set, proxy configuration can disable restricted-network sandboxing. + /// By default, restricted-network sandboxing uses isolated networking. + /// If set, sandbox setup switches to proxy-only network mode + /// (currently enforced the same as isolated networking). #[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)] pub allow_network_for_proxy: bool, @@ -118,7 +119,13 @@ pub fn run_main() -> ! { allow_network_for_proxy, command, ); - run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc); + run_bwrap_with_proc_fallback( + &sandbox_policy_cwd, + &sandbox_policy, + inner, + !no_proc, + allow_network_for_proxy, + ); } // Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled. @@ -138,6 +145,7 @@ fn run_bwrap_with_proc_fallback( sandbox_policy: &codex_core::protocol::SandboxPolicy, inner: Vec, mount_proc: bool, + allow_network_for_proxy: bool, ) -> ! { let mut mount_proc = mount_proc; @@ -146,9 +154,16 @@ fn run_bwrap_with_proc_fallback( mount_proc = false; } + let network_mode = if sandbox_policy.has_full_network_access() { + BwrapNetworkMode::FullAccess + } else if allow_network_for_proxy { + BwrapNetworkMode::ProxyOnly + } else { + BwrapNetworkMode::Isolated + }; let options = BwrapOptions { mount_proc, - isolate_network: !sandbox_policy.has_full_network_access(), + network_mode, }; let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options); exec_vendored_bwrap(argv); @@ -188,7 +203,7 @@ fn preflight_proc_mount_support( sandbox_policy_cwd, BwrapOptions { mount_proc: true, - isolate_network: false, + network_mode: BwrapNetworkMode::FullAccess, }, ); let stderr = run_bwrap_in_child_capture_stderr(preflight_argv); @@ -373,7 +388,7 @@ mod tests { Path::new("/"), BwrapOptions { mount_proc: true, - isolate_network: false, + network_mode: BwrapNetworkMode::FullAccess, }, ); assert_eq!( @@ -407,7 +422,21 @@ mod tests { Path::new("/"), BwrapOptions { mount_proc: true, - isolate_network: true, + network_mode: BwrapNetworkMode::Isolated, + }, + ); + assert_eq!(argv.contains(&"--unshare-net".to_string()), true); + } + + #[test] + fn inserts_unshare_net_when_proxy_only_network_mode_requested() { + let argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &SandboxPolicy::ReadOnly, + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::ProxyOnly, }, ); assert_eq!(argv.contains(&"--unshare-net".to_string()), true); From a90f9223bf09179b9fb108c47eac5c2ac90d81fb Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 19:37:54 -0800 Subject: [PATCH 19/23] refactor(core): rename ExecEnv to ExecRequest --- codex-rs/core/src/exec.rs | 6 +++--- codex-rs/core/src/sandboxing/mod.rs | 10 +++++----- codex-rs/core/src/tasks/user_shell.rs | 4 ++-- codex-rs/core/src/tools/runtimes/unified_exec.rs | 2 +- codex-rs/core/src/tools/sandboxing.rs | 2 +- codex-rs/core/src/unified_exec/mod.rs | 4 ++-- codex-rs/core/src/unified_exec/process_manager.rs | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 329ed55b6ba..11ca86d3fda 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -26,7 +26,7 @@ use crate::protocol::ExecCommandOutputDeltaEvent; use crate::protocol::ExecOutputStream; use crate::protocol::SandboxPolicy; use crate::sandboxing::CommandSpec; -use crate::sandboxing::ExecEnv; +use crate::sandboxing::ExecRequest; use crate::sandboxing::SandboxManager; use crate::sandboxing::SandboxPermissions; use crate::spawn::SpawnChildRequest; @@ -219,11 +219,11 @@ pub async fn process_exec_tool_call( } pub(crate) async fn execute_exec_env( - env: ExecEnv, + env: ExecRequest, sandbox_policy: &SandboxPolicy, stdout_stream: Option, ) -> Result { - let ExecEnv { + let ExecRequest { command, cwd, env, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index bf8f63f2340..b6fe331ddb4 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -1,7 +1,7 @@ /* Module: sandboxing -Build platform wrappers and produce ExecEnv for execution. Owns low‑level +Build platform wrappers and produce ExecRequest for execution. Owns low-level sandbox placement and transformation of portable CommandSpec into a ready‑to‑spawn environment. */ @@ -41,7 +41,7 @@ pub struct CommandSpec { } #[derive(Debug)] -pub struct ExecEnv { +pub struct ExecRequest { pub command: Vec, pub cwd: PathBuf, pub env: HashMap, @@ -124,7 +124,7 @@ impl SandboxManager { pub(crate) fn transform( &self, request: SandboxTransformRequest<'_>, - ) -> Result { + ) -> Result { let SandboxTransformRequest { mut spec, policy, @@ -198,7 +198,7 @@ impl SandboxManager { env.extend(sandbox_env); - Ok(ExecEnv { + Ok(ExecRequest { command, cwd: spec.cwd, env, @@ -218,7 +218,7 @@ impl SandboxManager { } pub async fn execute_env( - env: ExecEnv, + env: ExecRequest, policy: &SandboxPolicy, stdout_stream: Option, ) -> crate::error::Result { diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 19e7a82826b..02dd5fdf245 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -23,7 +23,7 @@ use crate::protocol::ExecCommandEndEvent; use crate::protocol::ExecCommandSource; use crate::protocol::SandboxPolicy; use crate::protocol::TurnStartedEvent; -use crate::sandboxing::ExecEnv; +use crate::sandboxing::ExecRequest; use crate::sandboxing::SandboxPermissions; use crate::state::TaskKind; use crate::tools::format_exec_output_str; @@ -140,7 +140,7 @@ pub(crate) async fn execute_user_shell_command( ) .await; - let exec_env = ExecEnv { + let exec_env = ExecRequest { command: exec_command.clone(), cwd: cwd.clone(), env: create_env( diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index d286adb2178..f75c8f75728 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -2,7 +2,7 @@ Runtime: unified exec Handles approval + sandbox orchestration for unified exec requests, delegating to -the process manager to spawn PTYs once an ExecEnv is prepared. +the process manager to spawn PTYs once an ExecRequest is prepared. */ use crate::error::CodexErr; use crate::error::SandboxErr; diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 06af8a5ce0c..6060e931dde 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -284,7 +284,7 @@ impl<'a> SandboxAttempt<'a> { &self, spec: CommandSpec, network: Option<&NetworkProxy>, - ) -> Result { + ) -> Result { self.manager .transform(crate::sandboxing::SandboxTransformRequest { spec, diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index e06f6d48a79..c1c7b87078f 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -4,7 +4,7 @@ //! - Manages interactive processes (create, reuse, buffer output with caps). //! - Uses the shared ToolOrchestrator to handle approval, sandbox selection, and //! retry semantics in a single, descriptive flow. -//! - Spawns the PTY from a sandbox‑transformed `ExecEnv`; on sandbox denial, +//! - Spawns the PTY from a sandbox-transformed `ExecRequest`; on sandbox denial, //! retries without sandbox when policy allows (no re‑prompt thanks to caching). //! - Uses the shared `is_likely_sandbox_denied` heuristic to keep denial messages //! consistent with other exec paths. @@ -12,7 +12,7 @@ //! Flow at a glance (open process) //! 1) Build a small request `{ command, cwd }`. //! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run. -//! 3) Runtime: transform `CommandSpec` → `ExecEnv` → spawn PTY. +//! 3) Runtime: transform `CommandSpec` -> `ExecRequest` -> spawn PTY. //! 4) If denial, orchestrator retries with `SandboxType::None`. //! 5) Process handle is returned with streaming output + metadata. //! diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index dd39c82c16b..8576bf77c08 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -14,7 +14,7 @@ use tokio_util::sync::CancellationToken; use crate::exec_env::create_env; use crate::exec_policy::ExecApprovalRequest; use crate::protocol::ExecCommandSource; -use crate::sandboxing::ExecEnv; +use crate::sandboxing::ExecRequest; use crate::tools::events::ToolEmitter; use crate::tools::events::ToolEventCtx; use crate::tools::events::ToolEventStage; @@ -460,7 +460,7 @@ impl UnifiedExecProcessManager { pub(crate) async fn open_session_with_exec_env( &self, - env: &ExecEnv, + env: &ExecRequest, tty: bool, ) -> Result { let (program, args) = env From 7c22f7755f66d29c32eba0044c2405abd92405e1 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 22:14:13 -0800 Subject: [PATCH 20/23] fix(sandbox): enforce managed network in yolo paths --- codex-rs/core/src/exec.rs | 12 ++- codex-rs/core/src/landlock.rs | 17 ++-- codex-rs/core/src/sandboxing/mod.rs | 50 ++++++++++- codex-rs/core/src/seatbelt.rs | 54 ++++++++++-- codex-rs/core/src/tools/orchestrator.rs | 21 ++++- codex-rs/core/src/tools/sandboxing.rs | 2 + codex-rs/linux-sandbox/src/bwrap.rs | 89 +++++++++++++++++++- codex-rs/linux-sandbox/src/landlock.rs | 49 ++++++++++- codex-rs/linux-sandbox/src/linux_run_main.rs | 29 +++++-- 9 files changed, 288 insertions(+), 35 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 11ca86d3fda..5cd58274197 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -158,9 +158,18 @@ pub async fn process_exec_tool_call( stdout_stream: Option, ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; + let enforce_managed_network = params.network.is_some(); let sandbox_type = match &sandbox_policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - SandboxType::None + if enforce_managed_network { + get_platform_sandbox( + windows_sandbox_level + != codex_protocol::config_types::WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None) + } else { + SandboxType::None + } } _ => get_platform_sandbox( windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled, @@ -206,6 +215,7 @@ pub async fn process_exec_tool_call( spec, policy: sandbox_policy, sandbox: sandbox_type, + enforce_managed_network, network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index deaf443da4e..51a46619389 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -36,7 +36,7 @@ where sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, - allow_network_for_proxy(network), + allow_network_for_proxy(false), ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -52,11 +52,11 @@ where .await } -pub(crate) fn allow_network_for_proxy(_network: Option<&NetworkProxy>) -> bool { - // Proxy environment variables are advisory only. In restricted policies we - // keep network sandboxing fail-closed unless we have explicit proxy-only - // enforcement in the sandbox runtime. - false +pub(crate) 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, + // preserve existing behavior. + enforce_managed_network } /// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. @@ -142,7 +142,8 @@ mod tests { } #[test] - fn proxy_network_does_not_bypass_restricted_network_policy() { - assert_eq!(allow_network_for_proxy(None), false); + fn proxy_network_requires_managed_requirements() { + assert_eq!(allow_network_for_proxy(false), false); + assert_eq!(allow_network_for_proxy(true), true); } } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index b6fe331ddb4..9a72b6e2492 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -61,6 +61,7 @@ pub(crate) struct SandboxTransformRequest<'a> { pub spec: CommandSpec, pub policy: &'a SandboxPolicy, pub sandbox: SandboxType, + pub enforce_managed_network: bool, // TODO(viyatb): Evaluate switching this to Option> // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, @@ -98,6 +99,7 @@ impl SandboxManager { policy: &SandboxPolicy, pref: SandboxablePreference, windows_sandbox_level: WindowsSandboxLevel, + has_managed_network_requirements: bool, ) -> SandboxType { match pref { SandboxablePreference::Forbid => SandboxType::None, @@ -111,7 +113,14 @@ impl SandboxManager { } SandboxablePreference::Auto => match policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - SandboxType::None + if has_managed_network_requirements { + crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None) + } else { + SandboxType::None + } } _ => crate::safety::get_platform_sandbox( windows_sandbox_level != WindowsSandboxLevel::Disabled, @@ -129,6 +138,7 @@ impl SandboxManager { mut spec, policy, sandbox, + enforce_managed_network, network, sandbox_policy_cwd, codex_linux_sandbox_exe, @@ -157,6 +167,7 @@ impl SandboxManager { command.clone(), policy, sandbox_policy_cwd, + enforce_managed_network, network, ); let mut full_command = Vec::with_capacity(1 + args.len()); @@ -169,7 +180,7 @@ impl SandboxManager { SandboxType::LinuxSeccomp => { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; - let allow_proxy_network = allow_network_for_proxy(network); + let allow_proxy_network = allow_network_for_proxy(enforce_managed_network); let mut args = create_linux_sandbox_command_args( command.clone(), policy, @@ -224,3 +235,38 @@ pub async fn execute_env( ) -> crate::error::Result { execute_exec_env(env, policy, stdout_stream).await } + +#[cfg(test)] +mod tests { + use super::SandboxManager; + use crate::exec::SandboxType; + use crate::protocol::SandboxPolicy; + use crate::tools::sandboxing::SandboxablePreference; + use codex_protocol::config_types::WindowsSandboxLevel; + use pretty_assertions::assert_eq; + + #[test] + fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() { + let manager = SandboxManager::new(); + let sandbox = manager.select_initial( + &SandboxPolicy::DangerFullAccess, + SandboxablePreference::Auto, + WindowsSandboxLevel::Disabled, + false, + ); + assert_eq!(sandbox, SandboxType::None); + } + + #[test] + fn danger_full_access_uses_platform_sandbox_with_network_requirements() { + let manager = SandboxManager::new(); + let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None); + let sandbox = manager.select_initial( + &SandboxPolicy::DangerFullAccess, + SandboxablePreference::Auto, + WindowsSandboxLevel::Disabled, + true, + ); + assert_eq!(sandbox, expected); + } +} diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index ee7f1572f15..c08f70b1e59 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -37,7 +37,8 @@ pub async fn spawn_command_under_seatbelt( network: Option<&NetworkProxy>, mut env: HashMap, ) -> std::io::Result { - let args = create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, network); + let args = + create_seatbelt_command_args(command, sandbox_policy, sandbox_policy_cwd, false, network); let arg0 = None; env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); spawn_child_async(SpawnChildRequest { @@ -128,7 +129,11 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { ProxyPolicyInputs::default() } -fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, proxy: &ProxyPolicyInputs) -> String { +fn dynamic_network_policy( + sandbox_policy: &SandboxPolicy, + enforce_managed_network: bool, + proxy: &ProxyPolicyInputs, +) -> String { if !proxy.ports.is_empty() { let mut policy = String::from("; allow outbound access only to configured loopback proxy endpoints\n"); @@ -152,6 +157,12 @@ fn dynamic_network_policy(sandbox_policy: &SandboxPolicy, proxy: &ProxyPolicyInp return String::new(); } + if enforce_managed_network { + // Managed network requirements are active but no usable proxy endpoints + // are available. Fail closed for network access. + return String::new(); + } + if sandbox_policy.has_full_network_access() { // No proxy env is configured: retain the existing full-network behavior. format!( @@ -166,6 +177,7 @@ pub(crate) fn create_seatbelt_command_args( command: Vec, sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, + enforce_managed_network: bool, network: Option<&NetworkProxy>, ) -> Vec { let (file_write_policy, file_write_dir_params) = { @@ -233,7 +245,7 @@ pub(crate) fn create_seatbelt_command_args( // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. let proxy = proxy_policy_inputs(network); - let network_policy = dynamic_network_policy(sandbox_policy, &proxy); + let network_policy = dynamic_network_policy(sandbox_policy, enforce_managed_network, &proxy); let full_policy = format!( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" @@ -307,6 +319,7 @@ mod tests { fn create_seatbelt_args_routes_network_through_proxy_ports() { let policy = dynamic_network_policy( &SandboxPolicy::ReadOnly, + false, &ProxyPolicyInputs { ports: vec![43128, 48081], has_proxy_config: true, @@ -340,6 +353,7 @@ mod tests { fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { let policy = dynamic_network_policy( &SandboxPolicy::ReadOnly, + false, &ProxyPolicyInputs { ports: vec![43128], has_proxy_config: true, @@ -374,6 +388,7 @@ mod tests { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + false, &ProxyPolicyInputs { ports: vec![], has_proxy_config: true, @@ -391,6 +406,26 @@ mod tests { ); } + #[test] + fn dynamic_network_policy_fails_closed_for_managed_network_without_proxy_config() { + let policy = dynamic_network_policy( + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + true, + &ProxyPolicyInputs { + ports: vec![], + has_proxy_config: false, + allow_local_binding: false, + }, + ); + + assert_eq!(policy, ""); + } + #[test] fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { let policy = dynamic_network_policy( @@ -400,6 +435,7 @@ mod tests { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }, + false, &ProxyPolicyInputs { ports: vec![43128], has_proxy_config: true, @@ -464,7 +500,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, None); + let args = create_seatbelt_command_args(shell_command.clone(), &policy, &cwd, false, None); // Build the expected policy text using a raw string for readability. // Note that the policy includes: @@ -553,7 +589,7 @@ mod tests { .map(std::string::ToString::to_string) .collect(); let write_hooks_file_args = - create_seatbelt_command_args(shell_command_git, &policy, &cwd, None); + create_seatbelt_command_args(shell_command_git, &policy, &cwd, false, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_hooks_file_args) .current_dir(&cwd) @@ -584,7 +620,7 @@ mod tests { .map(std::string::ToString::to_string) .collect(); let write_allowed_file_args = - create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, None); + create_seatbelt_command_args(shell_command_allowed, &policy, &cwd, false, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&write_allowed_file_args) .current_dir(&cwd) @@ -644,7 +680,7 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let args = create_seatbelt_command_args(shell_command, &policy, &cwd, None); + let args = create_seatbelt_command_args(shell_command, &policy, &cwd, false, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&args) @@ -674,7 +710,8 @@ mod tests { .iter() .map(std::string::ToString::to_string) .collect(); - let gitdir_args = create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, None); + let gitdir_args = + create_seatbelt_command_args(shell_command_gitdir, &policy, &cwd, false, None); let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE) .args(&gitdir_args) .current_dir(&cwd) @@ -734,6 +771,7 @@ mod tests { shell_command.clone(), &policy, vulnerable_root.as_path(), + false, None, ); diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 381f8ce1364..ef8fbf742d0 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -3,7 +3,8 @@ Module: orchestrator Central place for approvals + sandbox selection + retry semantics. Drives a simple sequence for any ToolRuntime: approval → select sandbox → attempt → -retry without sandbox on denial (no re‑approval thanks to caching). +retry with an escalated sandbox strategy on denial (no re‑approval thanks to +caching). */ use crate::error::CodexErr; use crate::error::SandboxErr; @@ -87,12 +88,19 @@ impl ToolOrchestrator { } // 2) First attempt under the selected sandbox. + let has_managed_network_requirements = turn_ctx + .config + .config_layer_stack + .requirements_toml() + .network + .is_some(); let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( &turn_ctx.sandbox_policy, tool.sandbox_preference(), turn_ctx.windows_sandbox_level, + has_managed_network_requirements, ), }; @@ -102,6 +110,7 @@ impl ToolOrchestrator { let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, policy: &turn_ctx.sandbox_policy, + enforce_managed_network: has_managed_network_requirements, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), @@ -128,7 +137,7 @@ impl ToolOrchestrator { }))); } - // Ask for approval before retrying without sandbox. + // Ask for approval before retrying with the escalated sandbox. if !tool.should_bypass_approval(approval_policy, already_approved) { let reason_msg = build_denial_reason_from_output(output.as_ref()); let approval_ctx = ApprovalCtx { @@ -151,9 +160,15 @@ impl ToolOrchestrator { } } + let escalated_sandbox = if has_managed_network_requirements { + initial_sandbox + } else { + crate::exec::SandboxType::None + }; let escalated_attempt = SandboxAttempt { - sandbox: crate::exec::SandboxType::None, + sandbox: escalated_sandbox, policy: &turn_ctx.sandbox_policy, + enforce_managed_network: has_managed_network_requirements, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: None, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 6060e931dde..56e54e62f1f 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -272,6 +272,7 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { pub(crate) struct SandboxAttempt<'a> { pub sandbox: crate::exec::SandboxType, pub policy: &'a crate::protocol::SandboxPolicy, + pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, @@ -290,6 +291,7 @@ impl<'a> SandboxAttempt<'a> { spec, policy: self.policy, sandbox: self.sandbox, + enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 493ab975de2..c74b799568a 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -63,8 +63,10 @@ impl BwrapNetworkMode { /// Wrap a command with bubblewrap so the filesystem is read-only by default, /// with explicit writable roots and read-only subpaths layered afterward. /// -/// When the policy grants full disk write access, this returns `command` -/// unchanged so we avoid unnecessary sandboxing overhead. +/// When the policy grants full disk write access and full network access, this +/// returns `command` unchanged so we avoid unnecessary sandboxing overhead. +/// If network isolation is requested, we still wrap with bubblewrap so network +/// namespace restrictions apply while preserving full filesystem access. pub(crate) fn create_bwrap_command_args( command: Vec, sandbox_policy: &SandboxPolicy, @@ -72,12 +74,37 @@ pub(crate) fn create_bwrap_command_args( options: BwrapOptions, ) -> Result> { if sandbox_policy.has_full_disk_write_access() { - return Ok(command); + if options.network_mode == BwrapNetworkMode::FullAccess { + return Ok(command); + } + + return Ok(create_bwrap_flags_full_filesystem(command, options)); } create_bwrap_flags(command, sandbox_policy, cwd, options) } +fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOptions) -> Vec { + let mut args = vec![ + "--new-session".to_string(), + "--die-with-parent".to_string(), + "--bind".to_string(), + "/".to_string(), + "/".to_string(), + "--unshare-pid".to_string(), + ]; + if options.network_mode.should_unshare_network() { + args.push("--unshare-net".to_string()); + } + if options.mount_proc { + args.push("--proc".to_string()); + args.push("/proc".to_string()); + } + args.push("--".to_string()); + args.extend(command); + args +} + /// Build the bubblewrap flags (everything after `argv[0]`). fn create_bwrap_flags( command: Vec, @@ -279,3 +306,59 @@ fn find_first_non_existent_component(target_path: &Path) -> Option { None } + +#[cfg(test)] +mod tests { + use super::*; + use codex_core::protocol::SandboxPolicy; + use pretty_assertions::assert_eq; + + #[test] + fn full_disk_write_full_network_returns_unwrapped_command() { + let command = vec!["/bin/true".to_string()]; + let args = create_bwrap_command_args( + command.clone(), + &SandboxPolicy::DangerFullAccess, + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::FullAccess, + }, + ) + .expect("create bwrap args"); + + assert_eq!(args, command); + } + + #[test] + fn full_disk_write_proxy_only_keeps_full_filesystem_but_unshares_network() { + let command = vec!["/bin/true".to_string()]; + let args = create_bwrap_command_args( + command.clone(), + &SandboxPolicy::DangerFullAccess, + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::ProxyOnly, + }, + ) + .expect("create bwrap args"); + + assert_eq!( + args, + vec![ + "--new-session".to_string(), + "--die-with-parent".to_string(), + "--bind".to_string(), + "/".to_string(), + "/".to_string(), + "--unshare-pid".to_string(), + "--unshare-net".to_string(), + "--proc".to_string(), + "/proc".to_string(), + "--".to_string(), + "/bin/true".to_string(), + ] + ); + } +} diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index a4ac62426a8..fce28c4b46a 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -42,9 +42,10 @@ pub(crate) fn apply_sandbox_policy_to_current_thread( sandbox_policy: &SandboxPolicy, cwd: &Path, apply_landlock_fs: bool, - _allow_network_for_proxy: bool, + allow_network_for_proxy: bool, ) -> Result<()> { - let install_network_seccomp = !sandbox_policy.has_full_network_access(); + let install_network_seccomp = + should_install_network_seccomp(sandbox_policy, allow_network_for_proxy); // `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents // setuid privilege elevation. Many `bwrap` deployments rely on setuid, so @@ -75,6 +76,15 @@ pub(crate) fn apply_sandbox_policy_to_current_thread( Ok(()) } +fn should_install_network_seccomp( + sandbox_policy: &SandboxPolicy, + allow_network_for_proxy: bool, +) -> bool { + // Managed-network sessions should remain fail-closed even for policies that + // would normally grant full network access (for example, DangerFullAccess). + !sandbox_policy.has_full_network_access() || allow_network_for_proxy +} + /// Enable `PR_SET_NO_NEW_PRIVS` so seccomp can be applied safely. fn set_no_new_privs() -> Result<()> { let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; @@ -186,3 +196,38 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), Ok(()) } + +#[cfg(test)] +mod tests { + use super::should_install_network_seccomp; + use codex_core::protocol::SandboxPolicy; + use pretty_assertions::assert_eq; + + #[test] + fn managed_network_enforces_seccomp_even_for_full_network_policy() { + assert_eq!( + should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, true), + true + ); + } + + #[test] + fn full_network_policy_without_managed_network_skips_seccomp() { + assert_eq!( + should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, false), + false + ); + } + + #[test] + fn restricted_network_policy_always_installs_seccomp() { + assert_eq!( + should_install_network_seccomp(&SandboxPolicy::ReadOnly, false), + true + ); + assert_eq!( + should_install_network_seccomp(&SandboxPolicy::ReadOnly, true), + true + ); + } +} diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 871d67710ec..2978efae2a2 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -96,7 +96,7 @@ pub fn run_main() -> ! { exec_or_panic(command); } - if sandbox_policy.has_full_disk_write_access() { + if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy { if let Err(e) = apply_sandbox_policy_to_current_thread( &sandbox_policy, &sandbox_policy_cwd, @@ -154,13 +154,7 @@ fn run_bwrap_with_proc_fallback( mount_proc = false; } - let network_mode = if sandbox_policy.has_full_network_access() { - BwrapNetworkMode::FullAccess - } else if allow_network_for_proxy { - BwrapNetworkMode::ProxyOnly - } else { - BwrapNetworkMode::Isolated - }; + let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy); let options = BwrapOptions { mount_proc, network_mode, @@ -169,6 +163,19 @@ fn run_bwrap_with_proc_fallback( exec_vendored_bwrap(argv); } +fn bwrap_network_mode( + sandbox_policy: &codex_core::protocol::SandboxPolicy, + allow_network_for_proxy: bool, +) -> BwrapNetworkMode { + if allow_network_for_proxy { + BwrapNetworkMode::ProxyOnly + } else if sandbox_policy.has_full_network_access() { + BwrapNetworkMode::FullAccess + } else { + BwrapNetworkMode::Isolated + } +} + fn build_bwrap_argv( inner: Vec, sandbox_policy: &codex_core::protocol::SandboxPolicy, @@ -441,4 +448,10 @@ mod tests { ); assert_eq!(argv.contains(&"--unshare-net".to_string()), true); } + + #[test] + fn proxy_only_mode_takes_precedence_over_full_network_policy() { + let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true); + assert_eq!(mode, BwrapNetworkMode::ProxyOnly); + } } From 66b486e107cdb80ece9c2c30093e7ebf48e6bc10 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 22:36:46 -0800 Subject: [PATCH 21/23] fix(sandbox): remove redundant clone in bwrap proxy-only test --- codex-rs/linux-sandbox/src/bwrap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index c74b799568a..7fff9702992 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -334,7 +334,7 @@ mod tests { fn full_disk_write_proxy_only_keeps_full_filesystem_but_unshares_network() { let command = vec!["/bin/true".to_string()]; let args = create_bwrap_command_args( - command.clone(), + command, &SandboxPolicy::DangerFullAccess, Path::new("/"), BwrapOptions { From 9d3a6f4ad510866a8f9592d46af6db35204a17a8 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 23:12:43 -0800 Subject: [PATCH 22/23] fix(core): tighten managed-network gating and cleanup exec wiring --- codex-rs/core/src/exec.rs | 4 +-- codex-rs/core/src/tools/orchestrator.rs | 38 ++++++++++++++++------- codex-rs/core/src/tools/runtimes/shell.rs | 4 +-- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 5cd58274197..e627ebec065 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -210,7 +210,7 @@ pub async fn process_exec_tool_call( }; let manager = SandboxManager::new(); - let exec_env = manager + let exec_req = manager .transform(crate::sandboxing::SandboxTransformRequest { spec, policy: sandbox_policy, @@ -225,7 +225,7 @@ pub async fn process_exec_tool_call( .map_err(CodexErr::from)?; // Route through the sandboxing module for a single, unified execution path. - crate::sandboxing::execute_env(exec_env, sandbox_policy, stdout_stream).await + crate::sandboxing::execute_env(exec_req, sandbox_policy, stdout_stream).await } pub(crate) async fn execute_exec_env( diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index ef8fbf742d0..3114f87331e 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -88,12 +88,15 @@ impl ToolOrchestrator { } // 2) First attempt under the selected sandbox. - let has_managed_network_requirements = turn_ctx - .config - .config_layer_stack - .requirements_toml() - .network - .is_some(); + let has_managed_network_requirements = is_managed_network_enabled( + turn_ctx + .config + .config_layer_stack + .requirements_toml() + .network + .as_ref() + .and_then(|network| network.enabled), + ); let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( @@ -160,13 +163,8 @@ impl ToolOrchestrator { } } - let escalated_sandbox = if has_managed_network_requirements { - initial_sandbox - } else { - crate::exec::SandboxType::None - }; let escalated_attempt = SandboxAttempt { - sandbox: escalated_sandbox, + sandbox: crate::exec::SandboxType::None, policy: &turn_ctx.sandbox_policy, enforce_managed_network: has_managed_network_requirements, manager: &self.sandbox, @@ -189,3 +187,19 @@ fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String { // output so we can evolve heuristics later without touching call sites. "command failed; retry without sandbox?".to_string() } + +fn is_managed_network_enabled(enabled: Option) -> bool { + matches!(enabled, Some(true)) +} + +#[cfg(test)] +mod tests { + use super::is_managed_network_enabled; + + #[test] + fn managed_network_requires_explicit_true() { + assert!(is_managed_network_enabled(Some(true))); + assert!(!is_managed_network_enabled(Some(false))); + assert!(!is_managed_network_enabled(None)); + } +} diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index cebaa99a697..f5e1ea17e4b 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -158,12 +158,10 @@ impl ToolRuntime for ShellRuntime { command }; - let env = req.env.clone(); - let spec = build_command_spec( &command, &req.cwd, - &env, + &req.env, req.timeout_ms.into(), req.sandbox_permissions, req.justification.clone(), From d1fa4153345243912ea41f26fdcb8fd26cc1beae Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 9 Feb 2026 23:32:06 -0800 Subject: [PATCH 23/23] fix(sandbox): align managed-network gate with requirements presence --- codex-rs/core/src/tools/orchestrator.rs | 31 +++++-------------------- codex-rs/linux-sandbox/src/bwrap.rs | 10 ++++---- codex-rs/linux-sandbox/src/landlock.rs | 14 +++++------ 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 3114f87331e..3f410e1b264 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -88,15 +88,12 @@ impl ToolOrchestrator { } // 2) First attempt under the selected sandbox. - let has_managed_network_requirements = is_managed_network_enabled( - turn_ctx - .config - .config_layer_stack - .requirements_toml() - .network - .as_ref() - .and_then(|network| network.enabled), - ); + let has_managed_network_requirements = turn_ctx + .config + .config_layer_stack + .requirements_toml() + .network + .is_some(); let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( @@ -187,19 +184,3 @@ fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String { // output so we can evolve heuristics later without touching call sites. "command failed; retry without sandbox?".to_string() } - -fn is_managed_network_enabled(enabled: Option) -> bool { - matches!(enabled, Some(true)) -} - -#[cfg(test)] -mod tests { - use super::is_managed_network_enabled; - - #[test] - fn managed_network_requires_explicit_true() { - assert!(is_managed_network_enabled(Some(true))); - assert!(!is_managed_network_enabled(Some(false))); - assert!(!is_managed_network_enabled(None)); - } -} diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 7fff9702992..1a835fd5e03 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -74,11 +74,11 @@ pub(crate) fn create_bwrap_command_args( options: BwrapOptions, ) -> Result> { if sandbox_policy.has_full_disk_write_access() { - if options.network_mode == BwrapNetworkMode::FullAccess { - return Ok(command); - } - - return Ok(create_bwrap_flags_full_filesystem(command, options)); + return if options.network_mode == BwrapNetworkMode::FullAccess { + Ok(command) + } else { + Ok(create_bwrap_flags_full_filesystem(command, options)) + }; } create_bwrap_flags(command, sandbox_policy, cwd, options) diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index fce28c4b46a..d86fafd297d 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -221,13 +221,13 @@ mod tests { #[test] fn restricted_network_policy_always_installs_seccomp() { - assert_eq!( - should_install_network_seccomp(&SandboxPolicy::ReadOnly, false), - true - ); - assert_eq!( - should_install_network_seccomp(&SandboxPolicy::ReadOnly, true), + assert!(should_install_network_seccomp( + &SandboxPolicy::ReadOnly, + false + )); + assert!(should_install_network_seccomp( + &SandboxPolicy::ReadOnly, true - ); + )); } }