From 55b9447a17a04b250dd48a5943d18f926401762d Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 7 Jul 2026 11:24:22 -0700 Subject: [PATCH] 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 a1b9b33c6d..c05f9b4fd3 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 571c5ce6df..793ad35a59 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 e52186f772..ef7ef8b8e9 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 fe3ec45759..40f9bd9554 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 20a930b552..056d6c4c9c 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 a265aaed0c..45834bed3e 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 3c488f0ad7..fdecc37f53 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 b5dd54e0b2..feced15185 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 bc71b57320..2f49fd93f0 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 6bf6d8eb38..6ee14f33c9 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 476c68aca8..0978418459 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 0000000000..fbb43880f6 --- /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 0000000000..c584592170 --- /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 a51f1d8f16..0afb54c940 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 b15019a8cb..5e18ea95cd 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()],