From 55b9447a17a04b250dd48a5943d18f926401762d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 7 Jul 2026 11:24:22 -0700 Subject: [PATCH 1/4] fix(exec-server): start managed proxy on executor Co-authored-by: Codex noreply@openai.com --- .../core/src/tools/runtimes/unified_exec.rs | 21 +++++ codex-rs/core/src/tools/sandboxing.rs | 7 +- codex-rs/core/src/tools/sandboxing_tests.rs | 5 +- .../core/src/unified_exec/process_manager.rs | 2 +- .../src/unified_exec/process_manager_tests.rs | 1 + codex-rs/exec-server-protocol/src/protocol.rs | 1 + codex-rs/exec-server/src/local_process.rs | 17 +++- codex-rs/exec-server/src/process_sandbox.rs | 51 ++++++++++- .../exec-server/src/process_sandbox_tests.rs | 91 +++++++++++++++++-- codex-rs/network-proxy/src/lib.rs | 2 + codex-rs/network-proxy/src/proxy.rs | 5 + codex-rs/network-proxy/src/remote_config.rs | 70 ++++++++++++++ .../network-proxy/src/remote_config_tests.rs | 46 ++++++++++ codex-rs/network-proxy/src/runtime.rs | 29 ++++++ codex-rs/sandboxing/src/seatbelt_tests.rs | 1 + 15 files changed, 327 insertions(+), 22 deletions(-) create mode 100644 codex-rs/network-proxy/src/remote_config.rs create mode 100644 codex-rs/network-proxy/src/remote_config_tests.rs diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index a1b9b33c6d5..c05f9b4fd3f 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -42,6 +42,7 @@ use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::RemoteNetworkProxyConfig; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::models::AdditionalPermissionProfile; @@ -324,6 +325,26 @@ impl<'a> ToolRuntime for UnifiedExecRunt )); let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); let (env, managed_network_context) = match managed_network { + Some(network) if environment_is_remote => { + let config = network.current_cfg().await.map_err(|err| { + ToolError::Codex(CodexErr::Io(io::Error::other(format!( + "failed to read effective network proxy config: {err}" + )))) + })?; + let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config) + .map_err(|err| { + ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string()))) + })?; + let allow_local_binding = proxy_config.allow_local_binding; + ( + env, + Some(codex_network_proxy::ManagedNetworkSandboxContext { + loopback_ports: Vec::new(), + allow_local_binding, + proxy_config: Some(proxy_config), + }), + ) + } Some(network) => { let prepared = network .prepare_for_optional_environment( diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 571c5ce6dfc..793ad35a591 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -475,10 +475,7 @@ impl<'a> SandboxAttempt<'a> { &self, command: SandboxCommand, options: ExecOptions, - network: Option<&NetworkProxy>, - environment_id: Option<&str>, ) -> Result { - let network = self.network_proxy(network); let managed_network = command.managed_network.clone(); let exec_server_permissions = effective_permission_profile( self.exec_server_permissions, @@ -492,8 +489,8 @@ impl<'a> SandboxAttempt<'a> { // The exec-server must receive the native command, not this host's wrapper. sandbox: SandboxType::None, enforce_managed_network: self.enforce_managed_network, - environment_id, - network, + environment_id: None, + network: None, sandbox_policy_cwd: self.sandbox_cwd, codex_linux_sandbox_exe: None, use_legacy_landlock: self.use_legacy_landlock, diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index e52186f7721..ef7ef8b8e9d 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -232,6 +232,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, + proxy_config: None, }; let command = || SandboxCommand { program: "/bin/bash".into(), @@ -246,7 +247,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { 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()) .expect("prepare remote exec request"); assert_eq!( @@ -278,7 +279,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { attempt.sandbox_requested = false; let request = attempt - .env_for_exec_server(command(), options(), /*network*/ None, Some("remote")) + .env_for_exec_server(command(), options()) .expect("prepare unsandboxed remote exec request"); assert_eq!(request.exec_server_sandbox, None); diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fe3ec45759b..40f9bd95543 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -922,7 +922,7 @@ impl UnifiedExecProcessManager { environment: &codex_exec_server::Environment, ) -> Result { let mut request = if environment.is_remote() { - attempt.env_for_exec_server(command, options, network, environment_id) + attempt.env_for_exec_server(command, options) } else { attempt.env_for(command, options, network, environment_id) } 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 20a930b5525..056d6c4c9cf 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -117,6 +117,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, + proxy_config: None, }; let mut request = ExecRequest { command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()], diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index a265aaed0c3..45834bed3e9 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -598,6 +598,7 @@ mod tests { managed_network: Some(ManagedNetworkSandboxContext { loopback_ports: vec![43123, 48081], allow_local_binding: false, + proxy_config: None, }), }; diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index 3c488f0ad7c..fdecc37f538 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -8,6 +8,7 @@ use std::sync::atomic::Ordering; use std::time::Duration; use codex_exec_server_protocol::JSONRPCErrorError; +use codex_network_proxy::NetworkProxyHandle; use codex_protocol::config_types::EnvironmentVariablePattern; use codex_protocol::config_types::ShellEnvironmentPolicy; use codex_protocol::exec_output::ExecToolCallOutput; @@ -97,6 +98,7 @@ struct RunningProcess { termination_requested: bool, sandbox: SandboxType, sandbox_denied: bool, + network_proxy_handle: Option, } /// Bounded cache of stdin write ids that have already been accepted for one process. @@ -235,7 +237,7 @@ impl LocalProcess { ) -> Result<(ExecResponse, watch::Sender, ExecProcessEventLog), JSONRPCErrorError> { let process_id = params.process_id.clone(); let prepared = - prepare_exec_request(¶ms, child_env(¶ms), self.runtime_paths.as_ref())?; + prepare_exec_request(¶ms, child_env(¶ms), self.runtime_paths.as_ref()).await?; let (program, args) = prepared .command .split_first() @@ -338,6 +340,7 @@ impl LocalProcess { termination_requested: false, sandbox: prepared.sandbox, sandbox_denied: false, + network_proxy_handle: prepared.network_proxy_handle, })), ); } @@ -823,7 +826,7 @@ async fn watch_exit( output_notify: Arc, ) { let exit_code = exit_rx.await.unwrap_or(-1); - let sandboxed = { + let (sandboxed, network_proxy_handle) = { let mut processes = inner.processes.lock().await; match processes.get_mut(&process_id) { Some(ProcessEntry::Running(process)) => { @@ -837,11 +840,16 @@ async fn watch_exit( "error" }); } - sandboxed + (sandboxed, process.network_proxy_handle.take()) } - Some(ProcessEntry::Starting(_)) | None => false, + Some(ProcessEntry::Starting(_)) | None => (false, None), } }; + if let Some(network_proxy_handle) = network_proxy_handle + && let Err(err) = network_proxy_handle.shutdown().await + { + tracing::warn!("failed to shut down executor network proxy: {err}"); + } if sandboxed { let _ = tokio::time::timeout(Duration::from_millis(20), output_notify.notified()).await; } @@ -1310,6 +1318,7 @@ mod tests { termination_requested: false, sandbox: SandboxType::None, sandbox_denied: false, + network_proxy_handle: None, })), ); assert!(previous.is_none()); diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index b5dd54e0b2c..feced151856 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; +use std::sync::Arc; use codex_exec_server_protocol::JSONRPCErrorError; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; +use codex_network_proxy::ManagedNetworkSandboxContext; +use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyHandle; +use codex_network_proxy::NetworkProxyState; use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxCommand; @@ -16,6 +21,7 @@ use codex_utils_path_uri::PathUri; use crate::ExecServerRuntimePaths; use crate::protocol::ExecParams; +use crate::rpc::internal_error; use crate::rpc::invalid_params; pub(crate) struct PreparedExecRequest { @@ -24,13 +30,16 @@ pub(crate) struct PreparedExecRequest { pub(crate) env: HashMap, pub(crate) arg0: Option, pub(crate) sandbox: SandboxType, + pub(crate) network_proxy_handle: Option, } -pub(crate) fn prepare_exec_request( +pub(crate) async fn prepare_exec_request( params: &ExecParams, env: HashMap, runtime_paths: Option<&ExecServerRuntimePaths>, ) -> Result { + let (env, managed_network, network_proxy_handle) = + prepare_managed_network(params.managed_network.as_ref(), env).await?; let Some(sandbox_context) = params.sandbox.as_ref() else { return Ok(PreparedExecRequest { command: params.argv.clone(), @@ -38,6 +47,7 @@ pub(crate) fn prepare_exec_request( env, arg0: params.arg0.clone(), sandbox: SandboxType::None, + network_proxy_handle, }); }; let runtime_paths = runtime_paths @@ -62,7 +72,7 @@ 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(|_| { + let managed_mitm_ca_trust_bundle_path = 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) { @@ -117,7 +127,7 @@ pub(crate) fn prepare_exec_request( args: args.to_vec(), cwd: params.cwd.clone(), env, - managed_network: params.managed_network.clone(), + managed_network, additional_permissions: None, }, permissions: &permissions, @@ -139,9 +149,44 @@ pub(crate) fn prepare_exec_request( env: request.env, arg0: request.arg0, sandbox: request.sandbox, + network_proxy_handle, }) } +async fn prepare_managed_network( + managed_network: Option<&ManagedNetworkSandboxContext>, + env: HashMap, +) -> Result< + ( + HashMap, + Option, + Option, + ), + JSONRPCErrorError, +> { + let Some(proxy_config) = managed_network.and_then(|context| context.proxy_config.clone()) + else { + return Ok((env, managed_network.cloned(), None)); + }; + let state = NetworkProxyState::from_static_config(proxy_config.into_network_proxy_config()) + .map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?; + let proxy = NetworkProxy::builder() + .state(Arc::new(state)) + .build() + .await + .map_err(|err| internal_error(format!("failed to build executor network proxy: {err}")))?; + let handle = proxy + .run() + .await + .map_err(|err| internal_error(format!("failed to start executor network proxy: {err}")))?; + let prepared = proxy + .prepare_for_optional_environment(env, /*environment_id*/ None) + .map_err(|err| { + internal_error(format!("failed to prepare executor network proxy: {err}")) + })?; + Ok((prepared.env, Some(prepared.sandbox_context), Some(handle))) +} + fn native_path(path: &PathUri, label: &str) -> Result { path.to_abs_path().map_err(|err| { invalid_params(format!( diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index bc71b573201..2f49fd93f08 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -1,12 +1,18 @@ use std::collections::HashMap; +use std::net::SocketAddr; +use std::time::Duration; -#[cfg(target_os = "macos")] use codex_network_proxy::ManagedNetworkSandboxContext; +use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::RemoteNetworkProxyConfig; #[cfg(unix)] use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; +use tokio::time::timeout; use super::prepare_exec_request; use crate::ExecParams; @@ -17,8 +23,8 @@ use crate::FileSystemSandboxContext; use crate::ProcessId; #[cfg(unix)] -#[test] -fn sandbox_request_wraps_native_argv_on_executor() { +#[tokio::test] +async fn sandbox_request_wraps_native_argv_on_executor() { let cwd: AbsolutePathBuf = std::env::current_dir() .expect("current directory") .try_into() @@ -50,6 +56,7 @@ fn sandbox_request_wraps_native_argv_on_executor() { }; let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) + .await .expect("prepare sandboxed request"); assert_ne!(prepared.command, params.argv); @@ -82,8 +89,8 @@ fn sandbox_request_wraps_native_argv_on_executor() { } #[cfg(target_os = "macos")] -#[test] -fn sandbox_request_allows_prepared_managed_proxy_port() { +#[tokio::test] +async fn sandbox_request_allows_prepared_managed_proxy_port() { let cwd: AbsolutePathBuf = std::env::current_dir() .expect("current directory") .try_into() @@ -110,10 +117,12 @@ fn sandbox_request_allows_prepared_managed_proxy_port() { managed_network: Some(ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, + proxy_config: None, }), }; let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) + .await .expect("prepare managed-network sandbox request"); let policy = prepared .command @@ -124,8 +133,8 @@ fn sandbox_request_allows_prepared_managed_proxy_port() { assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))")); } -#[test] -fn native_request_preserves_native_launch_fields() { +#[tokio::test] +async fn native_request_preserves_native_launch_fields() { let cwd: AbsolutePathBuf = std::env::current_dir() .expect("current directory") .try_into() @@ -147,6 +156,7 @@ fn native_request_preserves_native_launch_fields() { }; let prepared = prepare_exec_request(¶ms, env.clone(), /*runtime_paths*/ None) + .await .expect("prepare native request"); assert_eq!(prepared.command, params.argv); @@ -154,3 +164,70 @@ fn native_request_preserves_native_launch_fields() { assert_eq!(prepared.env, env); assert_eq!(prepared.arg0, params.arg0); } + +#[tokio::test] +async fn remote_proxy_config_starts_executor_local_proxy() { + let cwd: AbsolutePathBuf = std::env::current_dir() + .expect("current directory") + .try_into() + .expect("absolute cwd"); + let mut config = NetworkProxyConfig::default(); + config.network.enabled = true; + config + .network + .set_allowed_domains(vec!["allowed.example".to_string()]); + let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config) + .expect("supported remote proxy config"); + let params = ExecParams { + process_id: ProcessId::from("process-remote-proxy"), + argv: vec!["echo".to_string(), "hello".to_string()], + cwd: PathUri::from_abs_path(&cwd), + env_policy: None, + env: HashMap::new(), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: None, + enforce_managed_network: false, + managed_network: Some(ManagedNetworkSandboxContext { + loopback_ports: Vec::new(), + allow_local_binding: false, + proxy_config: Some(proxy_config), + }), + }; + let stale_proxy = "http://127.0.0.1:9".to_string(); + let env = HashMap::from([("HTTP_PROXY".to_string(), stale_proxy.clone())]); + + let prepared = prepare_exec_request(¶ms, env, /*runtime_paths*/ None) + .await + .expect("prepare request with executor-local proxy"); + + let http_proxy = prepared.env.get("HTTP_PROXY").expect("HTTP proxy env"); + assert_ne!(http_proxy, &stale_proxy); + assert!(http_proxy.starts_with("http://127.0.0.1:")); + let proxy_addr: SocketAddr = http_proxy + .strip_prefix("http://") + .expect("HTTP proxy scheme") + .parse() + .expect("HTTP proxy address"); + let mut stream = tokio::net::TcpStream::connect(proxy_addr) + .await + .expect("connect to executor proxy"); + stream + .write_all(b"CONNECT blocked.example:443 HTTP/1.1\r\nHost: blocked.example:443\r\n\r\n") + .await + .expect("write CONNECT request"); + let mut response = [0_u8; 256]; + let response_len = timeout(Duration::from_secs(2), stream.read(&mut response)) + .await + .expect("proxy response timeout") + .expect("read proxy response"); + assert!(String::from_utf8_lossy(&response[..response_len]).starts_with("HTTP/1.1 403")); + + prepared + .network_proxy_handle + .expect("running executor proxy") + .shutdown() + .await + .expect("shut down executor proxy"); +} diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 6bf6d8eb389..6ee14f33c9a 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -13,6 +13,7 @@ mod network_policy; mod policy; mod proxy; mod reasons; +mod remote_config; mod responses; mod runtime; mod socks5; @@ -67,6 +68,7 @@ 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 remote_config::RemoteNetworkProxyConfig; pub use runtime::BlockedRequest; pub use runtime::BlockedRequestArgs; pub use runtime::BlockedRequestObserver; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 476c68aca8b..09784184591 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -348,6 +348,9 @@ pub struct ManagedNetworkSandboxContext { /// Whether the command may bind local sockets and exchange loopback traffic. #[serde(default)] pub allow_local_binding: bool, + /// Effective proxy settings for an exec-server that must start its own local listeners. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy_config: Option, } /// Environment-specific managed-network settings prepared for one command launch. @@ -727,6 +730,7 @@ impl NetworkProxy { sandbox_context: ManagedNetworkSandboxContext { loopback_ports, allow_local_binding: runtime_settings.allow_local_binding, + proxy_config: None, }, } } @@ -1216,6 +1220,7 @@ mod tests { ManagedNetworkSandboxContext { loopback_ports: expected_ports, allow_local_binding: false, + proxy_config: None, } ); } diff --git a/codex-rs/network-proxy/src/remote_config.rs b/codex-rs/network-proxy/src/remote_config.rs new file mode 100644 index 00000000000..fbb43880f6e --- /dev/null +++ b/codex-rs/network-proxy/src/remote_config.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use anyhow::ensure; +use serde::Deserialize; +use serde::Serialize; + +use crate::NetworkDomainPermissions; +use crate::NetworkMode; +use crate::NetworkProxyConfig; +use crate::NetworkUnixSocketPermissions; + +/// Effective network proxy settings that are safe to send to a remote executor. +/// +/// Listener addresses are deliberately omitted because the executor chooses its own loopback +/// ports. MITM, credential injection, and hooks are not represented so their configuration cannot +/// cross the exec-server boundary accidentally. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteNetworkProxyConfig { + pub enabled: bool, + pub enable_socks5: bool, + pub enable_socks5_udp: bool, + pub allow_upstream_proxy: bool, + pub dangerously_allow_all_unix_sockets: bool, + pub mode: NetworkMode, + pub domains: Option, + pub unix_sockets: Option, + pub allow_local_binding: bool, +} + +impl RemoteNetworkProxyConfig { + pub fn from_effective_config(config: &NetworkProxyConfig) -> Result { + let settings = &config.network; + ensure!( + !settings.mitm + && !settings.credential_broker + && !settings.dangerously_allow_plaintext_credential_injection + && settings.mitm_hooks.is_empty(), + "remote exec-server network proxy does not support MITM, credential injection, or MITM hooks" + ); + Ok(Self { + enabled: settings.enabled, + enable_socks5: settings.enable_socks5, + enable_socks5_udp: settings.enable_socks5_udp, + allow_upstream_proxy: settings.allow_upstream_proxy, + dangerously_allow_all_unix_sockets: settings.dangerously_allow_all_unix_sockets, + mode: settings.mode, + domains: settings.domains.clone(), + unix_sockets: settings.unix_sockets.clone(), + allow_local_binding: settings.allow_local_binding, + }) + } + + pub fn into_network_proxy_config(self) -> NetworkProxyConfig { + let mut config = NetworkProxyConfig::default(); + config.network.enabled = self.enabled; + config.network.enable_socks5 = self.enable_socks5; + config.network.enable_socks5_udp = self.enable_socks5_udp; + config.network.allow_upstream_proxy = self.allow_upstream_proxy; + config.network.dangerously_allow_all_unix_sockets = self.dangerously_allow_all_unix_sockets; + config.network.mode = self.mode; + config.network.domains = self.domains; + config.network.unix_sockets = self.unix_sockets; + config.network.allow_local_binding = self.allow_local_binding; + config + } +} + +#[cfg(test)] +#[path = "remote_config_tests.rs"] +mod tests; diff --git a/codex-rs/network-proxy/src/remote_config_tests.rs b/codex-rs/network-proxy/src/remote_config_tests.rs new file mode 100644 index 00000000000..c584592170e --- /dev/null +++ b/codex-rs/network-proxy/src/remote_config_tests.rs @@ -0,0 +1,46 @@ +use pretty_assertions::assert_eq; + +use super::RemoteNetworkProxyConfig; +use crate::NetworkMode; +use crate::NetworkProxyConfig; + +#[test] +fn round_trip_preserves_supported_effective_settings() { + let mut config = NetworkProxyConfig::default(); + config.network.enabled = true; + config.network.enable_socks5 = false; + config.network.enable_socks5_udp = false; + config.network.allow_upstream_proxy = false; + config.network.dangerously_allow_all_unix_sockets = true; + config.network.mode = NetworkMode::Limited; + config + .network + .set_allowed_domains(vec!["example.com".into()]); + config + .network + .set_denied_domains(vec!["blocked.example.com".into()]); + config + .network + .set_allow_unix_sockets(vec!["/var/run/example.sock".into()]); + config.network.allow_local_binding = true; + + let remote = + RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config"); + let round_trip = remote.into_network_proxy_config(); + + assert_eq!(round_trip, config); +} + +#[test] +fn rejects_mitm_configuration() { + let mut config = NetworkProxyConfig::default(); + config.network.mitm = true; + + let error = RemoteNetworkProxyConfig::from_effective_config(&config) + .expect_err("MITM must not cross the remote executor boundary"); + + assert_eq!( + error.to_string(), + "remote exec-server network proxy does not support MITM, credential injection, or MITM hooks" + ); +} diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index a51f1d8f165..0afb54c940b 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -187,6 +187,26 @@ pub trait ConfigReloader: Send + Sync { pub type ConfigReloaderFuture<'a, T> = Pin> + Send + 'a>>; +#[derive(Clone)] +struct StaticConfigReloader { + state: ConfigState, +} + +impl ConfigReloader for StaticConfigReloader { + fn source_label(&self) -> String { + "static config state".to_string() + } + + fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option> { + Box::pin(async { Ok(None) }) + } + + fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> { + let state = self.state.clone(); + Box::pin(async move { Ok(state) }) + } +} + pub trait BlockedRequestObserver: Send + Sync + 'static { fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_>; } @@ -257,6 +277,15 @@ impl Clone for NetworkProxyState { } impl NetworkProxyState { + /// Builds immutable runtime state from an already-computed effective configuration. + pub fn from_static_config(config: NetworkProxyConfig) -> Result { + let state = build_config_state(config, NetworkProxyConstraints::default())?; + Ok(Self::with_reloader( + state.clone(), + Arc::new(StaticConfigReloader { state }), + )) + } + pub fn with_reloader(state: ConfigState, reloader: Arc) -> Self { Self::with_reloader_and_audit_metadata( state, diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index b15019a8cb7..5e18ea95cd6 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -269,6 +269,7 @@ fn prepared_managed_network_context_allows_only_its_proxy_ports() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123, 48081], allow_local_binding: false, + proxy_config: None, }; let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/bin/true".to_string()], From 2af252a7b8192f12f411b18f8f9f62f6c723ddcf Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 7 Jul 2026 11:43:50 -0700 Subject: [PATCH 2/4] fix(network-proxy): keep remote config extensible Co-authored-by: Codex noreply@openai.com --- codex-rs/network-proxy/src/remote_config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/network-proxy/src/remote_config.rs b/codex-rs/network-proxy/src/remote_config.rs index fbb43880f6e..d076c308374 100644 --- a/codex-rs/network-proxy/src/remote_config.rs +++ b/codex-rs/network-proxy/src/remote_config.rs @@ -15,6 +15,7 @@ use crate::NetworkUnixSocketPermissions; /// cross the exec-server boundary accidentally. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct RemoteNetworkProxyConfig { pub enabled: bool, pub enable_socks5: bool, From 6d48c777bc931ec68c9f74cbf2ce37dab7438229 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 9 Jul 2026 10:04:13 -0700 Subject: [PATCH 3/4] fix(exec-server): separate proxy launch inputs Co-authored-by: Codex noreply@openai.com --- codex-rs/core/src/exec.rs | 1 + codex-rs/core/src/sandboxing/mod.rs | 4 ++ codex-rs/core/src/tasks/user_shell.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 2 + .../core/src/tools/runtimes/unified_exec.rs | 28 ++++--------- codex-rs/core/src/tools/sandboxing_tests.rs | 1 - .../core/src/unified_exec/process_manager.rs | 3 ++ .../src/unified_exec/process_manager_tests.rs | 2 +- codex-rs/exec-server-protocol/src/protocol.rs | 31 +++++++++++++- codex-rs/exec-server/src/client.rs | 1 + codex-rs/exec-server/src/environment.rs | 2 + codex-rs/exec-server/src/local_process.rs | 1 + codex-rs/exec-server/src/process_sandbox.rs | 15 ++++--- .../exec-server/src/process_sandbox_tests.rs | 15 ++++--- .../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 | 1 + codex-rs/network-proxy/src/proxy.rs | 28 ++++++++++--- .../src/proxy/execution_scope.rs | 2 + codex-rs/network-proxy/src/remote_config.rs | 40 +++++++++++++++++++ .../network-proxy/src/remote_config_tests.rs | 27 +++++++++++++ codex-rs/network-proxy/src/runtime.rs | 27 ++++++++++++- .../rmcp-client/src/stdio_server_launcher.rs | 1 + codex-rs/sandboxing/src/seatbelt_tests.rs | 1 - 26 files changed, 206 insertions(+), 43 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 015b20715af..47dd317de1d 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -461,6 +461,7 @@ pub(crate) async fn execute_exec_request( exec_server_sandbox: _, exec_server_enforce_managed_network: _, exec_server_managed_network: _, + exec_server_network_proxy: _, } = 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 1c570e1bbb1..9a59477709b 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -17,6 +17,7 @@ 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_network_proxy::RemoteNetworkProxyLaunchConfig; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::PermissionProfile; @@ -65,6 +66,7 @@ pub struct ExecRequest { pub(crate) exec_server_sandbox: Option, pub(crate) exec_server_enforce_managed_network: bool, pub(crate) exec_server_managed_network: Option, + pub(crate) exec_server_network_proxy: Option, } impl ExecRequest { @@ -110,6 +112,7 @@ impl ExecRequest { exec_server_sandbox: None, exec_server_enforce_managed_network: false, exec_server_managed_network: None, + exec_server_network_proxy: None, } } @@ -169,6 +172,7 @@ impl ExecRequest { exec_server_sandbox: None, exec_server_enforce_managed_network: false, exec_server_managed_network: None, + exec_server_network_proxy: None, } } } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index a15c53e16b4..ba40893bcc0 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -231,6 +231,7 @@ pub(crate) async fn execute_user_shell_command( exec_server_sandbox: None, exec_server_enforce_managed_network: false, exec_server_managed_network: None, + exec_server_network_proxy: None, }; let stdout_stream = Some(StdoutStream { 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 10cb4602056..42ffd3e9aa6 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -169,6 +169,7 @@ pub(super) async fn try_run_zsh_fork( exec_server_sandbox: _, exec_server_enforce_managed_network: _, exec_server_managed_network: _, + exec_server_network_proxy: _, } = sandbox_exec_request; let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?; let effective_timeout = Duration::from_millis( @@ -904,6 +905,7 @@ impl CoreShellCommandExecutor { exec_server_sandbox: None, exec_server_enforce_managed_network: false, exec_server_managed_network: None, + exec_server_network_proxy: None, }, /*stdout_stream*/ None, after_spawn, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index c05f9b4fd3f..5c924e2ba88 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -42,7 +42,6 @@ use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; -use codex_network_proxy::RemoteNetworkProxyConfig; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::models::AdditionalPermissionProfile; @@ -324,26 +323,12 @@ impl<'a> ToolRuntime for UnifiedExecRunt launch_sandbox_permissions, )); let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); - let (env, managed_network_context) = match managed_network { + let (env, managed_network_context, network_proxy_launch) = match managed_network { Some(network) if environment_is_remote => { - let config = network.current_cfg().await.map_err(|err| { - ToolError::Codex(CodexErr::Io(io::Error::other(format!( - "failed to read effective network proxy config: {err}" - )))) + let launch = network.remote_launch_config().await.map_err(|err| { + ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string()))) })?; - let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config) - .map_err(|err| { - ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string()))) - })?; - let allow_local_binding = proxy_config.allow_local_binding; - ( - env, - Some(codex_network_proxy::ManagedNetworkSandboxContext { - loopback_ports: Vec::new(), - allow_local_binding, - proxy_config: Some(proxy_config), - }), - ) + (env, None, Some(launch)) } Some(network) => { let prepared = network @@ -357,9 +342,9 @@ impl<'a> ToolRuntime for UnifiedExecRunt req.turn_environment.environment_id )))) })?; - (prepared.env, Some(prepared.sandbox_context)) + (prepared.env, Some(prepared.sandbox_context), None) } - None => (env, None), + None => (env, None, None), }; let explicit_env_overrides = req.explicit_env_overrides.clone(); #[cfg(unix)] @@ -496,6 +481,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt options, attempt, managed_network, + network_proxy_launch, /*environment_id*/ Some(&req.turn_environment.environment_id), req.exec_server_env_config.clone(), req.tty, diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index ef7ef8b8e9d..9b52121406f 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -232,7 +232,6 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, - proxy_config: None, }; let command = || SandboxCommand { program: "/bin/bash".into(), diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 40f9bd95543..caec902e7b3 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -192,6 +192,7 @@ fn exec_server_params_for_request( sandbox: request.exec_server_sandbox.clone(), enforce_managed_network: request.exec_server_enforce_managed_network, managed_network: request.exec_server_managed_network.clone(), + network_proxy: request.exec_server_network_proxy.clone(), } } @@ -915,6 +916,7 @@ impl UnifiedExecProcessManager { options: ExecOptions, attempt: &SandboxAttempt<'_>, network: Option<&NetworkProxy>, + network_proxy_launch: Option, environment_id: Option<&str>, exec_server_env_config: Option, tty: bool, @@ -927,6 +929,7 @@ impl UnifiedExecProcessManager { attempt.env_for(command, options, network, environment_id) } .map_err(ToolError::Codex)?; + request.exec_server_network_proxy = network_proxy_launch; request.exec_server_env_config = exec_server_env_config; self.open_session_with_prepared_exec_env( process_id, 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 056d6c4c9cf..92c4c70e21c 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -117,7 +117,6 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, - proxy_config: None, }; let mut request = ExecRequest { command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()], @@ -175,6 +174,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { exec_server_sandbox: None, exec_server_enforce_managed_network: true, exec_server_managed_network: Some(managed_network.clone()), + exec_server_network_proxy: None, }; let params = diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 45834bed3e9..848cedc681a 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -5,6 +5,7 @@ use codex_file_system::FileSystemSandboxContext; pub use codex_file_system::WalkOptions; pub use codex_file_system::WalkOutcome; use codex_network_proxy::ManagedNetworkSandboxContext; +use codex_network_proxy::RemoteNetworkProxyLaunchConfig; use codex_protocol::config_types::ShellEnvironmentPolicyInherit; use codex_shell_command::shell_detect::DetectedShell; use codex_utils_path_uri::PathUri; @@ -144,6 +145,9 @@ pub struct ExecParams { /// continue to fail closed. This preserves compatibility with older clients. #[serde(default)] pub managed_network: Option, + /// Optional instructions for starting an executor-local managed-network proxy. + #[serde(default)] + pub network_proxy: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -574,13 +578,17 @@ mod tests { use super::ShellInfo; use codex_file_system::FileSystemSandboxContext; use codex_network_proxy::ManagedNetworkSandboxContext; + use codex_network_proxy::NetworkProxyAuditMetadata; + use codex_network_proxy::NetworkProxyConfig; + use codex_network_proxy::RemoteNetworkProxyConfig; + use codex_network_proxy::RemoteNetworkProxyLaunchConfig; 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() { + fn exec_params_keeps_proxy_launch_separate_from_sandbox_facts() { let cwd = PathUri::from_host_native_path(std::env::current_dir().expect("current directory")) .expect("cwd URI"); @@ -598,8 +606,18 @@ mod tests { managed_network: Some(ManagedNetworkSandboxContext { loopback_ports: vec![43123, 48081], allow_local_binding: false, - proxy_config: None, }), + network_proxy: Some( + RemoteNetworkProxyLaunchConfig::new( + RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default()) + .expect("supported remote config"), + ) + .with_audit_metadata(NetworkProxyAuditMetadata { + conversation_id: Some("conversation-1".to_string()), + ..NetworkProxyAuditMetadata::default() + }) + .for_execution("remote".to_string(), "execution-1".to_string()), + ), }; let mut serialized = serde_json::to_value(¶ms).expect("serialize exec params"); @@ -610,6 +628,10 @@ mod tests { "allowLocalBinding": false, }) ); + assert_eq!( + serialized["networkProxy"]["auditMetadata"]["conversationId"], + "conversation-1" + ); let round_trip: ExecParams = serde_json::from_value(serialized.clone()).expect("deserialize exec params"); assert_eq!(round_trip, params); @@ -618,10 +640,15 @@ mod tests { .as_object_mut() .expect("exec params object") .remove("managedNetwork"); + serialized + .as_object_mut() + .expect("exec params object") + .remove("networkProxy"); let legacy: ExecParams = serde_json::from_value(serialized).expect("deserialize legacy exec params"); assert!(legacy.enforce_managed_network); assert_eq!(legacy.managed_network, None); + assert_eq!(legacy.network_proxy, None); } #[test] diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 5590fd3b5eb..2e9e4aeb06b 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -1387,6 +1387,7 @@ mod tests { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .instrument(parent_span) .await diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 66f73811031..f0e4e0cd1f4 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1176,6 +1176,7 @@ mod tests { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await .expect("start process"); @@ -1216,6 +1217,7 @@ mod tests { sandbox: Some(sandbox), enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await; let Err(err) = result else { diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index fdecc37f538..39cfbf4d316 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -1010,6 +1010,7 @@ mod tests { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, } } diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index feced151856..5b1798aebc6 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -7,6 +7,7 @@ use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyHandle; use codex_network_proxy::NetworkProxyState; +use codex_network_proxy::RemoteNetworkProxyLaunchConfig; use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxCommand; @@ -38,8 +39,12 @@ pub(crate) async fn prepare_exec_request( env: HashMap, runtime_paths: Option<&ExecServerRuntimePaths>, ) -> Result { - let (env, managed_network, network_proxy_handle) = - prepare_managed_network(params.managed_network.as_ref(), env).await?; + let (env, managed_network, network_proxy_handle) = prepare_managed_network( + params.managed_network.as_ref(), + params.network_proxy.as_ref(), + env, + ) + .await?; let Some(sandbox_context) = params.sandbox.as_ref() else { return Ok(PreparedExecRequest { command: params.argv.clone(), @@ -155,6 +160,7 @@ pub(crate) async fn prepare_exec_request( async fn prepare_managed_network( managed_network: Option<&ManagedNetworkSandboxContext>, + network_proxy: Option<&RemoteNetworkProxyLaunchConfig>, env: HashMap, ) -> Result< ( @@ -164,11 +170,10 @@ async fn prepare_managed_network( ), JSONRPCErrorError, > { - let Some(proxy_config) = managed_network.and_then(|context| context.proxy_config.clone()) - else { + let Some(network_proxy) = network_proxy.cloned() else { return Ok((env, managed_network.cloned(), None)); }; - let state = NetworkProxyState::from_static_config(proxy_config.into_network_proxy_config()) + let state = NetworkProxyState::from_remote_launch_config(network_proxy) .map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?; let proxy = NetworkProxy::builder() .state(Arc::new(state)) diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index 2f49fd93f08..81e2add859f 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -5,6 +5,7 @@ use std::time::Duration; use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::RemoteNetworkProxyConfig; +use codex_network_proxy::RemoteNetworkProxyLaunchConfig; #[cfg(unix)] use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; @@ -53,6 +54,7 @@ async fn sandbox_request_wraps_native_argv_on_executor() { sandbox: Some(sandbox), enforce_managed_network: false, managed_network: None, + network_proxy: None, }; let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) @@ -117,8 +119,8 @@ async fn sandbox_request_allows_prepared_managed_proxy_port() { managed_network: Some(ManagedNetworkSandboxContext { loopback_ports: vec![43123], allow_local_binding: false, - proxy_config: None, }), + network_proxy: None, }; let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths)) @@ -153,6 +155,7 @@ async fn native_request_preserves_native_launch_fields() { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }; let prepared = prepare_exec_request(¶ms, env.clone(), /*runtime_paths*/ None) @@ -189,11 +192,11 @@ async fn remote_proxy_config_starts_executor_local_proxy() { arg0: None, sandbox: None, enforce_managed_network: false, - managed_network: Some(ManagedNetworkSandboxContext { - loopback_ports: Vec::new(), - allow_local_binding: false, - proxy_config: Some(proxy_config), - }), + managed_network: None, + network_proxy: Some( + RemoteNetworkProxyLaunchConfig::new(proxy_config) + .for_execution("remote".to_string(), "execution-1".to_string()), + ), }; let stale_proxy = "http://127.0.0.1:9".to_string(); let env = HashMap::from([("HTTP_PROXY".to_string(), stale_proxy.clone())]); diff --git a/codex-rs/exec-server/src/server/handler/tests.rs b/codex-rs/exec-server/src/server/handler/tests.rs index 47416039c6b..b70b7ffc269 100644 --- a/codex-rs/exec-server/src/server/handler/tests.rs +++ b/codex-rs/exec-server/src/server/handler/tests.rs @@ -37,6 +37,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec) -> ExecParams { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, } } diff --git a/codex-rs/exec-server/src/server/processor.rs b/codex-rs/exec-server/src/server/processor.rs index 63f6bd45d9a..2944c780418 100644 --- a/codex-rs/exec-server/src/server/processor.rs +++ b/codex-rs/exec-server/src/server/processor.rs @@ -594,6 +594,7 @@ mod tests { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, } } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index 73377da60b1..8e8bd56a88a 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -84,6 +84,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), "proc-1"); @@ -229,6 +230,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -263,6 +265,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -313,6 +316,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -364,6 +368,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close( sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -440,6 +445,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -480,6 +486,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -516,6 +523,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool) sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -553,6 +561,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -609,6 +618,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; @@ -652,6 +662,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe( sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; @@ -713,6 +724,7 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()> sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; diff --git a/codex-rs/exec-server/tests/relay.rs b/codex-rs/exec-server/tests/relay.rs index 918cba7393b..55fa44f6577 100644 --- a/codex-rs/exec-server/tests/relay.rs +++ b/codex-rs/exec-server/tests/relay.rs @@ -153,6 +153,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await?; assert_eq!( diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 6ee14f33c9a..fee3364c192 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -69,6 +69,7 @@ pub use proxy::PreparedManagedNetwork; pub use proxy::has_proxy_url_env_vars; pub use proxy::proxy_url_env_value; pub use remote_config::RemoteNetworkProxyConfig; +pub use remote_config::RemoteNetworkProxyLaunchConfig; pub use runtime::BlockedRequest; pub use runtime::BlockedRequestArgs; pub use runtime::BlockedRequestObserver; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 09784184591..862f5d8750c 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -348,9 +348,6 @@ pub struct ManagedNetworkSandboxContext { /// Whether the command may bind local sockets and exchange loopback traffic. #[serde(default)] pub allow_local_binding: bool, - /// Effective proxy settings for an exec-server that must start its own local listeners. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub proxy_config: Option, } /// Environment-specific managed-network settings prepared for one command launch. @@ -660,6 +657,29 @@ impl NetworkProxy { self.state.current_cfg().await } + /// Captures the static inputs needed to launch a matching executor-local proxy. + pub async fn remote_launch_config(&self) -> Result { + let proxy = crate::RemoteNetworkProxyConfig::from_effective_config( + &self.state.current_cfg().await?, + )?; + let (environment_id, execution_id) = self + .execution_scope + .as_ref() + .map(|scope| { + ( + Some(scope.environment_id.clone()), + Some(scope.execution_id.clone()), + ) + }) + .unwrap_or_default(); + Ok(crate::RemoteNetworkProxyLaunchConfig { + proxy, + audit_metadata: self.state.audit_metadata().clone(), + environment_id, + execution_id, + }) + } + pub async fn add_allowed_domain(&self, host: &str) -> Result<()> { self.state.add_allowed_domain(host).await } @@ -730,7 +750,6 @@ impl NetworkProxy { sandbox_context: ManagedNetworkSandboxContext { loopback_ports, allow_local_binding: runtime_settings.allow_local_binding, - proxy_config: None, }, } } @@ -1220,7 +1239,6 @@ mod tests { ManagedNetworkSandboxContext { loopback_ports: expected_ports, allow_local_binding: false, - proxy_config: None, } ); } diff --git a/codex-rs/network-proxy/src/proxy/execution_scope.rs b/codex-rs/network-proxy/src/proxy/execution_scope.rs index 37068625d49..621b047553f 100644 --- a/codex-rs/network-proxy/src/proxy/execution_scope.rs +++ b/codex-rs/network-proxy/src/proxy/execution_scope.rs @@ -2,6 +2,7 @@ use super::*; pub(super) struct ExecutionScope { pub(super) environment_id: String, + pub(super) execution_id: String, pub(super) attribution_token: String, state: Arc, } @@ -30,6 +31,7 @@ impl NetworkProxy { let mut proxy = self.clone(); proxy.execution_scope = Some(Arc::new(ExecutionScope { environment_id: environment_id.to_string(), + execution_id: execution_id.to_string(), attribution_token, state: Arc::clone(&self.state), })); diff --git a/codex-rs/network-proxy/src/remote_config.rs b/codex-rs/network-proxy/src/remote_config.rs index d076c308374..9e5fc349dc7 100644 --- a/codex-rs/network-proxy/src/remote_config.rs +++ b/codex-rs/network-proxy/src/remote_config.rs @@ -5,9 +5,49 @@ use serde::Serialize; use crate::NetworkDomainPermissions; use crate::NetworkMode; +use crate::NetworkProxyAuditMetadata; use crate::NetworkProxyConfig; use crate::NetworkUnixSocketPermissions; +/// Executor-local proxy launch inputs transported with one process start. +/// +/// Unlike [`crate::ManagedNetworkSandboxContext`], this describes how the executor should create +/// proxy listeners. The sandbox context is materialized only after those listeners are running. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct RemoteNetworkProxyLaunchConfig { + pub proxy: RemoteNetworkProxyConfig, + #[serde(default)] + pub audit_metadata: NetworkProxyAuditMetadata, + #[serde(default)] + pub environment_id: Option, + #[serde(default)] + pub execution_id: Option, +} + +impl RemoteNetworkProxyLaunchConfig { + pub fn new(proxy: RemoteNetworkProxyConfig) -> Self { + Self { + proxy, + audit_metadata: NetworkProxyAuditMetadata::default(), + environment_id: None, + execution_id: None, + } + } + + pub fn with_audit_metadata(mut self, audit_metadata: NetworkProxyAuditMetadata) -> Self { + self.audit_metadata = audit_metadata; + self + } + + pub fn for_execution(mut self, environment_id: String, execution_id: String) -> Self { + self.environment_id = Some(environment_id); + self.execution_id = Some(execution_id); + self + } +} + /// Effective network proxy settings that are safe to send to a remote executor. /// /// Listener addresses are deliberately omitted because the executor chooses its own loopback diff --git a/codex-rs/network-proxy/src/remote_config_tests.rs b/codex-rs/network-proxy/src/remote_config_tests.rs index c584592170e..32558fdbd25 100644 --- a/codex-rs/network-proxy/src/remote_config_tests.rs +++ b/codex-rs/network-proxy/src/remote_config_tests.rs @@ -1,8 +1,11 @@ use pretty_assertions::assert_eq; use super::RemoteNetworkProxyConfig; +use super::RemoteNetworkProxyLaunchConfig; use crate::NetworkMode; +use crate::NetworkProxyAuditMetadata; use crate::NetworkProxyConfig; +use crate::NetworkProxyState; #[test] fn round_trip_preserves_supported_effective_settings() { @@ -44,3 +47,27 @@ fn rejects_mitm_configuration() { "remote exec-server network proxy does not support MITM, credential injection, or MITM hooks" ); } + +#[test] +fn launch_config_materializes_audit_and_execution_attribution() { + let proxy = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default()) + .expect("supported remote config"); + let audit_metadata = NetworkProxyAuditMetadata { + conversation_id: Some("conversation-1".to_string()), + user_account_id: Some("account-1".to_string()), + originator: Some("codex_cli_rs".to_string()), + model: Some("model-1".to_string()), + ..NetworkProxyAuditMetadata::default() + }; + let state = NetworkProxyState::from_remote_launch_config(RemoteNetworkProxyLaunchConfig { + proxy, + audit_metadata: audit_metadata.clone(), + environment_id: Some("remote".to_string()), + execution_id: Some("execution-1".to_string()), + }) + .expect("remote launch state"); + + assert_eq!(state.audit_metadata(), &audit_metadata); + assert_eq!(state.environment_id(), Some("remote")); + assert_eq!(state.execution_id().as_deref(), Some("execution-1")); +} diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 0afb54c940b..1ba777de336 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -23,6 +23,7 @@ use anyhow::Context; use anyhow::Result; use codex_utils_absolute_path::AbsolutePathBuf; use globset::GlobSet; +use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::collections::HashSet; @@ -47,7 +48,8 @@ const MAX_BLOCKED_EVENTS: usize = 200; const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2); const NETWORK_POLICY_VIOLATION_PREFIX: &str = "CODEX_NETWORK_POLICY_VIOLATION"; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] pub struct NetworkProxyAuditMetadata { pub conversation_id: Option, pub app_version: Option, @@ -286,6 +288,29 @@ impl NetworkProxyState { )) } + /// Builds immutable runtime state for one executor-local proxy launch. + pub fn from_remote_launch_config( + launch: crate::RemoteNetworkProxyLaunchConfig, + ) -> Result { + let crate::RemoteNetworkProxyLaunchConfig { + proxy, + audit_metadata, + environment_id, + execution_id, + } = launch; + let config = proxy.into_network_proxy_config(); + let state = build_config_state(config, NetworkProxyConstraints::default())?; + Ok(Self { + environment_id: environment_id.map(Into::into), + execution_id: execution_id.map(Into::into), + ..Self::with_reloader_and_audit_metadata( + state.clone(), + Arc::new(StaticConfigReloader { state }), + audit_metadata, + ) + }) + } + pub fn with_reloader(state: ConfigState, reloader: Arc) -> Self { Self::with_reloader_and_audit_metadata( state, diff --git a/codex-rs/rmcp-client/src/stdio_server_launcher.rs b/codex-rs/rmcp-client/src/stdio_server_launcher.rs index 7d03e1437e0..70f97bb18fc 100644 --- a/codex-rs/rmcp-client/src/stdio_server_launcher.rs +++ b/codex-rs/rmcp-client/src/stdio_server_launcher.rs @@ -510,6 +510,7 @@ impl ExecutorStdioServerLauncher { sandbox: None, enforce_managed_network: false, managed_network: None, + network_proxy: None, }) .await .map_err(io::Error::other)?; diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 5e18ea95cd6..b15019a8cb7 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -269,7 +269,6 @@ fn prepared_managed_network_context_allows_only_its_proxy_ports() { let managed_network = ManagedNetworkSandboxContext { loopback_ports: vec![43123, 48081], allow_local_binding: false, - proxy_config: None, }; let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command: vec!["/bin/true".to_string()], From 75b5e6e81a43055f927f1f46e3fa9500442030c7 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 9 Jul 2026 10:38:41 -0700 Subject: [PATCH 4/4] fix(exec-server): gate macOS-only test import Co-authored-by: Codex noreply@openai.com --- codex-rs/exec-server/src/process_sandbox_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index 81e2add859f..c12c728d1da 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::time::Duration; +#[cfg(target_os = "macos")] use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::RemoteNetworkProxyConfig;