diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 27f4ee594659..5ea83db920f3 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -14,11 +14,12 @@ use codex_install_context::InstallContext; #[cfg(target_os = "macos")] use codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; +use codex_network_proxy::MITM_CA_ENV_ACTIVE_ENV_KEY; use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; use codex_network_proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; -use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path; +use codex_network_proxy::SSL_CERT_DIR_ENV_KEY; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::AdditionalPermissionProfile; use codex_sandboxing::SandboxCommand; @@ -68,25 +69,7 @@ pub(crate) fn exec_env_for_sandbox_permissions( } pub(crate) fn strip_managed_proxy_env(env: &mut HashMap) { - for key in PROXY_ENV_KEYS { - env.remove(*key); - } - for key in CUSTOM_CA_ENV_KEYS { - if env - .get(key) - .is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value)) - { - env.remove(key); - } - } - // Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy. - #[cfg(target_os = "macos")] - if env - .get(PROXY_GIT_SSH_COMMAND_ENV_KEY) - .is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)) - { - env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY); - } + codex_network_proxy::strip_managed_proxy_env(env); } /// Prepends `path_entry` to `PATH`, removing duplicate and empty existing @@ -336,10 +319,17 @@ fn build_proxy_env_exports() -> (String, String) { let (captures, restores) = build_override_exports_for_keys("__CODEX_SNAPSHOT_PROXY_OVERRIDE", &keys); let key = PROXY_ACTIVE_ENV_KEY; + let (ssl_cert_dir_captures, ssl_cert_dir_restores) = build_override_exports_for_keys( + "__CODEX_SNAPSHOT_MITM_CA_OVERRIDE", + &[SSL_CERT_DIR_ENV_KEY], + ); + let mitm_ca_key = MITM_CA_ENV_ACTIVE_ENV_KEY; let proxy_blocks = ( - format!("{captures}\n__CODEX_SNAPSHOT_PROXY_ENV_SET=\"${{{key}+x}}\""), format!( - "if [ -n \"$__CODEX_SNAPSHOT_PROXY_ENV_SET\" ] || [ -n \"${{{key}+x}}\" ]; then\n{restores}\nfi" + "{captures}\n__CODEX_SNAPSHOT_PROXY_ENV_SET=\"${{{key}+x}}\"\n{ssl_cert_dir_captures}\n__CODEX_SNAPSHOT_MITM_CA_ENV_SET=\"${{{mitm_ca_key}+x}}\"" + ), + format!( + "__CODEX_SNAPSHOT_MITM_CA_ENV_AFTER_SET=\"${{{mitm_ca_key}+x}}\"\nif [ -n \"$__CODEX_SNAPSHOT_PROXY_ENV_SET\" ] || [ -n \"${{{key}+x}}\" ]; then\n{restores}\nfi\nif [ -n \"$__CODEX_SNAPSHOT_MITM_CA_ENV_SET\" ] || [ -n \"$__CODEX_SNAPSHOT_MITM_CA_ENV_AFTER_SET\" ]; then\n{ssl_cert_dir_restores}\nfi" ), ); let git_blocks = build_codex_proxy_git_ssh_command_exports(); diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 542589ad89c2..c6ebfe7567e0 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -11,6 +11,7 @@ use codex_network_proxy::CUSTOM_CA_ENV_KEYS; use codex_network_proxy::ConfigReloader; use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; +use codex_network_proxy::MITM_CA_ENV_ACTIVE_ENV_KEY; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::NetworkProxyConstraints; @@ -19,6 +20,7 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; use codex_network_proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; +use codex_network_proxy::SSL_CERT_DIR_ENV_KEY; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxManager; @@ -580,6 +582,61 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() { ); } +#[test] +fn maybe_wrap_shell_lc_with_snapshot_restores_ssl_cert_dir_for_mitm_state() { + let cases = [ + (false, Some("1"), "unset"), + (false, None, "/tmp/snapshot-certs"), + (true, None, "unset"), + ]; + + for (index, (snapshot_mitm, live_mitm, expected)) in cases.into_iter().enumerate() { + let dir = tempdir().expect("create temp dir"); + let snapshot_path = dir.path().join(format!("snapshot-{index}.sh")); + let snapshot_mitm = if snapshot_mitm { + format!("export {MITM_CA_ENV_ACTIVE_ENV_KEY}=1\n") + } else { + Default::default() + }; + std::fs::write( + &snapshot_path, + format!( + "# Snapshot file\n{snapshot_mitm}export {SSL_CERT_DIR_ENV_KEY}='/tmp/snapshot-certs'\n" + ), + ) + .expect("write snapshot"); + let (session_shell, shell_snapshot) = + shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs()); + let command = vec![ + "/bin/bash".to_string(), + "-lc".to_string(), + format!("printf '%s' \"${{{SSL_CERT_DIR_ENV_KEY}-unset}}\""), + ]; + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + Some(&shell_snapshot), + &HashMap::new(), + &HashMap::new(), + &RuntimePathPrepends::default(), + ); + let mut command = Command::new(&rewritten[0]); + command + .args(&rewritten[1..]) + .env(PROXY_ACTIVE_ENV_KEY, "1") + .env_remove(SSL_CERT_DIR_ENV_KEY); + if let Some(live_mitm) = live_mitm { + command.env(MITM_CA_ENV_ACTIVE_ENV_KEY, live_mitm); + } else { + command.env_remove(MITM_CA_ENV_ACTIVE_ENV_KEY); + } + let output = command.output().expect("run rewritten command"); + + assert!(output.status.success(), "command failed: {output:?}"); + assert_eq!(String::from_utf8_lossy(&output.stdout), expected); + } +} + #[cfg(target_os = "macos")] #[test] fn maybe_wrap_shell_lc_with_snapshot_refreshes_codex_proxy_git_ssh_command() { diff --git a/codex-rs/network-proxy/src/child_env.rs b/codex-rs/network-proxy/src/child_env.rs new file mode 100644 index 000000000000..dc3663e1b2b5 --- /dev/null +++ b/codex-rs/network-proxy/src/child_env.rs @@ -0,0 +1,142 @@ +use super::NetworkProxyRuntimeSettings; +use super::STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY; +use super::apply_proxy_env_overrides; +use super::ca_env_keys; +use super::is_tracked_startup_ca_env_key; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::Path; +use tracing::warn; + +/// Immutable proxy settings used to prepare one child process environment. +/// +/// Keeping the managed CA path and environment rewrite on the same snapshot +/// prevents a live proxy configuration reload from changing the MITM state in +/// the middle of sandbox policy construction. +#[derive(Clone)] +pub struct NetworkProxyChildEnvSnapshot { + http_addr: SocketAddr, + socks_addr: SocketAddr, + socks_enabled: bool, + runtime_settings: NetworkProxyRuntimeSettings, +} + +impl NetworkProxyChildEnvSnapshot { + pub(super) fn new( + http_addr: SocketAddr, + socks_addr: SocketAddr, + socks_enabled: bool, + runtime_settings: NetworkProxyRuntimeSettings, + ) -> Self { + Self { + http_addr, + socks_addr, + socks_enabled, + runtime_settings, + } + } + + pub fn has_managed_mitm_ca(&self) -> bool { + self.runtime_settings.mitm_ca_trust_bundle.is_some() + } + + /// Returns the generated MITM CA bundle path this snapshot will expose. + pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option { + self.runtime_settings + .mitm_ca_trust_bundle + .as_ref() + .and_then(|bundle| { + AbsolutePathBuf::from_absolute_path(&bundle.path) + .map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}")) + .ok() + }) + } + + pub fn apply_to_env(&self, env: &mut HashMap) { + apply_proxy_env_overrides( + env, + self.http_addr, + self.socks_addr, + self.socks_enabled, + self.runtime_settings.allow_local_binding, + self.runtime_settings.mitm_ca_trust_bundle.as_ref(), + ); + } + + /// Prepares a child environment without creating a command-specific CA bundle. + /// + /// Persistent sandbox identities cannot safely receive a read grant for a + /// derived bundle because later commands would retain that grant. Preserve + /// the pre-materialization behavior there and expose only the stable + /// managed baseline path. + pub fn prepare_persistent_sandbox_child_env( + &self, + env: &mut HashMap, + ) -> Vec { + self.apply_to_env(env); + env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY); + self.managed_mitm_ca_trust_bundle_path() + .into_iter() + .collect() + } + + /// Returns whether this child would need a command-specific managed CA bundle. + /// + /// Persistent sandbox identities must reject this shape rather than grant + /// access to a derived bundle that later commands could continue reading. + pub fn requires_child_specific_mitm_ca_bundle(&self, env: &HashMap) -> bool { + let Some(mitm_ca_trust_bundle) = self.runtime_settings.mitm_ca_trust_bundle.as_ref() else { + return false; + }; + if env + .get(crate::certs::SSL_CERT_DIR_ENV_KEY) + .is_some_and(|value| !value.is_empty()) + { + // The stable Windows baseline only embeds file-backed startup CA + // overrides. Directory contents can change after proxy startup, + // so they still require per-child materialization and cannot be + // exposed through a persistent sandbox identity. + return true; + } + + let managed_path = mitm_ca_trust_bundle.path.to_string_lossy(); + crate::certs::CUSTOM_CA_ENV_KEYS.into_iter().any(|key| { + env.get(key) + .filter(|value| !value.is_empty()) + .is_some_and(|value| { + value != managed_path.as_ref() + && mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value) + }) + }) + } + + /// Rewrites readable child-selected CA bundles into immutable managed MITM bundles. + pub fn prepare_child_env( + &self, + env: &mut HashMap, + cwd: &Path, + can_read_path: F, + ) -> Vec + where + F: Fn(&Path) -> bool, + { + self.apply_to_env(env); + let startup_ca_env_keys_present_in_child = ca_env_keys() + .filter(|&key| is_tracked_startup_ca_env_key(env, key)) + .collect::>(); + env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY); + self.runtime_settings + .mitm_ca_trust_bundle + .as_ref() + .map_or_else(Vec::new, |mitm_ca_trust_bundle| { + crate::child_ca::prepare_mitm_ca_trust_bundle_env( + mitm_ca_trust_bundle, + env, + cwd, + &startup_ca_env_keys_present_in_child, + can_read_path, + ) + }) + } +} diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 2aded28034dd..88254ba3d985 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -1,11 +1,11 @@ #![deny(clippy::print_stdout, clippy::print_stderr)] -#![allow(dead_code)] mod certs; mod child_ca; mod config; mod connect_policy; mod http_proxy; +mod managed_env; mod mitm; mod mitm_hook; mod native_certs; @@ -30,6 +30,7 @@ pub use config::NetworkProxyConfig; pub use config::NetworkUnixSocketPermission; pub use config::NetworkUnixSocketPermissions; pub use config::host_and_port_from_network_addr; +pub use managed_env::strip_managed_proxy_env; pub use mitm_hook::InjectedHeaderConfig; pub use mitm_hook::MitmHookActionsConfig; pub use mitm_hook::MitmHookBodyConfig; @@ -54,6 +55,7 @@ pub use proxy::MITM_CA_ENV_ACTIVE_ENV_KEY; pub use proxy::NO_PROXY_ENV_KEYS; pub use proxy::NetworkProxy; pub use proxy::NetworkProxyBuilder; +pub use proxy::NetworkProxyChildEnvSnapshot; pub use proxy::NetworkProxyHandle; pub use proxy::PROXY_ACTIVE_ENV_KEY; pub use proxy::PROXY_ENV_KEYS; diff --git a/codex-rs/network-proxy/src/managed_env.rs b/codex-rs/network-proxy/src/managed_env.rs new file mode 100644 index 000000000000..3441e1a58967 --- /dev/null +++ b/codex-rs/network-proxy/src/managed_env.rs @@ -0,0 +1,36 @@ +#[cfg(target_os = "macos")] +use crate::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; +use crate::CUSTOM_CA_ENV_KEYS; +use crate::PROXY_ENV_KEYS; +#[cfg(target_os = "macos")] +use crate::PROXY_GIT_SSH_COMMAND_ENV_KEY; +use crate::is_managed_mitm_ca_trust_bundle_path; +use std::collections::HashMap; + +/// Removes environment values owned by a managed network proxy before a command escapes its +/// sandbox and proxy containment. +pub fn strip_managed_proxy_env(env: &mut HashMap) { + for key in PROXY_ENV_KEYS { + env.remove(*key); + } + for key in CUSTOM_CA_ENV_KEYS { + if env + .get(key) + .is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value)) + { + env.remove(key); + } + } + // Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy. + #[cfg(target_os = "macos")] + if env + .get(PROXY_GIT_SSH_COMMAND_ENV_KEY) + .is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)) + { + env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY); + } +} + +#[cfg(test)] +#[path = "managed_env_tests.rs"] +mod tests; diff --git a/codex-rs/network-proxy/src/managed_env_tests.rs b/codex-rs/network-proxy/src/managed_env_tests.rs new file mode 100644 index 000000000000..888b79be43a6 --- /dev/null +++ b/codex-rs/network-proxy/src/managed_env_tests.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::PROXY_ACTIVE_ENV_KEY; +use pretty_assertions::assert_eq; + +#[test] +fn strips_managed_proxy_env() { + let mut env = HashMap::from([ + (PROXY_ACTIVE_ENV_KEY.to_string(), "1".to_string()), + ( + "HTTPS_PROXY".to_string(), + "http://127.0.0.1:1234".to_string(), + ), + ("CUSTOM_ENV".to_string(), "kept".to_string()), + ]); + + strip_managed_proxy_env(&mut env); + + assert_eq!( + env, + HashMap::from([("CUSTOM_ENV".to_string(), "kept".to_string())]) + ); +} + +#[test] +fn preserves_unmanaged_ca_env() { + let mut env = HashMap::from([( + "SSL_CERT_FILE".to_string(), + "/tmp/user-ca-bundle.pem".to_string(), + )]); + + strip_managed_proxy_env(&mut env); + + assert_eq!( + env, + HashMap::from([( + "SSL_CERT_FILE".to_string(), + "/tmp/user-ca-bundle.pem".to_string(), + )]) + ); +} diff --git a/codex-rs/network-proxy/src/native_certs.rs b/codex-rs/network-proxy/src/native_certs.rs index 0ab697375cd8..dfc19f488ed8 100644 --- a/codex-rs/network-proxy/src/native_certs.rs +++ b/codex-rs/network-proxy/src/native_certs.rs @@ -7,8 +7,10 @@ use rustls_native_certs::Error; use rustls_native_certs::ErrorKind; // `rustls_native_certs::load_native_certs()` first consults SSL_CERT_FILE and -// SSL_CERT_DIR. The managed baseline must contain platform roots only; child -// CA overrides are copied later after the child filesystem policy can be checked. +// SSL_CERT_DIR. Load platform roots directly so Unix child CA overrides can be +// copied later after the child filesystem policy is checked. Windows preserves +// process-start file overrides separately in its stable managed baseline because +// its persistent sandbox identity cannot safely receive per-command grants. #[cfg(all(unix, not(target_os = "macos")))] pub(crate) fn load_platform_native_certs() -> CertificateResult { let mut result = diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 61b0639cf1c4..209918888622 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -19,6 +19,10 @@ use std::sync::RwLock; use tokio::task::JoinHandle; use tracing::warn; +#[path = "child_env.rs"] +mod child_env; +pub use child_env::NetworkProxyChildEnvSnapshot; + #[derive(Debug, Clone, Parser)] #[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] pub struct Args {} @@ -662,29 +666,24 @@ impl NetworkProxy { self.runtime_settings().dangerously_allow_all_unix_sockets } + /// Captures the proxy settings used to prepare a single child process. + pub fn child_env_snapshot(&self) -> NetworkProxyChildEnvSnapshot { + NetworkProxyChildEnvSnapshot::new( + self.http_addr, + self.socks_addr, + self.socks_enabled, + self.runtime_settings(), + ) + } + /// Returns the generated MITM CA bundle path child sandboxes should expose to TLS clients. pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option { - self.runtime_settings() - .mitm_ca_trust_bundle - .and_then(|bundle| { - AbsolutePathBuf::from_absolute_path(bundle.path) - .map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}")) - .ok() - }) + self.child_env_snapshot() + .managed_mitm_ca_trust_bundle_path() } pub fn apply_to_env(&self, env: &mut HashMap) { - let runtime_settings = self.runtime_settings(); - // Enforce proxying for child processes. Proxy endpoint values are always rewritten; - // managed MITM CA vars preserve command-scoped overrides after proxy startup. - apply_proxy_env_overrides( - env, - self.http_addr, - self.socks_addr, - self.socks_enabled, - runtime_settings.allow_local_binding, - runtime_settings.mitm_ca_trust_bundle.as_ref(), - ); + self.child_env_snapshot().apply_to_env(env); } pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> { @@ -709,7 +708,6 @@ impl NetworkProxy { new_state.config.network.enable_socks5_udp == current_cfg.network.enable_socks5_udp, "cannot update network.enable_socks5_udp on a running proxy" ); - let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config)?; self.state.replace_config_state(new_state).await?; let mut guard = self @@ -866,11 +864,14 @@ impl Drop for NetworkProxyHandle { mod tests { use super::*; use crate::config::NetworkProxySettings; + use crate::state::NetworkProxyConstraints; + use crate::state::build_config_state; use crate::state::network_proxy_state_for_policy; use pretty_assertions::assert_eq; use std::net::IpAddr; use std::net::Ipv4Addr; use std::path::Path; + use tempfile::TempDir; #[tokio::test] async fn managed_proxy_builder_uses_loopback_ports() { @@ -938,6 +939,155 @@ mod tests { ); } + #[tokio::test] + async fn child_env_snapshot_pins_mitm_settings_across_reload() { + let settings = NetworkProxySettings { + proxy_url: "http://127.0.0.1:43128".to_string(), + socks_url: "http://127.0.0.1:48081".to_string(), + ..NetworkProxySettings::default() + }; + let state = Arc::new(network_proxy_state_for_policy(settings)); + let proxy = NetworkProxy::builder() + .state(state) + .managed_by_codex(/*managed_by_codex*/ false) + .build() + .await + .unwrap(); + let without_mitm = proxy.child_env_snapshot(); + + let temp_dir = TempDir::new().expect("create temp dir"); + let managed_bundle_path = temp_dir.path().join("ca-bundle.pem"); + let custom_bundle_path = temp_dir.path().join("custom-ca.pem"); + std::fs::write(&managed_bundle_path, "managed ca\n").expect("write managed bundle"); + std::fs::write(&custom_bundle_path, "custom ca\n").expect("write custom bundle"); + let managed_bundle = crate::certs::ManagedMitmCaTrustBundle { + path: managed_bundle_path.clone(), + startup_env_values: HashMap::new(), + startup_cwd: temp_dir.path().to_path_buf(), + }; + { + let mut guard = proxy + .runtime_settings + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + guard.mitm_ca_trust_bundle = Some(managed_bundle); + } + + let mut env = HashMap::from([( + "REQUESTS_CA_BUNDLE".to_string(), + custom_bundle_path.display().to_string(), + )]); + assert!( + without_mitm + .prepare_child_env(&mut env, temp_dir.path(), |_| true) + .is_empty() + ); + assert_eq!( + env.get("REQUESTS_CA_BUNDLE"), + Some(&custom_bundle_path.display().to_string()) + ); + assert!(!without_mitm.requires_child_specific_mitm_ca_bundle(&env)); + + let with_mitm = proxy.child_env_snapshot(); + let mut persistent_sandbox_env = HashMap::from([( + "REQUESTS_CA_BUNDLE".to_string(), + custom_bundle_path.display().to_string(), + )]); + assert!(with_mitm.requires_child_specific_mitm_ca_bundle(&persistent_sandbox_env)); + assert_eq!( + with_mitm.prepare_persistent_sandbox_child_env(&mut persistent_sandbox_env), + vec![AbsolutePathBuf::from_absolute_path(&managed_bundle_path).unwrap()] + ); + assert_eq!( + persistent_sandbox_env.get("REQUESTS_CA_BUNDLE"), + Some(&custom_bundle_path.display().to_string()) + ); + assert_eq!( + persistent_sandbox_env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY), + None + ); + let managed_path = managed_bundle_path.display().to_string(); + let baseline_env = + HashMap::from([("REQUESTS_CA_BUNDLE".to_string(), managed_path.clone())]); + assert!(!with_mitm.requires_child_specific_mitm_ca_bundle(&baseline_env)); + let startup_env = HashMap::from([ + ("REQUESTS_CA_BUNDLE".to_string(), managed_path), + ( + STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY.to_string(), + "REQUESTS_CA_BUNDLE".to_string(), + ), + ]); + assert!(!with_mitm.requires_child_specific_mitm_ca_bundle(&startup_env)); + let startup_ssl_cert_dir_env = HashMap::from([ + ( + crate::certs::SSL_CERT_DIR_ENV_KEY.to_string(), + temp_dir.path().display().to_string(), + ), + ( + STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY.to_string(), + crate::certs::SSL_CERT_DIR_ENV_KEY.to_string(), + ), + ]); + assert!(with_mitm.requires_child_specific_mitm_ca_bundle(&startup_ssl_cert_dir_env)); + let ssl_cert_dir_env = HashMap::from([( + crate::certs::SSL_CERT_DIR_ENV_KEY.to_string(), + temp_dir.path().display().to_string(), + )]); + assert!(with_mitm.requires_child_specific_mitm_ca_bundle(&ssl_cert_dir_env)); + { + let mut guard = proxy + .runtime_settings + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + guard.mitm_ca_trust_bundle = None; + } + let bundle_paths = with_mitm.prepare_child_env(&mut env, temp_dir.path(), |_| true); + let prepared_bundle_path = Path::new( + env.get("REQUESTS_CA_BUNDLE") + .expect("custom CA env should be rewritten"), + ); + assert!( + bundle_paths + .iter() + .any(|path| path.as_path() == prepared_bundle_path) + ); + assert_ne!(prepared_bundle_path, custom_bundle_path); + let prepared_bundle = + std::fs::read_to_string(prepared_bundle_path).expect("read prepared bundle"); + assert!(prepared_bundle.contains("custom ca")); + assert!(prepared_bundle.contains("managed ca")); + assert!(!proxy.child_env_snapshot().has_managed_mitm_ca()); + } + + #[tokio::test] + async fn running_proxy_updates_mitm_mode_changes() { + let settings = NetworkProxySettings { + proxy_url: "http://127.0.0.1:43128".to_string(), + socks_url: "http://127.0.0.1:48081".to_string(), + ..NetworkProxySettings::default() + }; + let state = Arc::new(network_proxy_state_for_policy(settings)); + let proxy = NetworkProxy::builder() + .state(Arc::clone(&state)) + .managed_by_codex(/*managed_by_codex*/ false) + .build() + .await + .unwrap(); + let mut config = state.current_cfg().await.unwrap(); + config.network.mitm = true; + let new_state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + + proxy.replace_config_state(new_state).await.unwrap(); + assert!(proxy.child_env_snapshot().has_managed_mitm_ca()); + + let mut config = state.current_cfg().await.unwrap(); + config.network.mitm = false; + let new_state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + + proxy.replace_config_state(new_state).await.unwrap(); + assert!(!proxy.child_env_snapshot().has_managed_mitm_ca()); + } + #[tokio::test] async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled() { let settings = NetworkProxySettings {