From 869f37eaddb5c38da09043ae8a875c2edbfaba29 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 22 Jun 2026 17:39:48 +0200 Subject: [PATCH 1/3] Prepare managed network sandbox context --- codex-rs/Cargo.lock | 1 + codex-rs/core/src/exec.rs | 2 + codex-rs/core/src/sandboxing/mod.rs | 4 + codex-rs/core/src/tasks/user_shell.rs | 1 + codex-rs/core/src/tools/runtimes/mod.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 3 + .../core/src/tools/runtimes/unified_exec.rs | 37 ++++-- codex-rs/core/src/tools/sandboxing.rs | 2 + codex-rs/core/src/tools/sandboxing_tests.rs | 8 +- .../core/src/unified_exec/process_manager.rs | 23 +++- .../src/unified_exec/process_manager_tests.rs | 25 +++- codex-rs/exec-server/Cargo.toml | 1 + codex-rs/exec-server/src/environment.rs | 2 + codex-rs/exec-server/src/fs_sandbox.rs | 1 + codex-rs/exec-server/src/local_process.rs | 1 + codex-rs/exec-server/src/process_sandbox.rs | 1 + .../exec-server/src/process_sandbox_tests.rs | 47 +++++++ codex-rs/exec-server/src/protocol.rs | 54 ++++++++ .../exec-server/src/server/handler/tests.rs | 1 + codex-rs/exec-server/src/server/processor.rs | 1 + codex-rs/exec-server/tests/exec_process.rs | 12 ++ codex-rs/exec-server/tests/relay.rs | 1 + codex-rs/network-proxy/src/lib.rs | 2 + codex-rs/network-proxy/src/proxy.rs | 123 ++++++++++++++++-- .../rmcp-client/src/stdio_server_launcher.rs | 1 + codex-rs/sandboxing/src/manager.rs | 10 +- codex-rs/sandboxing/src/manager_tests.rs | 13 +- codex-rs/sandboxing/src/seatbelt.rs | 72 ++++++---- codex-rs/sandboxing/src/seatbelt_tests.rs | 37 ++++++ 29 files changed, 428 insertions(+), 59 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f35402efd08e..b5f5a1d58579 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2882,6 +2882,7 @@ dependencies = [ "codex-app-server-protocol", "codex-client", "codex-file-system", + "codex-network-proxy", "codex-protocol", "codex-sandboxing", "codex-shell-command", diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 99c87cdd99ea..59024f1570c2 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -376,6 +376,7 @@ pub fn build_exec_request( args: args.to_vec(), cwd, env, + managed_network: None, additional_permissions: None, }; let options = ExecOptions { @@ -458,6 +459,7 @@ pub(crate) async fn execute_exec_request( arg0, exec_server_sandbox: _, exec_server_enforce_managed_network: _, + exec_server_managed_network: _, } = exec_request; // TODO(anp): Keep PathUri through the local process launch boundary. diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index d0006f8657ed..1c570e1bbb1a 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -15,6 +15,7 @@ use crate::exec::execute_exec_request; use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_file_system::FileSystemSandboxContext; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::exec_output::ExecToolCallOutput; @@ -63,6 +64,7 @@ pub struct ExecRequest { pub arg0: Option, pub(crate) exec_server_sandbox: Option, pub(crate) exec_server_enforce_managed_network: bool, + pub(crate) exec_server_managed_network: Option, } impl ExecRequest { @@ -107,6 +109,7 @@ impl ExecRequest { arg0, exec_server_sandbox: None, exec_server_enforce_managed_network: false, + exec_server_managed_network: None, } } @@ -165,6 +168,7 @@ impl ExecRequest { arg0, exec_server_sandbox: None, exec_server_enforce_managed_network: false, + exec_server_managed_network: None, } } } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 844e327bdf90..d588e5a35611 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -226,6 +226,7 @@ pub(crate) async fn execute_user_shell_command( arg0: None, exec_server_sandbox: None, exec_server_enforce_managed_network: false, + exec_server_managed_network: None, }; let stdout_stream = Some(StdoutStream { diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 27f4ee594659..205476f41f5e 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -50,6 +50,7 @@ pub(crate) fn build_sandbox_command( args: args.to_vec(), cwd, env: env.clone(), + managed_network: None, additional_permissions, }) } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index a77645f941fe..10cb4602056c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -168,6 +168,7 @@ pub(super) async fn try_run_zsh_fork( arg0, exec_server_sandbox: _, exec_server_enforce_managed_network: _, + exec_server_managed_network: _, } = sandbox_exec_request; let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?; let effective_timeout = Duration::from_millis( @@ -902,6 +903,7 @@ impl CoreShellCommandExecutor { arg0: self.arg0.clone(), exec_server_sandbox: None, exec_server_enforce_managed_network: false, + exec_server_managed_network: None, }, /*stdout_stream*/ None, after_spawn, @@ -1010,6 +1012,7 @@ impl CoreShellCommandExecutor { args: args.to_vec(), cwd, env, + managed_network: None, additional_permissions, }; let options = ExecOptions { diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index c1d519120471..d9d4d02fcf9a 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -41,6 +41,7 @@ use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; @@ -117,6 +118,7 @@ fn build_unified_exec_sandbox_command( command: &[String], cwd: &PathUri, env: &HashMap, + managed_network: Option, additional_permissions: Option, ) -> Result { let (program, args) = command @@ -127,6 +129,7 @@ fn build_unified_exec_sandbox_command( args: args.to_vec(), cwd: cwd.clone(), env: env.clone(), + managed_network, additional_permissions, }) } @@ -321,20 +324,24 @@ impl<'a> ToolRuntime for UnifiedExecRunt req.network.as_ref(), launch_sandbox_permissions, ); - let mut env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); - if let Some(network) = managed_network { - network - .apply_to_env_for_optional_environment( - &mut env, - Some(&req.turn_environment.environment_id), - ) - .map_err(|err| { - ToolError::Codex(CodexErr::Io(io::Error::other(format!( - "failed to prepare network proxy for environment `{}`: {err}", - req.turn_environment.environment_id - )))) - })?; - } + let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); + let (mut env, managed_network_context) = match managed_network { + Some(network) => { + let prepared = network + .prepare_for_optional_environment( + env, + Some(&req.turn_environment.environment_id), + ) + .map_err(|err| { + ToolError::Codex(CodexErr::Io(io::Error::other(format!( + "failed to prepare network proxy for environment `{}`: {err}", + req.turn_environment.environment_id + )))) + })?; + (prepared.env, Some(prepared.sandbox_context)) + } + None => (env, None), + }; let explicit_env_overrides = req.explicit_env_overrides.clone(); #[cfg(unix)] let runtime_path_prepends = { @@ -385,6 +392,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &command, &req.cwd, &env, + managed_network_context.clone(), req.additional_permissions.clone(), ) .map_err(|error| match error { @@ -450,6 +458,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &command, &req.cwd, &env, + managed_network_context, req.additional_permissions.clone(), ) .map_err(|error| match error { diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 77f68d96b511..4eae3bd0db6e 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -466,6 +466,7 @@ impl<'a> SandboxAttempt<'a> { network: Option<&NetworkProxy>, environment_id: Option<&str>, ) -> Result { + let managed_network = command.managed_network.clone(); let exec_server_permissions = effective_permission_profile( self.exec_server_permissions, command.additional_permissions.as_ref(), @@ -502,6 +503,7 @@ impl<'a> SandboxAttempt<'a> { use_legacy_landlock: self.use_legacy_landlock, }); exec_request.exec_server_enforce_managed_network = self.enforce_managed_network; + exec_request.exec_server_managed_network = managed_network; } Ok(exec_request) } diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index c647e1c40113..abf2f8c36872 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::sandboxing::SandboxPermissions; use crate::tools::hook_names::HookToolName; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -227,18 +228,22 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { windows_sandbox_private_desktop: false, network_denial_cancellation_token: None, }; + let managed_network = ManagedNetworkSandboxContext { + loopback_ports: vec![43123], + allow_local_binding: false, + }; let command = SandboxCommand { program: "/bin/bash".into(), args: vec!["-lc".to_string(), "pwd".to_string()], cwd: cwd_uri.clone(), env: HashMap::new(), + managed_network: Some(managed_network.clone()), additional_permissions: None, }; let options = crate::sandboxing::ExecOptions { expiration: crate::exec::ExecExpiration::DefaultTimeout, capture_policy: crate::exec::ExecCapturePolicy::ShellTool, }; - let request = attempt .env_for_exec_server(command, options, /*network*/ None, Some("remote")) .expect("prepare remote exec request"); @@ -265,4 +270,5 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { }) ); assert!(request.exec_server_enforce_managed_network); + assert_eq!(request.exec_server_managed_network, Some(managed_network)); } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index a3b4bcf9c76e..df924b6674f9 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -52,7 +52,11 @@ use crate::unified_exec::process::OutputBuffer; use crate::unified_exec::process::OutputHandles; use crate::unified_exec::process::SpawnLifecycleHandle; use crate::unified_exec::process::UnifiedExecProcess; +use codex_network_proxy::CUSTOM_CA_ENV_KEYS; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::PROXY_ENV_KEYS; +#[cfg(target_os = "macos")] +use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; use codex_protocol::config_types::ShellEnvironmentPolicy; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; @@ -142,10 +146,20 @@ fn exec_server_env_for_request( HashMap, ) { if let Some(exec_server_env_config) = &request.exec_server_env_config { - ( - Some(exec_server_env_config.policy.clone()), - env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env), - ) + let mut env = + env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env); + if request.exec_server_managed_network.is_some() { + for key in PROXY_ENV_KEYS.iter().chain(CUSTOM_CA_ENV_KEYS.iter()) { + if let Some(value) = request.env.get(*key) { + env.insert((*key).to_string(), value.clone()); + } + } + #[cfg(target_os = "macos")] + if let Some(value) = request.env.get(PROXY_GIT_SSH_COMMAND_ENV_KEY) { + env.insert(PROXY_GIT_SSH_COMMAND_ENV_KEY.to_string(), value.clone()); + } + } + (Some(exec_server_env_config.policy.clone()), env) } else { (None, request.env.clone()) } @@ -168,6 +182,7 @@ fn exec_server_params_for_request( arg0: request.arg0.clone(), sandbox: request.exec_server_sandbox.clone(), enforce_managed_network: request.exec_server_enforce_managed_network, + managed_network: request.exec_server_managed_network.clone(), } } diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index b6afdc85ba1a..2e61c3e948a1 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::unified_exec::clamp_yield_time; +use codex_network_proxy::ManagedNetworkSandboxContext; use pretty_assertions::assert_eq; use tokio::time::Duration; use tokio::time::Instant; @@ -76,6 +77,10 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { codex_protocol::permissions::FileSystemSandboxPolicy::unrestricted(); let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::Restricted; let permission_profile = codex_protocol::models::PermissionProfile::Disabled; + let managed_network = ManagedNetworkSandboxContext { + loopback_ports: vec![43123], + allow_local_binding: false, + }; let request = ExecRequest { command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()], cwd: cwd.clone().into(), @@ -83,6 +88,11 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { ("HOME".to_string(), "/client-home".to_string()), ("PATH".to_string(), "/sandbox-path".to_string()), ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()), + ( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43123".to_string(), + ), + ("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()), ]), exec_server_env_config: Some(ExecServerEnvConfig { policy: codex_exec_server::ExecEnvPolicy { @@ -95,6 +105,11 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { local_policy_env: HashMap::from([ ("HOME".to_string(), "/client-home".to_string()), ("PATH".to_string(), "/client-path".to_string()), + ( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43123".to_string(), + ), + ("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()), ]), }), network: None, @@ -112,7 +127,8 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { windows_sandbox_filesystem_overrides: None, arg0: None, exec_server_sandbox: None, - exec_server_enforce_managed_network: false, + exec_server_enforce_managed_network: true, + exec_server_managed_network: Some(managed_network.clone()), }; let params = @@ -120,12 +136,19 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { assert_eq!(params.process_id.as_str(), "123"); assert_eq!(params.cwd, request.cwd); + assert!(params.enforce_managed_network); + assert_eq!(params.managed_network, Some(managed_network)); assert!(params.env_policy.is_some()); assert_eq!( params.env, HashMap::from([ ("PATH".to_string(), "/sandbox-path".to_string()), ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()), + ( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:43123".to_string(), + ), + ("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string(),), ]) ); } diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 31fc4ea0aa9e..02f23dfc49e4 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -20,6 +20,7 @@ codex-app-server-protocol = { workspace = true } codex-api = { workspace = true } codex-client = { workspace = true } codex-file-system = { workspace = true } +codex-network-proxy = { workspace = true } codex-protocol = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-command = { workspace = true } diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 5990d8811dff..5e2156b3c2ec 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1160,6 +1160,7 @@ mod tests { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await .expect("start process"); @@ -1197,6 +1198,7 @@ mod tests { arg0: None, sandbox: Some(sandbox), enforce_managed_network: false, + managed_network: None, }) .await; let Err(err) = result else { diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 3dbcf73c1461..6db3804aef8a 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -115,6 +115,7 @@ impl FileSystemSandboxRunner { args: vec![CODEX_FS_HELPER_ARG1.to_string()], cwd: cwd.uri.clone(), env: self.helper_env.clone(), + managed_network: None, additional_permissions: None, }; let native_workspace_roots = sandbox_context diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index 802c8e69cff2..6a3a2019f7bd 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -921,6 +921,7 @@ mod tests { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, } } diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index be95b92d34fa..1118f93fef3f 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -96,6 +96,7 @@ pub(crate) fn prepare_exec_request( args: args.to_vec(), cwd: params.cwd.clone(), env, + managed_network: params.managed_network.clone(), additional_permissions: None, }, permissions: &permissions, diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index 1b0408afd85d..bc71b573201b 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +#[cfg(target_os = "macos")] +use codex_network_proxy::ManagedNetworkSandboxContext; #[cfg(unix)] use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; @@ -44,6 +46,7 @@ fn sandbox_request_wraps_native_argv_on_executor() { arg0: None, sandbox: Some(sandbox), enforce_managed_network: false, + managed_network: None, }; let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) @@ -78,6 +81,49 @@ fn sandbox_request_wraps_native_argv_on_executor() { ); } +#[cfg(target_os = "macos")] +#[test] +fn sandbox_request_allows_prepared_managed_proxy_port() { + let cwd: AbsolutePathBuf = std::env::current_dir() + .expect("current directory") + .try_into() + .expect("absolute cwd"); + let cwd_uri = PathUri::from_abs_path(&cwd); + let self_exe = std::env::current_exe().expect("current executable"); + let runtime_paths = + ExecServerRuntimePaths::new(self_exe.clone(), Some(self_exe)).expect("runtime paths"); + let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::workspace_write(), + cwd_uri.clone(), + ); + let params = ExecParams { + process_id: ProcessId::from("process-managed-network"), + argv: vec!["/usr/bin/true".to_string()], + cwd: cwd_uri, + env_policy: None, + env: HashMap::new(), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: Some(sandbox), + enforce_managed_network: true, + managed_network: Some(ManagedNetworkSandboxContext { + loopback_ports: vec![43123], + allow_local_binding: false, + }), + }; + + let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) + .expect("prepare managed-network sandbox request"); + let policy = prepared + .command + .windows(2) + .find_map(|args| (args[0] == "-p").then_some(args[1].as_str())) + .expect("Seatbelt policy argument"); + + assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))")); +} + #[test] fn native_request_preserves_native_launch_fields() { let cwd: AbsolutePathBuf = std::env::current_dir() @@ -97,6 +143,7 @@ fn native_request_preserves_native_launch_fields() { arg0: Some("custom-arg0".to_string()), sandbox: None, enforce_managed_network: false, + managed_network: None, }; let prepared = prepare_exec_request(¶ms, env.clone(), /*runtime_paths*/ None) diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index e05595f273f6..1b2dc84f2965 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_file_system::FileSystemSandboxContext; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_protocol::config_types::ShellEnvironmentPolicyInherit; use codex_utils_path_uri::PathUri; use serde::Deserialize; @@ -109,6 +110,12 @@ pub struct ExecParams { /// Whether the eventual executor-side sandbox must enforce managed networking. #[serde(default)] pub enforce_managed_network: bool, + /// Optional details for enforcing managed networking without a live proxy object. + /// + /// When `enforce_managed_network` is true and these details are absent, the executor must + /// continue to fail closed. This preserves compatibility with older clients. + #[serde(default)] + pub managed_network: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -501,12 +508,59 @@ mod base64_bytes { #[cfg(test)] mod tests { + use super::ExecParams; use super::FsReadFileParams; use super::HttpRequestParams; + use super::ProcessId; use codex_file_system::FileSystemSandboxContext; + use codex_network_proxy::ManagedNetworkSandboxContext; use codex_protocol::models::PermissionProfile; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; + use std::collections::HashMap; + + #[test] + fn exec_params_managed_network_context_round_trips_and_defaults_for_legacy_peers() { + let cwd = PathUri::from_path(std::env::current_dir().expect("current directory")) + .expect("cwd URI"); + let params = ExecParams { + process_id: ProcessId::from("managed-network"), + argv: vec!["true".to_string()], + cwd, + env_policy: None, + env: HashMap::new(), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: None, + enforce_managed_network: true, + managed_network: Some(ManagedNetworkSandboxContext { + loopback_ports: vec![43123, 48081], + allow_local_binding: false, + }), + }; + + let mut serialized = serde_json::to_value(¶ms).expect("serialize exec params"); + assert_eq!( + serialized["managedNetwork"], + serde_json::json!({ + "loopbackPorts": [43123, 48081], + "allowLocalBinding": false, + }) + ); + let round_trip: ExecParams = + serde_json::from_value(serialized.clone()).expect("deserialize exec params"); + assert_eq!(round_trip, params); + + serialized + .as_object_mut() + .expect("exec params object") + .remove("managedNetwork"); + let legacy: ExecParams = + serde_json::from_value(serialized).expect("deserialize legacy exec params"); + assert!(legacy.enforce_managed_network); + assert_eq!(legacy.managed_network, None); + } #[test] fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris() { diff --git a/codex-rs/exec-server/src/server/handler/tests.rs b/codex-rs/exec-server/src/server/handler/tests.rs index 9cbbf78d0055..1a74e3b3d416 100644 --- a/codex-rs/exec-server/src/server/handler/tests.rs +++ b/codex-rs/exec-server/src/server/handler/tests.rs @@ -35,6 +35,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec) -> ExecParams { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, } } diff --git a/codex-rs/exec-server/src/server/processor.rs b/codex-rs/exec-server/src/server/processor.rs index f5c2ba760d5c..4309b4d8e8bb 100644 --- a/codex-rs/exec-server/src/server/processor.rs +++ b/codex-rs/exec-server/src/server/processor.rs @@ -440,6 +440,7 @@ mod tests { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, } } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index ac38f50eafe7..3e62b83ca3de 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -83,6 +83,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), "proc-1"); @@ -226,6 +227,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -259,6 +261,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -308,6 +311,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -358,6 +362,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close( arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -433,6 +438,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -472,6 +478,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -507,6 +514,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool) arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -543,6 +551,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -598,6 +607,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; @@ -640,6 +650,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe( arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; @@ -700,6 +711,7 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()> arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; diff --git a/codex-rs/exec-server/tests/relay.rs b/codex-rs/exec-server/tests/relay.rs index 5f49655e3939..666afc212b47 100644 --- a/codex-rs/exec-server/tests/relay.rs +++ b/codex-rs/exec-server/tests/relay.rs @@ -152,6 +152,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await?; assert_eq!( diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index e80b154a5eae..88577f07031e 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -46,6 +46,7 @@ pub use proxy::Args; #[cfg(target_os = "macos")] pub use proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; pub use proxy::DEFAULT_NO_PROXY_VALUE; +pub use proxy::ManagedNetworkSandboxContext; pub use proxy::NO_PROXY_ENV_KEYS; pub use proxy::NetworkProxy; pub use proxy::NetworkProxyBuilder; @@ -55,6 +56,7 @@ pub use proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] pub use proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; pub use proxy::PROXY_URL_ENV_KEYS; +pub use proxy::PreparedManagedNetwork; pub use proxy::has_proxy_url_env_vars; pub use proxy::proxy_url_env_value; pub use runtime::BlockedRequest; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index ef56846599ae..1b4adbab1e20 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -10,6 +10,8 @@ use anyhow::Context; use anyhow::Result; use clap::Parser; use codex_utils_absolute_path::AbsolutePathBuf; +use serde::Deserialize; +use serde::Serialize; use std::collections::HashMap; use std::net::SocketAddr; use std::net::TcpListener as StdTcpListener; @@ -331,6 +333,27 @@ struct EnvironmentProxyAddrs { socks_addr: SocketAddr, } +/// Portable managed-network facts needed by an operating-system sandbox. +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedNetworkSandboxContext { + /// Loopback proxy ports that sandboxed commands may connect to. + #[serde(default)] + pub loopback_ports: Vec, + /// Whether the command may bind local sockets and exchange loopback traffic. + #[serde(default)] + pub allow_local_binding: bool, +} + +/// Environment-specific managed-network settings prepared for one command launch. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PreparedManagedNetwork { + /// Complete command environment with managed proxy variables applied. + pub env: HashMap, + /// Matching portable sandbox inputs for the command environment. + pub sandbox_context: ManagedNetworkSandboxContext, +} + struct EnvironmentProxy { addrs: EnvironmentProxyAddrs, http_task: JoinHandle>, @@ -656,22 +679,49 @@ impl NetworkProxy { }) } - fn apply_to_env_for_addrs( + fn prepare_for_addrs( &self, - env: &mut HashMap, + mut env: HashMap, addrs: EnvironmentProxyAddrs, - ) { + ) -> PreparedManagedNetwork { let runtime_settings = self.runtime_settings(); // Enforce proxying for child processes. Proxy endpoint values are always rewritten; // managed MITM CA vars preserve child-scoped overrides after proxy startup. apply_proxy_env_overrides( - env, + &mut env, addrs.http_addr, addrs.socks_addr, self.socks_enabled, runtime_settings.allow_local_binding, runtime_settings.mitm_ca_trust_bundle.as_ref(), ); + let mut loopback_ports = [ + Some(addrs.http_addr), + self.socks_enabled.then_some(addrs.socks_addr), + ] + .into_iter() + .flatten() + .filter(|addr| addr.ip().is_loopback()) + .map(|addr| addr.port()) + .collect::>(); + loopback_ports.sort_unstable(); + loopback_ports.dedup(); + PreparedManagedNetwork { + env, + sandbox_context: ManagedNetworkSandboxContext { + loopback_ports, + allow_local_binding: runtime_settings.allow_local_binding, + }, + } + } + + fn apply_to_env_for_addrs( + &self, + env: &mut HashMap, + addrs: EnvironmentProxyAddrs, + ) { + let prepared = self.prepare_for_addrs(std::mem::take(env), addrs); + *env = prepared.env; } pub fn apply_to_env(&self, env: &mut HashMap) { @@ -708,6 +758,23 @@ impl NetworkProxy { } } + /// Applies the environment-specific proxy settings and returns the matching portable sandbox + /// projection from the same runtime configuration snapshot. + pub fn prepare_for_optional_environment( + &self, + env: HashMap, + environment_id: Option<&str>, + ) -> Result { + let addrs = match environment_id { + Some(environment_id) => self.environment_proxy_addrs(environment_id)?, + None => EnvironmentProxyAddrs { + http_addr: self.http_addr, + socks_addr: self.socks_addr, + }, + }; + Ok(self.prepare_for_addrs(env, addrs)) + } + fn environment_proxy_addrs(&self, environment_id: &str) -> Result { let mut proxies = self .environment_proxies @@ -1074,27 +1141,59 @@ mod tests { } #[tokio::test] - async fn apply_to_env_for_environment_uses_distinct_proxy_ports() -> Result<()> { + async fn prepare_for_environment_keeps_env_and_sandbox_ports_in_sync() -> Result<()> { let state = Arc::new(network_proxy_state_for_policy( NetworkProxySettings::default(), )); let proxy = NetworkProxy::builder().state(state).build().await?; let handle = proxy.run().await?; - let mut local_env = HashMap::new(); - proxy.apply_to_env_for_environment(&mut local_env, "local")?; - let mut remote_env = HashMap::new(); - proxy.apply_to_env_for_environment(&mut remote_env, "remote")?; + let base_env = HashMap::from([("PRESERVED".to_string(), "value".to_string())]); + let local = proxy.prepare_for_optional_environment(base_env.clone(), Some("local"))?; + let remote = proxy.prepare_for_optional_environment(HashMap::new(), Some("remote"))?; - assert_ne!(local_env.get("HTTP_PROXY"), remote_env.get("HTTP_PROXY")); + assert_eq!( + local.env.get("PRESERVED").map(String::as_str), + Some("value") + ); + assert_ne!(local.env.get("HTTP_PROXY"), remote.env.get("HTTP_PROXY")); assert_ne!( - local_env.get("HTTP_PROXY"), + local.env.get("HTTP_PROXY"), Some(&format!("http://{}", proxy.http_addr())) ); assert_ne!( - remote_env.get("HTTP_PROXY"), + remote.env.get("HTTP_PROXY"), Some(&format!("http://{}", proxy.http_addr())) ); + for prepared in [&local, &remote] { + let http_port = prepared + .env + .get("HTTP_PROXY") + .and_then(|value| value.strip_prefix("http://")) + .and_then(|value| value.parse::().ok()) + .map(|addr| addr.port()) + .expect("managed HTTP proxy address"); + let socks_port = prepared + .env + .get("ALL_PROXY") + .and_then(|value| value.strip_prefix("socks5h://")) + .and_then(|value| value.parse::().ok()) + .map(|addr| addr.port()) + .expect("managed SOCKS proxy address"); + let mut expected_ports = vec![http_port, socks_port]; + expected_ports.sort_unstable(); + expected_ports.dedup(); + assert_eq!( + prepared.sandbox_context, + ManagedNetworkSandboxContext { + loopback_ports: expected_ports, + allow_local_binding: false, + } + ); + } + let mut legacy_env = base_env; + proxy.apply_to_env_for_environment(&mut legacy_env, "local")?; + assert_eq!(legacy_env, local.env); handle.shutdown().await?; Ok(()) diff --git a/codex-rs/rmcp-client/src/stdio_server_launcher.rs b/codex-rs/rmcp-client/src/stdio_server_launcher.rs index 94aa45c1e7dd..caa85d34e973 100644 --- a/codex-rs/rmcp-client/src/stdio_server_launcher.rs +++ b/codex-rs/rmcp-client/src/stdio_server_launcher.rs @@ -505,6 +505,7 @@ impl ExecutorStdioServerLauncher { arg0: None, sandbox: None, enforce_managed_network: false, + managed_network: None, }) .await .map_err(io::Error::other)?; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 29b1177b11de..46fa6e7bda77 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -13,6 +13,7 @@ use crate::resolve_windows_elevated_filesystem_overrides; use crate::resolve_windows_restricted_token_filesystem_overrides; #[cfg(target_os = "windows")] use crate::windows_sandbox_uses_elevated_backend; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::AdditionalPermissionProfile; @@ -99,6 +100,7 @@ pub struct SandboxCommand { pub args: Vec, pub cwd: PathUri, pub env: HashMap, + pub managed_network: Option, pub additional_permissions: Option, } @@ -332,6 +334,8 @@ impl SandboxManager { windows_sandbox_level, windows_sandbox_private_desktop, } = request; + #[cfg(target_os = "macos")] + let managed_network = command.managed_network.as_ref(); let additional_permissions = command.additional_permissions.take(); let managed_mitm_ca_trust_bundle_path = network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path); @@ -364,6 +368,7 @@ impl SandboxManager { network_sandbox_policy: pending.effective_network_policy, sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(), enforce_managed_network, + managed_network, environment_id, network, extra_allow_unix_sockets: &[], @@ -479,12 +484,14 @@ impl SandboxManager { codex_home: &Path, ) -> Result { let workspace_roots = request.workspace_roots; + let enforce_managed_network = request.transform.enforce_managed_network; let mut request = self.transform(request.transform)?; if request.sandbox == SandboxType::WindowsRestrictedToken { wrap_windows_sandbox_exec_request_for_direct_spawn( &mut request, workspace_roots, codex_home, + enforce_managed_network, )?; } Ok(request) @@ -496,6 +503,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn( request: &mut SandboxExecRequest, workspace_roots: &[AbsolutePathBuf], codex_home: &Path, + enforce_managed_network: bool, ) -> Result<(), SandboxTransformError> { // TODO(anp): Keep PathUri through the Windows sandbox wrapper boundary. let native_cwd = @@ -522,7 +530,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn( *program = helper.to_string_lossy().into_owned(); let inner_command = std::mem::take(&mut request.command); - let proxy_enforced = request.network.is_some(); + let proxy_enforced = enforce_managed_network || request.network.is_some(); let use_elevated = windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced); let overrides = if use_elevated { diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 64fc87b6e4b9..81aa1fbea62c 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -92,6 +92,7 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli args: Vec::new(), cwd: cwd_uri.clone(), env: HashMap::new(), + managed_network: None, additional_permissions: None, }, permissions: &permissions, @@ -139,6 +140,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { args: Vec::new(), cwd: cwd_uri.clone(), env: HashMap::new(), + managed_network: None, additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), @@ -211,6 +213,7 @@ fn transform_additional_permissions_preserves_denied_entries() { args: Vec::new(), cwd: cwd_uri.clone(), env: HashMap::new(), + managed_network: None, additional_permissions: Some(AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, @@ -314,6 +317,7 @@ fn transform_linux_seccomp_request( args: Vec::new(), cwd: cwd_uri.clone(), env: HashMap::new(), + managed_network: None, additional_permissions: None, }, permissions: &permissions, @@ -504,11 +508,12 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { "Path".to_string(), r"C:\Windows\System32".to_string(), )]), + managed_network: None, additional_permissions: None, }, permissions: &permissions, sandbox: SandboxType::WindowsRestrictedToken, - enforce_managed_network: false, + enforce_managed_network: true, environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, @@ -539,6 +544,12 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { .iter() .any(|arg| arg == "--run-as-windows-sandbox") ); + assert!( + exec_request + .command + .iter() + .any(|arg| arg == "--proxy-enforced") + ); assert!( exec_request .command diff --git a/codex-rs/sandboxing/src/seatbelt.rs b/codex-rs/sandboxing/src/seatbelt.rs index d3233e705b3e..f53569de071d 100644 --- a/codex-rs/sandboxing/src/seatbelt.rs +++ b/codex-rs/sandboxing/src/seatbelt.rs @@ -1,3 +1,4 @@ +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_network_proxy::has_proxy_url_env_vars; @@ -103,6 +104,7 @@ struct UnixSocketPathParam { } fn proxy_policy_inputs( + managed_network: Option<&ManagedNetworkSandboxContext>, network: Option<&NetworkProxy>, environment_id: Option<&str>, extra_allow_unix_sockets: &[AbsolutePathBuf], @@ -112,33 +114,47 @@ fn proxy_policy_inputs( .filter_map(|socket_path| normalize_path_for_sandbox(socket_path.as_path())) .collect::>(); + let unix_domain_socket_policy = match network { + Some(network) if network.dangerously_allow_all_unix_sockets() => { + UnixDomainSocketPolicy::AllowAll + } + Some(network) => { + let mut allowed = network + .allow_unix_sockets() + .iter() + .filter_map(|socket_path| { + match normalize_path_for_sandbox(Path::new(socket_path)) { + Some(path) => Some(path), + None => { + warn!( + "ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}" + ); + None + } + } + }) + .collect::>(); + allowed.extend(extra_allowed); + UnixDomainSocketPolicy::Restricted { allowed } + } + None => UnixDomainSocketPolicy::Restricted { + allowed: extra_allowed, + }, + }; + if let Some(managed_network) = managed_network { + return Ok(ProxyPolicyInputs { + ports: managed_network.loopback_ports.clone(), + has_proxy_config: true, + allow_local_binding: managed_network.allow_local_binding, + unix_domain_socket_policy, + }); + } match network { Some(network) => { let mut env = HashMap::new(); network .apply_to_env_for_optional_environment(&mut env, environment_id) .map_err(|err| err.to_string())?; - let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() { - UnixDomainSocketPolicy::AllowAll - } else { - let mut allowed = network - .allow_unix_sockets() - .iter() - .filter_map(|socket_path| { - match normalize_path_for_sandbox(Path::new(socket_path)) { - Some(path) => Some(path), - None => { - warn!( - "ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}" - ); - None - } - } - }) - .collect::>(); - allowed.extend(extra_allowed); - UnixDomainSocketPolicy::Restricted { allowed } - }; Ok(ProxyPolicyInputs { ports: proxy_loopback_ports_from_env(&env), has_proxy_config: has_proxy_url_env_vars(&env), @@ -147,9 +163,7 @@ fn proxy_policy_inputs( }) } None => Ok(ProxyPolicyInputs { - unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted { - allowed: extra_allowed, - }, + unix_domain_socket_policy, ..Default::default() }), } @@ -586,6 +600,7 @@ fn create_seatbelt_command_args_for_legacy_policy( network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), sandbox_policy_cwd, enforce_managed_network, + managed_network: None, environment_id: None, network, extra_allow_unix_sockets: &[], @@ -599,6 +614,7 @@ pub struct CreateSeatbeltCommandArgsParams<'a> { pub network_sandbox_policy: NetworkSandboxPolicy, pub sandbox_policy_cwd: &'a Path, pub enforce_managed_network: bool, + pub managed_network: Option<&'a ManagedNetworkSandboxContext>, pub environment_id: Option<&'a str>, pub network: Option<&'a NetworkProxy>, pub extra_allow_unix_sockets: &'a [AbsolutePathBuf], @@ -613,6 +629,7 @@ pub fn create_seatbelt_command_args( network_sandbox_policy, sandbox_policy_cwd, enforce_managed_network, + managed_network, environment_id, network, extra_allow_unix_sockets, @@ -709,7 +726,12 @@ pub fn create_seatbelt_command_args( } }; - let proxy = proxy_policy_inputs(network, environment_id, extra_allow_unix_sockets)?; + let proxy = proxy_policy_inputs( + managed_network, + network, + environment_id, + extra_allow_unix_sockets, + )?; let network_policy = dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy); diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index e0ff0e66523b..b15019a8cb78 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -14,6 +14,7 @@ use super::unix_socket_policy; use codex_network_proxy::ConfigReloader; use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; +use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkMode; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyConfig; @@ -205,6 +206,7 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() network_sandbox_policy: NetworkSandboxPolicy::Restricted, sandbox_policy_cwd: Path::new("/"), enforce_managed_network: false, + managed_network: None, environment_id: None, network: None, extra_allow_unix_sockets: &[], @@ -258,6 +260,37 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() ); } +#[test] +fn prepared_managed_network_context_allows_only_its_proxy_ports() { + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &SandboxPolicy::new_read_only_policy(), + Path::new("/"), + ); + let managed_network = ManagedNetworkSandboxContext { + loopback_ports: vec![43123, 48081], + allow_local_binding: false, + }; + let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { + command: vec!["/bin/true".to_string()], + file_system_sandbox_policy: &file_system_policy, + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + sandbox_policy_cwd: Path::new("/"), + enforce_managed_network: true, + managed_network: Some(&managed_network), + environment_id: None, + network: None, + extra_allow_unix_sockets: &[], + }) + .unwrap(); + + let policy = seatbelt_policy_arg(&args); + assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))")); + assert!(policy.contains("(allow network-outbound (remote ip \"localhost:48081\"))")); + assert!(!policy.contains("(allow network-outbound (remote ip \"localhost:9999\"))")); + assert!(!policy.contains("(allow network-bind (local ip \"*:*\"))")); + assert!(!policy.contains("(allow network-outbound)\n")); +} + #[test] fn explicit_unreadable_paths_are_excluded_from_readable_roots() { let root = absolute_path("/tmp/codex-readable"); @@ -279,6 +312,7 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() { network_sandbox_policy: NetworkSandboxPolicy::Restricted, sandbox_policy_cwd: Path::new("/"), enforce_managed_network: false, + managed_network: None, environment_id: None, network: None, extra_allow_unix_sockets: &[], @@ -585,6 +619,7 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() { network_sandbox_policy: NetworkSandboxPolicy::Restricted, sandbox_policy_cwd: cwd.path(), enforce_managed_network: false, + managed_network: None, environment_id: None, network: None, extra_allow_unix_sockets: &extra_allow_unix_sockets, @@ -645,6 +680,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a network_sandbox_policy: NetworkSandboxPolicy::Restricted, sandbox_policy_cwd: cwd.path(), enforce_managed_network: false, + managed_network: None, environment_id: None, network: Some(&network_proxy), extra_allow_unix_sockets: &extra_allow_unix_sockets, @@ -688,6 +724,7 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths() network_sandbox_policy: NetworkSandboxPolicy::Enabled, sandbox_policy_cwd: cwd.path(), enforce_managed_network: false, + managed_network: None, environment_id: None, network: None, extra_allow_unix_sockets: &extra_allow_unix_sockets, From bff385c5128ccc68a63b3c15efb8c9a75101258a Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 22 Jun 2026 19:18:55 +0200 Subject: [PATCH 2/3] Fix platform-specific managed network builds --- codex-rs/cli/src/debug_sandbox.rs | 1 + codex-rs/core/src/tools/runtimes/unified_exec.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 5e5bcb6c4e1d..296fa7543f68 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -282,6 +282,7 @@ async fn run_command_under_sandbox( network_sandbox_policy, sandbox_policy_cwd: sandbox_policy_cwd.as_path(), enforce_managed_network, + managed_network: None, environment_id: None, network: network.as_ref(), extra_allow_unix_sockets: allow_unix_sockets, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index d9d4d02fcf9a..5df42cc979a5 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -325,7 +325,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt launch_sandbox_permissions, ); let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); - let (mut env, managed_network_context) = match managed_network { + let (env, managed_network_context) = match managed_network { Some(network) => { let prepared = network .prepare_for_optional_environment( @@ -344,6 +344,8 @@ impl<'a> ToolRuntime for UnifiedExecRunt }; let explicit_env_overrides = req.explicit_env_overrides.clone(); #[cfg(unix)] + let mut env = env; + #[cfg(unix)] let runtime_path_prepends = { let mut runtime_path_prepends = RuntimePathPrepends::default(); if !environment_is_remote { From 19f5f6b3f9ecef4cc1b933076c009be81135dae0 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 23 Jun 2026 12:10:05 +0100 Subject: [PATCH 3/3] Preserve prepared managed network state --- codex-rs/core/src/tools/runtimes/mod.rs | 30 +++++++++---------- codex-rs/core/src/tools/sandboxing.rs | 2 +- codex-rs/core/src/tools/sandboxing_tests.rs | 24 +++++++++++---- .../core/src/unified_exec/process_manager.rs | 15 +++------- .../src/unified_exec/process_manager_tests.rs | 8 +++++ codex-rs/exec-server/src/process_sandbox.rs | 17 +++++++++++ codex-rs/exec-server/src/protocol.rs | 5 ++-- codex-rs/sandboxing/src/manager.rs | 5 +--- codex-rs/sandboxing/src/manager_tests.rs | 8 +---- 9 files changed, 68 insertions(+), 46 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 205476f41f5e..db55cc2bbc20 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -68,28 +68,28 @@ pub(crate) fn exec_env_for_sandbox_permissions( env } -pub(crate) fn strip_managed_proxy_env(env: &mut HashMap) { - for key in PROXY_ENV_KEYS { - env.remove(*key); +pub(crate) fn is_managed_proxy_env_var(key: &str, value: &str) -> bool { + if PROXY_ENV_KEYS.contains(&key) { + return true; } - for key in CUSTOM_CA_ENV_KEYS { - if env - .get(key) - .is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value)) - { - env.remove(key); - } + if CUSTOM_CA_ENV_KEYS.contains(&key) { + return is_managed_mitm_ca_trust_bundle_path(value); } - // Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy. #[cfg(target_os = "macos")] - if env - .get(PROXY_GIT_SSH_COMMAND_ENV_KEY) - .is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)) { - env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY); + key == PROXY_GIT_SSH_COMMAND_ENV_KEY + && value.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER) + } + #[cfg(not(target_os = "macos"))] + { + false } } +pub(crate) fn strip_managed_proxy_env(env: &mut HashMap) { + env.retain(|key, value| !is_managed_proxy_env_var(key, value)); +} + /// Prepends `path_entry` to `PATH`, removing duplicate and empty existing /// entries. /// diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 4eae3bd0db6e..ee398dae595a 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -493,6 +493,7 @@ impl<'a> SandboxAttempt<'a> { options, self.workspace_roots.to_vec(), ); + exec_request.exec_server_managed_network = managed_network; if self.sandbox_requested { exec_request.exec_server_sandbox = Some(FileSystemSandboxContext { permissions: exec_server_permissions.into(), @@ -503,7 +504,6 @@ impl<'a> SandboxAttempt<'a> { use_legacy_landlock: self.use_legacy_landlock, }); exec_request.exec_server_enforce_managed_network = self.enforce_managed_network; - exec_request.exec_server_managed_network = managed_network; } Ok(exec_request) } diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index abf2f8c36872..28b9de9ce156 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -213,7 +213,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { .clone() .materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd)); let manager = SandboxManager::new(); - let attempt = SandboxAttempt { + let mut attempt = SandboxAttempt { sandbox: SandboxType::None, sandbox_requested: true, permissions: &permissions, @@ -232,7 +232,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { loopback_ports: vec![43123], allow_local_binding: false, }; - let command = SandboxCommand { + let command = || SandboxCommand { program: "/bin/bash".into(), args: vec!["-lc".to_string(), "pwd".to_string()], cwd: cwd_uri.clone(), @@ -240,12 +240,12 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { managed_network: Some(managed_network.clone()), additional_permissions: None, }; - let options = crate::sandboxing::ExecOptions { + let options = || crate::sandboxing::ExecOptions { expiration: crate::exec::ExecExpiration::DefaultTimeout, capture_policy: crate::exec::ExecCapturePolicy::ShellTool, }; let request = attempt - .env_for_exec_server(command, options, /*network*/ None, Some("remote")) + .env_for_exec_server(command(), options(), /*network*/ None, Some("remote")) .expect("prepare remote exec request"); assert_eq!( @@ -261,8 +261,8 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { assert_eq!( request.exec_server_sandbox, Some(codex_exec_server::FileSystemSandboxContext { - permissions: exec_server_permissions.into(), - cwd: Some(cwd_uri), + permissions: exec_server_permissions.clone().into(), + cwd: Some(cwd_uri.clone()), workspace_roots: Vec::new(), windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -270,5 +270,17 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { }) ); assert!(request.exec_server_enforce_managed_network); + assert_eq!( + request.exec_server_managed_network, + Some(managed_network.clone()) + ); + + attempt.sandbox_requested = false; + let request = attempt + .env_for_exec_server(command(), options(), /*network*/ None, Some("remote")) + .expect("prepare unsandboxed remote exec request"); + + assert_eq!(request.exec_server_sandbox, None); + assert!(!request.exec_server_enforce_managed_network); assert_eq!(request.exec_server_managed_network, Some(managed_network)); } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 5c88a1da393a..5e8944576567 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -26,6 +26,7 @@ use crate::tools::events::ToolEventStage; use crate::tools::network_approval::DeferredNetworkApproval; use crate::tools::network_approval::finish_deferred_network_approval; use crate::tools::orchestrator::ToolOrchestrator; +use crate::tools::runtimes::is_managed_proxy_env_var; use crate::tools::runtimes::unified_exec::UnifiedExecRequest as UnifiedExecToolRequest; use crate::tools::runtimes::unified_exec::UnifiedExecRuntime; use crate::tools::sandboxing::SandboxAttempt; @@ -53,11 +54,7 @@ use crate::unified_exec::process::OutputBuffer; use crate::unified_exec::process::OutputHandles; use crate::unified_exec::process::SpawnLifecycleHandle; use crate::unified_exec::process::UnifiedExecProcess; -use codex_network_proxy::CUSTOM_CA_ENV_KEYS; use codex_network_proxy::NetworkProxy; -use codex_network_proxy::PROXY_ENV_KEYS; -#[cfg(target_os = "macos")] -use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; use codex_protocol::config_types::ShellEnvironmentPolicy; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; @@ -150,15 +147,11 @@ fn exec_server_env_for_request( let mut env = env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env); if request.exec_server_managed_network.is_some() { - for key in PROXY_ENV_KEYS.iter().chain(CUSTOM_CA_ENV_KEYS.iter()) { - if let Some(value) = request.env.get(*key) { - env.insert((*key).to_string(), value.clone()); + for (key, value) in &request.env { + if is_managed_proxy_env_var(key, value) { + env.insert(key.clone(), value.clone()); } } - #[cfg(target_os = "macos")] - if let Some(value) = request.env.get(PROXY_GIT_SSH_COMMAND_ENV_KEY) { - env.insert(PROXY_GIT_SSH_COMMAND_ENV_KEY.to_string(), value.clone()); - } } (Some(exec_server_env_config.policy.clone()), env) } else { diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 290dfe15617b..93ffd0cedb8a 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -93,6 +93,10 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { "http://127.0.0.1:43123".to_string(), ), ("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()), + ( + "SSL_CERT_FILE".to_string(), + "/client/custom-ca.pem".to_string(), + ), ]), exec_server_env_config: Some(ExecServerEnvConfig { policy: codex_exec_server::ExecEnvPolicy { @@ -110,6 +114,10 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { "http://127.0.0.1:43123".to_string(), ), ("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string()), + ( + "SSL_CERT_FILE".to_string(), + "/client/custom-ca.pem".to_string(), + ), ]), }), network: None, diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index fde24119eb30..e3bffeaa6069 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use codex_app_server_protocol::JSONRPCErrorError; +use codex_network_proxy::CUSTOM_CA_ENV_KEYS; +use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxDirectSpawnTransformRequest; @@ -8,6 +10,7 @@ use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; +use codex_sandboxing::with_managed_mitm_ca_readable_root; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; @@ -59,6 +62,20 @@ pub(crate) fn prepare_exec_request( native_workspace_roots.as_slice() }; let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots); + let managed_mitm_ca_trust_bundle_path = params.managed_network.as_ref().and_then(|_| { + CUSTOM_CA_ENV_KEYS.iter().find_map(|key| { + let path = env.get(*key)?; + if !is_managed_mitm_ca_trust_bundle_path(path) { + return None; + } + AbsolutePathBuf::from_absolute_path(path).ok() + }) + }); + let permissions = with_managed_mitm_ca_readable_root( + permissions, + managed_mitm_ca_trust_bundle_path.as_ref(), + native_sandbox_policy_cwd.as_path(), + ); let (file_system_policy, network_policy) = permissions.to_runtime_permissions(); let sandbox_manager = SandboxManager::new(); let sandbox = sandbox_manager.select_initial( diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 881738a6c67d..921223e312bc 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -524,8 +524,9 @@ mod tests { #[test] fn exec_params_managed_network_context_round_trips_and_defaults_for_legacy_peers() { - let cwd = PathUri::from_path(std::env::current_dir().expect("current directory")) - .expect("cwd URI"); + let cwd = + PathUri::from_host_native_path(std::env::current_dir().expect("current directory")) + .expect("cwd URI"); let params = ExecParams { process_id: ProcessId::from("managed-network"), argv: vec!["true".to_string()], diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 46fa6e7bda77..30870417fe38 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -484,14 +484,12 @@ impl SandboxManager { codex_home: &Path, ) -> Result { let workspace_roots = request.workspace_roots; - let enforce_managed_network = request.transform.enforce_managed_network; let mut request = self.transform(request.transform)?; if request.sandbox == SandboxType::WindowsRestrictedToken { wrap_windows_sandbox_exec_request_for_direct_spawn( &mut request, workspace_roots, codex_home, - enforce_managed_network, )?; } Ok(request) @@ -503,7 +501,6 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn( request: &mut SandboxExecRequest, workspace_roots: &[AbsolutePathBuf], codex_home: &Path, - enforce_managed_network: bool, ) -> Result<(), SandboxTransformError> { // TODO(anp): Keep PathUri through the Windows sandbox wrapper boundary. let native_cwd = @@ -530,7 +527,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn( *program = helper.to_string_lossy().into_owned(); let inner_command = std::mem::take(&mut request.command); - let proxy_enforced = enforce_managed_network || request.network.is_some(); + let proxy_enforced = request.network.is_some(); let use_elevated = windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced); let overrides = if use_elevated { diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 81aa1fbea62c..547155c63d5a 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -513,7 +513,7 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { }, permissions: &permissions, sandbox: SandboxType::WindowsRestrictedToken, - enforce_managed_network: true, + enforce_managed_network: false, environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, @@ -544,12 +544,6 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { .iter() .any(|arg| arg == "--run-as-windows-sandbox") ); - assert!( - exec_request - .command - .iter() - .any(|arg| arg == "--proxy-enforced") - ); assert!( exec_request .command