diff --git a/codex-rs/core/src/config/network_config.rs b/codex-rs/core/src/config/network_config.rs index 4f5797cc96..e703387513 100644 --- a/codex-rs/core/src/config/network_config.rs +++ b/codex-rs/core/src/config/network_config.rs @@ -16,6 +16,7 @@ use codex_features::FeaturesToml; use codex_network_proxy::EnvironmentNetworkPolicy; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::NetworkProxyConstraints; +use codex_network_proxy::NetworkProxyExecutorOs; use codex_network_proxy::build_config_state; use codex_protocol::models::PermissionProfile; @@ -29,8 +30,13 @@ pub struct EnvironmentNetworkConfigError; /// Drops listener addresses, which the executor chooses for each command, before /// validating an environment's selected network configuration. Use the returned /// value when deciding whether a policy is present and when preparing it. +/// +/// Socket policy validation uses the supplied executor OS: Allow keys must be +/// NUL-free and absolute for that OS; Deny keys are preserved unchanged. +/// Path normalization and socket support are determined by the executor. pub fn project_environment_profile_network( network: Option, + executor_os: NetworkProxyExecutorOs, ) -> Result, EnvironmentNetworkConfigError> { let Some(mut network) = network else { return Ok(None); @@ -46,20 +52,16 @@ pub fn project_environment_profile_network( allow_local_binding: network.allow_local_binding, ..Default::default() }; - let invalid_socket = network.unix_sockets.as_ref().is_some_and(|sockets| { - sockets - .entries - .keys() - .any(|path| !path.starts_with('/') || path.contains('\0')) - }); - if network != supported || invalid_socket { + if network != supported { return Err(EnvironmentNetworkConfigError); } - // MITM and credential settings have been excluded. This only validates - // patterns and lexical socket paths; it does not discover executor files. + // Reuse shared, host-independent policy validation. + // With MITM and credentials excluded, this checks patterns and allowed + // socket paths without discovering executor files. build_config_state( network.to_network_proxy_config(), NetworkProxyConstraints::default(), + executor_os, ) .map_err(|_| EnvironmentNetworkConfigError)?; Ok(Some(network)) @@ -67,9 +69,11 @@ pub fn project_environment_profile_network( /// Validates the owner policy for its final permission profile using the native /// environment resolver. Execution also applies controller and saved exec rules. +/// This checks the supplied executor's path syntax; socket support is checked at execution. pub fn validate_environment_network_policy( policy: &EnvironmentNetworkPolicy, permission_profile: &PermissionProfile, + executor_os: NetworkProxyExecutorOs, ) -> Result<(), EnvironmentNetworkConfigError> { NetworkProxySpec::for_environment( /*controller*/ None, @@ -77,7 +81,7 @@ pub fn validate_environment_network_policy( permission_profile, &Policy::empty(), ) - .and_then(|spec| spec.build_config_state_for_spec().map(|_| ())) + .and_then(|spec| spec.build_config_state_for_spec(executor_os).map(|_| ())) .map_err(|_| EnvironmentNetworkConfigError) } diff --git a/codex-rs/core/src/config/network_config_tests.rs b/codex-rs/core/src/config/network_config_tests.rs index b931545918..27bbcbdcfd 100644 --- a/codex-rs/core/src/config/network_config_tests.rs +++ b/codex-rs/core/src/config/network_config_tests.rs @@ -38,10 +38,17 @@ fn environment_policy_presence_keeps_selected_and_managed_denials() { } #[test] -fn attachment_projection_preserves_policy_and_drops_listener_addresses() { - for raw in [ - "", - r#" +fn attachment_projection_uses_executor_os_and_drops_listener_addresses() { + for executor_os in [ + NetworkProxyExecutorOs::Linux, + NetworkProxyExecutorOs::Macos, + NetworkProxyExecutorOs::Windows, + NetworkProxyExecutorOs::Unknown, + ] { + for (raw, windows_only) in [ + ("", false), + ( + r#" enabled = false allow_upstream_proxy = false dangerously_allow_all_unix_sockets = false @@ -52,23 +59,69 @@ allow_local_binding = false [unix_sockets] '/tmp/allowed.sock' = 'allow' '/tmp/blocked.sock' = 'deny' +'relative.sock' = 'deny' +'~/blocked.sock' = 'deny' +'C:\blocked.sock' = 'deny' "#, - ] { - let expected: NetworkToml = toml::from_str(raw).unwrap(); - let network = NetworkToml { - proxy_url: Some("not a listener URL".to_string()), - socks_url: Some("socks5://127.0.0.1:1080".to_string()), - ..expected.clone() - }; + false, + ), + ( + r#"[unix_sockets] +'C:\allowed.sock' = 'allow' +'\\server\share\allowed.sock' = 'allow' +"#, + true, + ), + ] { + let expected: NetworkToml = toml::from_str(raw).unwrap(); + let accepted = !windows_only + || matches!( + executor_os, + NetworkProxyExecutorOs::Windows | NetworkProxyExecutorOs::Unknown + ); + assert_eq!( + build_config_state( + expected.to_network_proxy_config(), + NetworkProxyConstraints::default(), + executor_os, + ) + .is_ok(), + accepted, + "native validation: {executor_os:?}, {raw}" + ); + assert_eq!( + validate_environment_network_policy( + &EnvironmentNetworkPolicy::from_config( + &expected.to_network_proxy_config(), + /*managed_allowed_domains_only*/ false, + ), + &PermissionProfile::read_only(), + executor_os, + ) + .is_ok(), + accepted, + "environment validation: {executor_os:?}, {raw}" + ); + let network = NetworkToml { + proxy_url: Some("not a listener URL".to_string()), + socks_url: Some("socks5://127.0.0.1:1080".to_string()), + ..expected.clone() + }; + assert_eq!( + project_environment_profile_network(Some(network), executor_os), + if accepted { + Ok(Some(expected)) + } else { + Err(EnvironmentNetworkConfigError) + }, + "projection: {executor_os:?}, {raw}" + ); + } assert_eq!( - project_environment_profile_network(Some(network)), - Ok(Some(expected)) + project_environment_profile_network(/*network*/ None, executor_os), + Ok(None) ); } - assert_eq!( - project_environment_profile_network(/*network*/ None), - Ok(None) - ); } #[test] @@ -86,12 +139,13 @@ path_prefixes = ['/'] action = ['redact'] "#, "[unix_sockets]\n'relative.sock' = 'allow'", - "[unix_sockets]\n'relative.sock' = 'deny'", + "[unix_sockets]\n'~/allowed.sock' = 'allow'", + "[unix_sockets]\n\"/tmp/\\u0000allowed.sock\" = 'allow'", "[domains]\n'[' = 'allow'", ] { let network: NetworkToml = toml::from_str(raw).unwrap(); assert_eq!( - project_environment_profile_network(Some(network)), + project_environment_profile_network(Some(network), NetworkProxyExecutorOs::Unknown), Err(EnvironmentNetworkConfigError), "{raw}" ); diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index a195ff5f44..8b84b1f9cf 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -12,6 +12,7 @@ use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyAuditMetadata; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::NetworkProxyConstraints; +use codex_network_proxy::NetworkProxyExecutorOs; use codex_network_proxy::NetworkProxyHandle; use codex_network_proxy::NetworkProxyState; use codex_network_proxy::build_config_state; @@ -196,7 +197,10 @@ impl NetworkProxySpec { enable_network_approval_flow: bool, audit_metadata: NetworkProxyAuditMetadata, ) -> std::io::Result { - let state = self.build_state_with_audit_metadata(audit_metadata)?; + let state = self.build_state_with_audit_metadata( + audit_metadata, + NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; let mut builder = NetworkProxy::builder() .state(Arc::new(state)) .managed_proxy_routing(managed_proxy_routing); @@ -337,7 +341,9 @@ impl NetworkProxySpec { &self, started_proxy: &StartedNetworkProxy, ) -> std::io::Result<()> { - let state = self.build_config_state_for_spec()?; + let state = self.build_config_state_for_spec(NetworkProxyExecutorOs::from_platform_os( + Some(std::env::consts::OS), + ))?; started_proxy .proxy() .replace_config_state(state) @@ -350,8 +356,9 @@ impl NetworkProxySpec { pub(crate) fn build_state_with_audit_metadata( &self, audit_metadata: NetworkProxyAuditMetadata, + executor_os: NetworkProxyExecutorOs, ) -> std::io::Result { - let state = self.build_config_state_for_spec()?; + let state = self.build_config_state_for_spec(executor_os)?; let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone())); Ok(NetworkProxyState::with_reloader_and_audit_metadata( state, @@ -360,10 +367,13 @@ impl NetworkProxySpec { )) } - pub(super) fn build_config_state_for_spec(&self) -> std::io::Result { - build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| { - std::io::Error::other(format!("failed to build network proxy state: {err}")) - }) + pub(super) fn build_config_state_for_spec( + &self, + executor_os: NetworkProxyExecutorOs, + ) -> std::io::Result { + build_config_state(self.config.clone(), self.constraints.clone(), executor_os).map_err( + |err| std::io::Error::other(format!("failed to build network proxy state: {err}")), + ) } fn apply_requirements( diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index b4a2f55be2..158711f8ea 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -84,7 +84,11 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun Some(expected), "{name}" ); - let state = Arc::new(controller.build_state_with_audit_metadata(Default::default())?); + let state = + Arc::new(controller.build_state_with_audit_metadata( + Default::default(), + NetworkProxyExecutorOs::Linux, + )?); let proxy = NetworkProxy::builder() .state(Arc::clone(&state)) .managed_by_codex(false) @@ -138,7 +142,9 @@ async fn attachment_socket_grants_respect_configured_restrictions_at_remote_laun &profile, )?; proxy - .replace_config_state(restricted.build_config_state_for_spec()?) + .replace_config_state( + restricted.build_config_state_for_spec(NetworkProxyExecutorOs::Linux)?, + ) .await?; assert!( !scoped @@ -169,7 +175,7 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() { }; let state = spec - .build_state_with_audit_metadata(metadata.clone()) + .build_state_with_audit_metadata(metadata.clone(), NetworkProxyExecutorOs::Linux) .expect("state should build"); assert_eq!(state.audit_metadata(), &metadata); } @@ -252,11 +258,15 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials() let owner_policy = EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false); assert_eq!( - validate_environment_network_policy(&owner_policy, &profile), + validate_environment_network_policy(&owner_policy, &profile, NetworkProxyExecutorOs::Linux), Ok(()) ); assert_eq!( - validate_environment_network_policy(&owner_policy, &PermissionProfile::Disabled), + validate_environment_network_policy( + &owner_policy, + &PermissionProfile::Disabled, + NetworkProxyExecutorOs::Linux + ), Err(EnvironmentNetworkConfigError) ); let compose = NetworkProxySpec::for_environment; @@ -324,14 +334,22 @@ fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials() EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false); assert!(compose(Some(&spec), &wildcard_policy, &profile, &empty).is_err()); assert_eq!( - validate_environment_network_policy(&wildcard_policy, &profile), + validate_environment_network_policy( + &wildcard_policy, + &profile, + NetworkProxyExecutorOs::Linux + ), Err(EnvironmentNetworkConfigError) ); owner.set_allowed_domains(vec!["[".to_string()]); let malformed_policy = EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false); assert_eq!( - validate_environment_network_policy(&malformed_policy, &profile), + validate_environment_network_policy( + &malformed_policy, + &profile, + NetworkProxyExecutorOs::Linux + ), Err(EnvironmentNetworkConfigError) ); } diff --git a/codex-rs/core/src/shell_snapshot_tests.rs b/codex-rs/core/src/shell_snapshot_tests.rs index 70987fa3e4..0b519a545f 100644 --- a/codex-rs/core/src/shell_snapshot_tests.rs +++ b/codex-rs/core/src/shell_snapshot_tests.rs @@ -521,6 +521,9 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() - .replace_config_state(codex_network_proxy::build_config_state( config, codex_network_proxy::NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some( + std::env::consts::OS, + )), )?) .await?; assert_eq!( @@ -569,6 +572,9 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() - .replace_config_state(codex_network_proxy::build_config_state( config, codex_network_proxy::NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some( + std::env::consts::OS, + )), )?) .await?; fs::write(dir.path().join("finish-startup"), "").await?; diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 9ea9089e98..5cc87a77d1 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -18,6 +18,7 @@ use codex_network_proxy::NetworkPolicyDecider; use codex_network_proxy::NetworkPolicyRequest; use codex_network_proxy::NetworkProtocol; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyExecutorOs; use codex_network_proxy::NetworkRequestCancellation; use codex_network_proxy::NetworkRequestCancellationReason; use codex_network_proxy::NetworkRequestDisconnect; @@ -1151,9 +1152,16 @@ pub(crate) async fn begin_network_approval( })? } else { // This carrier never listens: the executor starts the real per-command proxy. + let executor_os = NetworkProxyExecutorOs::from_platform_os( + environments + .turn_environments() + .find(|environment| environment.selection.environment_id == environment_id) + .and_then(|environment| environment.executor_platform_os.as_deref()), + ); let state = owner_spec .build_state_with_audit_metadata( session.services.network_proxy_audit_metadata.clone(), + executor_os, ) .map_err(|error| { ToolError::Rejected(format!( diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index f5b25eda62..46bfff2f00 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -76,8 +76,11 @@ pub(super) async fn test_credential_broker_network_proxy() -> anyhow::Result anyhow::Result { - let state = - codex_network_proxy::build_config_state(config, NetworkProxyConstraints::default())?; + let state = codex_network_proxy::build_config_state( + config, + NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; NetworkProxy::builder() .state(Arc::new(NetworkProxyState::with_reloader( state, diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index be4606883c..82689ea3cc 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -1819,6 +1819,9 @@ mod tests { .expect("build remote network proxy config"); let state = NetworkProxyState::from_remote_launch_config( RemoteNetworkProxyLaunchConfig::new(proxy_config), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some( + std::env::consts::OS, + )), ) .expect("build network proxy state"); let proxy = NetworkProxy::builder() diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index 5bc2b5d9fc..268fc6e2ca 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -348,8 +348,11 @@ async fn prepare_managed_network( let Some(network_proxy) = network_proxy.cloned() else { return Ok((env, managed_network.cloned(), None, None)); }; - let mut state = NetworkProxyState::from_remote_launch_config(network_proxy) - .map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?; + let mut state = NetworkProxyState::from_remote_launch_config( + network_proxy, + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?; if let Some(observer) = network_policy_audit_observer { state.set_policy_audit_observer(observer); } diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy_unix_sockets_tests.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy_unix_sockets_tests.rs index f8f26ca3c7..479bb9ef71 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy_unix_sockets_tests.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy_unix_sockets_tests.rs @@ -140,8 +140,14 @@ for family in (socket.AF_NETLINK, getattr(socket, 'AF_VSOCK', 40)): if allow_path { config.set_allow_unix_sockets(vec![socket_path.to_string_lossy().into_owned()]); } - let state = build_config_state(config, NetworkProxyConstraints::default()) - .expect("valid managed network configuration"); + let state = build_config_state( + config, + NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some( + std::env::consts::OS, + )), + ) + .expect("valid managed network configuration"); let network = NetworkProxy::builder() .state(Arc::new(NetworkProxyState::with_reloader( state.clone(), diff --git a/codex-rs/network-proxy/src/brokered_tunnel_tests.rs b/codex-rs/network-proxy/src/brokered_tunnel_tests.rs index 72affafb9a..c65a0335e5 100644 --- a/codex-rs/network-proxy/src/brokered_tunnel_tests.rs +++ b/codex-rs/network-proxy/src/brokered_tunnel_tests.rs @@ -172,7 +172,12 @@ impl TestProxy { ..NetworkProxyConfig::default() }; config.set_allowed_domains(vec!["127.0.0.1".to_string()]); - let config_state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let config_state = build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(); let state = Arc::new(NetworkProxyState::with_reloader( config_state.clone(), Arc::new(StaticReloader(config_state)), diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 587318a845..8bdd53ecf5 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -14,6 +14,7 @@ use std::path::Path; use tracing::warn; use url::Url; +use crate::NetworkProxyExecutorOs; use crate::mitm_hook::MitmHookConfig; use crate::policy::normalize_host; @@ -106,6 +107,9 @@ pub enum NetworkUnixSocketPermission { Deny, } +/// Socket policy keys are preserved across controller and executor hosts. +/// Allow entries must be NUL-free and absolute according to the executor OS; +/// Deny entries are retained without path validation or expansion. #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct NetworkUnixSocketPermissions { #[serde(flatten)] @@ -487,21 +491,28 @@ impl ValidatedUnixSocketPath { if let Some(path) = UnixStyleAbsolutePath::parse(socket_path) { return Ok(Self::UnixStyleAbsolute(path)); } - bail!("expected an absolute path, got {socket_path:?}"); } } -pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> { +pub(crate) fn validate_unix_socket_allowlist_paths( + cfg: &NetworkProxyConfig, + executor_os: NetworkProxyExecutorOs, +) -> Result<()> { for (index, socket_path) in cfg.allow_unix_sockets().iter().enumerate() { - ValidatedUnixSocketPath::parse(socket_path) - .with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?; + anyhow::ensure!( + executor_os.socket_path_is_absolute(socket_path) && !socket_path.contains('\0'), + "invalid network.allow_unix_sockets[{index}]: expected a NUL-free absolute path for {executor_os:?}, got {socket_path:?}" + ); } Ok(()) } -pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result { - validate_unix_socket_allowlist_paths(cfg)?; +pub fn resolve_runtime( + cfg: &NetworkProxyConfig, + executor_os: NetworkProxyExecutorOs, +) -> Result { + validate_unix_socket_allowlist_paths(cfg, executor_os)?; let http_addr = resolve_addr(&cfg.proxy_url, /*default_port*/ 3128) .with_context(|| format!("invalid network.proxy_url: {}", cfg.proxy_url))?; @@ -517,7 +528,10 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result { /// Returns the sorted loopback ports used by the configured managed proxy listeners. pub fn managed_proxy_ports(cfg: &NetworkProxyConfig) -> Result> { - let runtime = resolve_runtime(cfg)?; + let runtime = resolve_runtime( + cfg, + NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; if runtime.http_addr.port() == 0 { bail!("network.proxy_url must use a fixed non-zero port for managed proxy provisioning"); } @@ -1085,29 +1099,77 @@ mod tests { } #[test] - fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() { - let cfg = settings_with_unix_sockets(&["relative.sock"]); - - let err = match resolve_runtime(&cfg) { - Ok(runtime) => panic!( - "relative allow_unix_sockets should fail, but resolve_runtime succeeded: {:?}", - runtime.http_addr - ), - Err(err) => err, - }; - assert!( - err.to_string().contains("network.allow_unix_sockets[0]"), - "error should point at the invalid allow_unix_sockets entry: {err:#}" - ); + fn resolve_runtime_validates_allow_unix_sockets_for_executor_os() { + for (path, unix_absolute, windows_absolute) in [ + ("relative.sock", false, false), + ("~/example.sock", false, false), + ("/tmp/example.sock", true, true), + (r"C:\example.sock", false, true), + ("C:/example.sock", false, true), + (r"\\server\share\example.sock", false, true), + (r"\\?\C:\example.sock", false, true), + (r"\\.\pipe\example", false, true), + (r"C:example.sock", false, false), + (r"\example.sock", false, false), + (r"\\server", false, false), + ("/tmp/\0example.sock", false, false), + ("C:\\example\0.sock", false, false), + ] { + let cfg = settings_with_unix_sockets(&[path]); + for (executor_os, accepted) in [ + (NetworkProxyExecutorOs::Linux, unix_absolute), + (NetworkProxyExecutorOs::Macos, unix_absolute), + (NetworkProxyExecutorOs::Windows, windows_absolute), + ( + NetworkProxyExecutorOs::Unknown, + unix_absolute || windows_absolute, + ), + ] { + assert_eq!( + resolve_runtime(&cfg, executor_os).is_ok(), + accepted, + "{executor_os:?}: {path:?}" + ); + } + // Each platform's CI compares the portable implementation to native + // Core acceptance, with the deliberate shared NUL rejection. + assert_eq!( + resolve_runtime( + &cfg, + NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)) + ) + .is_ok(), + (Path::new(path).is_absolute() || path.starts_with('/')) && !path.contains('\0'), + "native parity: {path:?}" + ); + } } #[test] fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() { - let cfg = settings_with_unix_sockets(&["/private/tmp/example.sock"]); + let mut cfg = settings_with_unix_sockets(&[ + "/private/tmp/example.sock", + "/tmp/../example.sock", + r"/tmp/name\part.sock", + ]); + for path in ["relative.sock", "~/example.sock", r"C:\example.sock", "\0"] { + cfg.unix_sockets + .as_mut() + .unwrap() + .entries + .insert(path.to_string(), NetworkUnixSocketPermission::Deny); + } - assert!( - resolve_runtime(&cfg).is_ok(), - "unix-style absolute allow_unix_sockets entry should be accepted" - ); + for executor_os in [ + NetworkProxyExecutorOs::Linux, + NetworkProxyExecutorOs::Macos, + NetworkProxyExecutorOs::Windows, + NetworkProxyExecutorOs::Unknown, + ] { + assert!( + resolve_runtime(&cfg, executor_os).is_ok(), + "unix-style absolute allow_unix_sockets entry should be accepted" + ); + } } } diff --git a/codex-rs/network-proxy/src/executor_os.rs b/codex-rs/network-proxy/src/executor_os.rs new file mode 100644 index 0000000000..14ab49a3ef --- /dev/null +++ b/codex-rs/network-proxy/src/executor_os.rs @@ -0,0 +1,54 @@ +//! Executor path semantics for policy validation, independent of the controller OS. +//! Socket support and native path normalization remain executor runtime concerns. + +/// OS of the executor whose network policy is being validated. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkProxyExecutorOs { + Linux, + Macos, + Windows, + /// Legacy executors may omit their OS. Accept absolute syntax supported by + /// either platform here; the executor validates against its own OS at launch. + Unknown, +} + +impl NetworkProxyExecutorOs { + /// Uses executor metadata, never the OS of the process doing the validation. + pub fn from_platform_os(platform_os: Option<&str>) -> Self { + match platform_os { + Some("linux") => Self::Linux, + Some("macos") => Self::Macos, + Some("windows") => Self::Windows, + Some(_) | None => Self::Unknown, + } + } + + pub(crate) fn socket_path_is_absolute(self, path: &str) -> bool { + // Core also accepts Unix-style absolute paths on Windows, for portability. + path.starts_with('/') + || match self { + Self::Linux | Self::Macos => false, + Self::Windows | Self::Unknown => windows_path_is_absolute(path), + } + } +} + +// Match std::path's Windows absolute-path syntax without consulting the host. +// Non-drive prefixes have an implicit root, including device and verbatim paths. +fn windows_path_is_absolute(path: &str) -> bool { + let bytes = path.as_bytes(); + if matches!(bytes, [drive, b':', b'\\' | b'/', ..] if drive.is_ascii_alphabetic()) { + return true; + } + let [b'\\' | b'/', b'\\' | b'/', rest @ ..] = bytes else { + return false; + }; + if path.starts_with(r"\\?\") || matches!(rest, [b'.', b'\\' | b'/', ..]) { + return true; + } + let mut components = rest.split(|byte| matches!(byte, b'\\' | b'/')); + matches!( + (components.next(), components.next()), + (Some(server), Some(share)) if !server.is_empty() && !share.is_empty() + ) +} diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index ae33185fd7..2e37047062 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -9,6 +9,7 @@ mod connect_policy; mod connection_lifecycle; mod credential_broker; mod environment_policy; +mod executor_os; mod http_proxy; mod mitm; mod mitm_hook; @@ -60,6 +61,7 @@ pub use credential_broker::credential_broker_provider_context_env_keys; pub use credential_broker::credential_broker_provider_sources_allowed; pub use credential_broker::is_credential_broker_provider_env_key; pub use environment_policy::EnvironmentNetworkPolicy; +pub use executor_os::NetworkProxyExecutorOs; pub use mitm_hook::InjectedHeaderConfig; pub use mitm_hook::MitmHookActionsConfig; pub use mitm_hook::MitmHookBodyConfig; diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index a47e1249b9..cb009b6e77 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -674,7 +674,12 @@ mod tests { ..NetworkProxyConfig::default() }; let config = network; - let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let state = build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(); let reloader = Arc::new(StaticReloader { state: state.clone(), }); diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index ab7d398b41..631af671e8 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -224,7 +224,7 @@ impl NetworkProxyBuilder { state .set_blocked_request_observer(self.blocked_request_observer.clone()) .await; - let current_cfg = state.current_cfg().await?; + let (current_cfg, _, executor_os) = state.current_cfg_with_brokerage_provenance().await?; #[cfg(target_os = "windows")] let (current_cfg, runtime_settings, mut windows_ingress) = { let current_cfg = config::NetworkProxyConfig { @@ -238,7 +238,7 @@ impl NetworkProxyBuilder { let (requested_http_addr, requested_socks_addr, reserved_listeners) = if self .managed_by_codex { - let runtime = config::resolve_runtime(¤t_cfg)?; + let runtime = config::resolve_runtime(¤t_cfg, executor_os)?; #[cfg(target_os = "windows")] let shared_ingress_addrs = if self.managed_proxy_routing == ManagedProxyRouting::SharedIngress @@ -273,7 +273,7 @@ impl NetworkProxyBuilder { ) } } else { - let runtime = config::resolve_runtime(¤t_cfg)?; + let runtime = config::resolve_runtime(¤t_cfg, executor_os)?; ( self.http_addr.unwrap_or(runtime.http_addr), self.socks_addr.unwrap_or(runtime.socks_addr), @@ -926,7 +926,7 @@ impl NetworkProxy { /// Captures the static inputs needed to launch a matching executor-local proxy. pub async fn remote_launch_config(&self) -> Result { - let (mut config, brokerage_created_default_allowlist) = + let (mut config, brokerage_created_default_allowlist, _) = self.state.current_cfg_with_brokerage_provenance().await?; // Proxy enablement and credential brokerage remain controller-owned. let mut broker_only_config = config::NetworkProxyConfig { @@ -1817,7 +1817,11 @@ mod tests { format!("{expected_bind_host}:8081").parse::()?, ) ); - let replacement = crate::state::build_config_state(config.clone(), Default::default())?; + let replacement = crate::state::build_config_state( + config.clone(), + Default::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; proxy.replace_config_state(replacement).await?; let expected_unix_sockets = if cfg!(target_os = "windows") { Vec::new() @@ -2087,6 +2091,7 @@ mod tests { ..NetworkProxyConfig::default() }, Default::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), ) .expect("replacement config state"); proxy @@ -2169,7 +2174,11 @@ mod tests { for allow_all in [false, true] { config.dangerously_allow_all_unix_sockets = Some(allow_all); - let replacement = crate::state::build_config_state(config.clone(), Default::default())?; + let replacement = crate::state::build_config_state( + config.clone(), + Default::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; proxy.replace_config_state(replacement).await?; let prepared = proxy .prepare_for_optional_environment(HashMap::new(), /*environment_id*/ None)?; diff --git a/codex-rs/network-proxy/src/remote_config_tests.rs b/codex-rs/network-proxy/src/remote_config_tests.rs index 6661ec81a1..abcfafd425 100644 --- a/codex-rs/network-proxy/src/remote_config_tests.rs +++ b/codex-rs/network-proxy/src/remote_config_tests.rs @@ -4,9 +4,12 @@ use super::RemoteNetworkProxyConfig; use super::RemoteNetworkProxyLaunchConfig; use crate::MitmHookConfig; use crate::NetworkMode; +use crate::NetworkProxy; use crate::NetworkProxyAuditMetadata; use crate::NetworkProxyConfig; +use crate::NetworkProxyExecutorOs; use crate::NetworkProxyState; +use std::sync::Arc; #[test] fn optional_socket_policy_preserves_input_and_resolves_at_remote_boundary() { @@ -38,27 +41,65 @@ fn optional_socket_policy_preserves_input_and_resolves_at_remote_boundary() { } } -#[test] -fn round_trip_preserves_supported_effective_settings() { - let mut config = NetworkProxyConfig { - enabled: true, - enable_socks5: false, - enable_socks5_udp: false, - allow_upstream_proxy: false, - dangerously_allow_all_unix_sockets: Some(true), - mode: NetworkMode::Limited, - allow_local_binding: true, - ..NetworkProxyConfig::default() - }; - config.set_allowed_domains(vec!["example.com".into()]); - config.set_denied_domains(vec!["blocked.example.com".into()]); - config.set_allow_unix_sockets(vec!["/var/run/example.sock".into()]); +#[tokio::test] +async fn round_trip_preserves_supported_effective_settings() { + for (executor_os, socket) in [ + (NetworkProxyExecutorOs::Linux, "/var/run/example.sock"), + (NetworkProxyExecutorOs::Macos, "/var/run/example.sock"), + (NetworkProxyExecutorOs::Windows, r"C:\example.sock"), + (NetworkProxyExecutorOs::Unknown, r"C:\example.sock"), + ] { + let mut config = NetworkProxyConfig { + enabled: true, + enable_socks5: false, + enable_socks5_udp: false, + allow_upstream_proxy: false, + dangerously_allow_all_unix_sockets: Some(true), + mode: NetworkMode::Limited, + allow_local_binding: true, + ..NetworkProxyConfig::default() + }; + config.set_allowed_domains(vec!["example.com".into()]); + config.set_denied_domains(vec!["blocked.example.com".into()]); + config.set_allow_unix_sockets(vec![socket.into()]); - let remote = - RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config"); - let round_trip = remote.into_network_proxy_config(); + let remote = RemoteNetworkProxyConfig::from_effective_config(&config) + .expect("supported remote config"); + let round_trip = remote.clone().into_network_proxy_config(); - assert_eq!(round_trip, config); + assert_eq!(round_trip, config); + + let state = Arc::new( + NetworkProxyState::from_remote_launch_config( + RemoteNetworkProxyLaunchConfig::new(remote), + executor_os, + ) + .unwrap(), + ); + // Policy edits and carrier construction must retain the executor's OS even + // when the controller itself cannot interpret the socket as a native path. + state.add_allowed_domain("added.example.com").await.unwrap(); + let proxy = NetworkProxy::builder() + .state(state) + .managed_by_codex(/*managed_by_codex*/ false) + .build() + .await + .unwrap(); + config.upsert_domain_permission( + "added.example.com".into(), + crate::NetworkDomainPermission::Allow, + crate::normalize_host, + ); + assert_eq!( + proxy + .remote_launch_config() + .await + .unwrap() + .proxy + .into_network_proxy_config(), + config + ); + } } #[test] @@ -136,13 +177,16 @@ fn launch_config_materializes_audit_and_execution_attribution() { 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()), - policy_decision_timeout_ms: None, - }) + 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()), + policy_decision_timeout_ms: None, + }, + NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) .expect("remote launch state"); assert_eq!(state.audit_metadata(), &audit_metadata); diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 4ffe48d349..e331f59f6c 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -168,6 +168,8 @@ fn blocked_request_violation_log_line(entry: &BlockedRequest) -> String { #[derive(Clone)] pub struct ConfigState { + /// Preserve the target when policy edits rebuild state on a controller. + pub(crate) executor_os: crate::NetworkProxyExecutorOs, pub config: NetworkProxyConfig, pub(crate) brokerage_created_default_allowlist: bool, pub allow_set: GlobSet, @@ -285,8 +287,10 @@ impl Clone for NetworkProxyState { impl NetworkProxyState { /// Builds runtime state for one executor-local proxy launch. + /// The launching executor supplies its own OS; the wire policy cannot override it. pub fn from_remote_launch_config( launch: crate::RemoteNetworkProxyLaunchConfig, + executor_os: crate::NetworkProxyExecutorOs, ) -> Result { let crate::RemoteNetworkProxyLaunchConfig { proxy, @@ -300,7 +304,7 @@ impl NetworkProxyState { "executor-local network proxy launch requires an enabled proxy" ); let config = proxy.into_network_proxy_config(); - let state = build_config_state(config, NetworkProxyConstraints::default())?; + let state = build_config_state(config, NetworkProxyConstraints::default(), executor_os)?; Ok(Self { environment_id: environment_id.map(Into::into), execution_id: execution_id.map(Into::into), @@ -571,7 +575,7 @@ impl NetworkProxyState { pub async fn current_cfg(&self) -> Result { self.current_cfg_with_brokerage_provenance() .await - .map(|(config, _)| config) + .map(|(config, _, _)| config) } pub(crate) fn credential_broker_config_revision(&self) -> u64 { @@ -580,7 +584,7 @@ impl NetworkProxyState { pub(crate) async fn current_cfg_with_brokerage_provenance( &self, - ) -> Result<(NetworkProxyConfig, bool)> { + ) -> Result<(NetworkProxyConfig, bool, crate::NetworkProxyExecutorOs)> { // Callers treat `NetworkProxyState` as a live view of policy. We reload-on-demand so edits to // `config.toml` (including Codex-managed writes) take effect without a restart. self.reload_if_needed().await?; @@ -588,6 +592,7 @@ impl NetworkProxyState { Ok(( guard.config.clone(), guard.brokerage_created_default_allowlist, + guard.executor_os, )) } @@ -934,13 +939,14 @@ impl NetworkProxyState { loop { self.reload_if_needed().await?; - let (previous_cfg, constraints, blocked, blocked_total) = { + let (previous_cfg, constraints, blocked, blocked_total, executor_os) = { let guard = self.state.read().await; ( guard.config.clone(), guard.constraints.clone(), guard.blocked.clone(), guard.blocked_total, + guard.executor_os, ) }; @@ -967,13 +973,17 @@ impl NetworkProxyState { .map_err(NetworkProxyConstraintError::into_anyhow) .with_context(|| format!("{constraint_field} constrained by managed config"))?; - let mut new_state = build_config_state(candidate.clone(), constraints.clone()) - .with_context(|| format!("failed to compile updated network {list_name}"))?; + let mut new_state = + build_config_state(candidate.clone(), constraints.clone(), executor_os) + .with_context(|| format!("failed to compile updated network {list_name}"))?; new_state.blocked = blocked; new_state.blocked_total = blocked_total; let mut guard = self.state.write().await; - if guard.constraints != constraints || guard.config != previous_cfg { + if guard.constraints != constraints + || guard.config != previous_cfg + || guard.executor_os != executor_os + { drop(guard); continue; } @@ -1178,6 +1188,7 @@ pub(crate) fn network_proxy_state_for_policy( config.set_allowed_domains(vec!["*".to_string()]); } let state = ConfigState { + executor_os: crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), allow_set: crate::policy::compile_allowlist_globset( &config.allowed_domains().unwrap_or_default(), ) @@ -1282,12 +1293,20 @@ mod tests { enabled: true, ..NetworkProxyConfig::default() }; - let initial_state = - build_config_state(config.clone(), NetworkProxyConstraints::default()).unwrap(); + let initial_state = build_config_state( + config.clone(), + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(); let mut reloaded_config = config; reloaded_config.set_credential_broker_enabled(/*enabled*/ true); - let reloaded_state = - build_config_state(reloaded_config, NetworkProxyConstraints::default()).unwrap(); + let reloaded_state = build_config_state( + reloaded_config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(); let state = NetworkProxyState::with_reloader( initial_state, Arc::new(StaticReloader { @@ -1314,6 +1333,7 @@ mod tests { enabled: Some(false), ..NetworkProxyConstraints::default() }, + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), ) .expect("managed-disabled credential broker should fail open"); let state = NetworkProxyState::with_reloader(config_state, Arc::new(NoopReloader)); @@ -1361,8 +1381,12 @@ mod tests { ..NetworkProxyConfig::default() }; config.set_credential_broker_enabled(/*enabled*/ true); - let config_state = build_config_state(config, NetworkProxyConstraints::default()) - .expect("valid credential-broker configuration"); + let config_state = build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .expect("valid credential-broker configuration"); assert_eq!(config_state.brokerage_created_default_allowlist, !enabled); let state = NetworkProxyState::with_reloader(config_state, Arc::new(NoopReloader)); @@ -1448,7 +1472,12 @@ mod tests { ..NetworkProxyConstraints::default() }; let state = NetworkProxyState::with_reloader( - build_config_state(config, constraints).unwrap(), + build_config_state( + config, + constraints, + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(), Arc::new(NoopReloader), ); @@ -1475,7 +1504,12 @@ mod tests { ..NetworkProxyConstraints::default() }; let state = NetworkProxyState::with_reloader( - build_config_state(config, constraints).unwrap(), + build_config_state( + config, + constraints, + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(), Arc::new(NoopReloader), ); @@ -1500,7 +1534,12 @@ mod tests { ..NetworkProxyConstraints::default() }; let state = NetworkProxyState::with_reloader( - build_config_state(config, constraints).unwrap(), + build_config_state( + config, + constraints, + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(), Arc::new(NoopReloader), ); @@ -2168,7 +2207,14 @@ mod tests { let mut config = network_settings(&["*"], &[]); config.enabled = true; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok()); + assert!( + build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)) + ) + .is_ok() + ); } #[test] @@ -2176,7 +2222,14 @@ mod tests { let mut config = network_settings(&["[*]"], &[]); config.enabled = true; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok()); + assert!( + build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)) + ) + .is_ok() + ); } #[test] @@ -2184,7 +2237,14 @@ mod tests { let mut config = network_settings(&["example.com"], &["*"]); config.enabled = true; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err()); + assert!( + build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)) + ) + .is_err() + ); } #[test] @@ -2192,7 +2252,14 @@ mod tests { let mut config = network_settings(&["example.com"], &["[*]"]); config.enabled = true; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err()); + assert!( + build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)) + ) + .is_err() + ); } #[cfg(target_os = "macos")] diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 82a7093dab..ec8c0a1116 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -902,7 +902,12 @@ mod tests { fn state_for_settings(network: NetworkProxyConfig) -> Arc { let config = network; let _mitm_config_state_guard = config.mitm.then(|| MITM_CONFIG_STATE_LOCK.lock().unwrap()); - let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let state = build_config_state( + config, + NetworkProxyConstraints::default(), + crate::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + ) + .unwrap(); let reloader = Arc::new(StaticReloader { state: state.clone(), }); diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index d5a1f513b4..26684e4b39 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -57,9 +57,12 @@ pub struct PartialNetworkProxyConfig { pub mitm_hooks: Option>, } +/// Compiles policy using the supplied executor OS, which may differ from this host. +/// Socket paths are validated without native normalization or filesystem access. pub fn build_config_state( mut config: NetworkProxyConfig, constraints: NetworkProxyConstraints, + executor_os: crate::NetworkProxyExecutorOs, ) -> anyhow::Result { if constraints.enabled == Some(false) { config.credential_broker = false; @@ -74,7 +77,7 @@ pub fn build_config_state( if brokerage_created_default_allowlist { config.set_allowed_domains(vec!["*".to_string()]); } - crate::config::validate_unix_socket_allowlist_paths(&config)?; + crate::config::validate_unix_socket_allowlist_paths(&config, executor_os)?; anyhow::ensure!( !config.credential_broker || config.mitm, "network.credential_broker requires network.mitm = true" @@ -94,6 +97,7 @@ pub fn build_config_state( None }; Ok(ConfigState { + executor_os, config, brokerage_created_default_allowlist, allow_set, diff --git a/codex-rs/network-proxy/tests/windows_stable_ingress.rs b/codex-rs/network-proxy/tests/windows_stable_ingress.rs index 2aa408732c..938b6342b7 100644 --- a/codex-rs/network-proxy/tests/windows_stable_ingress.rs +++ b/codex-rs/network-proxy/tests/windows_stable_ingress.rs @@ -302,7 +302,11 @@ async fn build_proxy( ..NetworkProxyConfig::default() }; config.set_allowed_domains(vec![allowed_domain.to_string()]); - let config_state = build_config_state(config, Default::default())?; + let config_state = build_config_state( + config, + Default::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; let reloader = Arc::new(StaticReloader(config_state.clone())); let state = Arc::new(NetworkProxyState::with_reloader(config_state, reloader)); let mut builder = NetworkProxy::builder().state(state); diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 483bde07d4..5ef53c24f1 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -541,6 +541,7 @@ async fn linux_unix_socket_grant_uses_effective_managed_policy() -> anyhow::Resu ..Default::default() }, NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), )?; let network = NetworkProxy::builder() .state(Arc::new(NetworkProxyState::with_reloader( diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 824e83af6a..217535c63f 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -816,7 +816,11 @@ async fn prepared_managed_network_context_takes_precedence_over_live_proxy_socke dangerously_allow_all_unix_sockets: Some(true), ..Default::default() }; - let state = build_config_state(network_config, NetworkProxyConstraints::default())?; + let state = build_config_state( + network_config, + NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; let network_proxy = NetworkProxy::builder() .state(Arc::new(NetworkProxyState::with_reloader( state, @@ -1524,7 +1528,11 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a ..Default::default() }; network_config.set_allow_unix_sockets(vec![network_socket.to_string()]); - let state = build_config_state(network_config, NetworkProxyConstraints::default())?; + let state = build_config_state( + network_config, + NetworkProxyConstraints::default(), + codex_network_proxy::NetworkProxyExecutorOs::from_platform_os(Some(std::env::consts::OS)), + )?; let network_proxy = NetworkProxy::builder() .state(Arc::new(NetworkProxyState::with_reloader( state,