From 6d48c777bc931ec68c9f74cbf2ce37dab7438229 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Thu, 9 Jul 2026 10:04:13 -0700 Subject: [PATCH] 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 015b20715a..47dd317de1 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 1c570e1bbb..9a59477709 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 a15c53e16b..ba40893bcc 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 10cb460205..42ffd3e9aa 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 c05f9b4fd3..5c924e2ba8 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 ef7ef8b8e9..9b52121406 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 40f9bd9554..caec902e7b 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 056d6c4c9c..92c4c70e21 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 45834bed3e..848cedc681 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 5590fd3b5e..2e9e4aeb06 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 66f7381103..f0e4e0cd1f 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 fdecc37f53..39cfbf4d31 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 feced15185..5b1798aebc 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 2f49fd93f0..81e2add859 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 47416039c6..b70b7ffc26 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 63f6bd45d9..2944c78041 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 73377da60b..8e8bd56a88 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 918cba7393..55fa44f657 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 6ee14f33c9..fee3364c19 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 0978418459..862f5d8750 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 37068625d4..621b047553 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 d076c30837..9e5fc349dc 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 c584592170..32558fdbd2 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 0afb54c940..1ba777de33 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 7d03e1437e..70f97bb18f 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 5e18ea95cd..b15019a8cb 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()],