From 7a82c935db5264dae6aff8e43fd34e1b55e3fcf8 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 10 Feb 2026 13:56:54 -0800 Subject: [PATCH 1/7] fix(network-proxy): add unix socket allow-all and seatbelt rules --- .../codex_app_server_protocol.schemas.json | 6 + .../v2/ConfigRequirementsReadResponse.json | 6 + .../typescript/v2/NetworkRequirements.ts | 2 +- .../app-server-protocol/src/protocol/v2.rs | 1 + codex-rs/app-server/src/config_api.rs | 3 + codex-rs/config/src/config_requirements.rs | 9 ++ .../core/src/config/network_proxy_spec.rs | 7 ++ codex-rs/core/src/network_proxy_loader.rs | 6 + codex-rs/core/src/seatbelt.rs | 117 ++++++++++++++++-- codex-rs/network-proxy/README.md | 10 +- codex-rs/network-proxy/src/config.rs | 25 +++- codex-rs/network-proxy/src/http_proxy.rs | 4 +- codex-rs/network-proxy/src/proxy.rs | 22 +++- codex-rs/network-proxy/src/runtime.rs | 55 ++++++++ codex-rs/network-proxy/src/state.rs | 21 ++++ codex-rs/tui/src/debug_config.rs | 6 + 16 files changed, 284 insertions(+), 16 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index dd63418f44d..8489d137893 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12615,6 +12615,12 @@ "null" ] }, + "dangerouslyAllowAllUnixSockets": { + "type": [ + "boolean", + "null" + ] + }, "dangerouslyAllowNonLoopbackAdmin": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 3ba96a852a9..63a3bf98732 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -84,6 +84,12 @@ "null" ] }, + "dangerouslyAllowAllUnixSockets": { + "type": [ + "boolean", + "null" + ] + }, "dangerouslyAllowNonLoopbackAdmin": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts index b7ac9d2f7a8..6205de1f4bb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array | null, deniedDomains: Array | null, allowUnixSockets: Array | null, allowLocalBinding: boolean | null, }; +export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array | null, deniedDomains: Array | null, allowUnixSockets: Array | null, allowLocalBinding: boolean | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index bfc4b7a80fa..cb3af3475a9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -549,6 +549,7 @@ pub struct NetworkRequirements { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_non_loopback_admin: Option, + pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, pub denied_domains: Option>, pub allow_unix_sockets: Option>, diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index e1531dce2f6..c9317ac9f52 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -161,6 +161,7 @@ fn map_network_requirements_to_api( allow_upstream_proxy: network.allow_upstream_proxy, dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy, dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin, + dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets, allowed_domains: network.allowed_domains, denied_domains: network.denied_domains, allow_unix_sockets: network.allow_unix_sockets, @@ -221,6 +222,7 @@ mod tests { allow_upstream_proxy: Some(false), dangerously_allow_non_loopback_proxy: Some(false), dangerously_allow_non_loopback_admin: Some(false), + dangerously_allow_all_unix_sockets: Some(true), allowed_domains: Some(vec!["api.openai.com".to_string()]), denied_domains: Some(vec!["example.com".to_string()]), allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), @@ -258,6 +260,7 @@ mod tests { allow_upstream_proxy: Some(false), dangerously_allow_non_loopback_proxy: Some(false), dangerously_allow_non_loopback_admin: Some(false), + dangerously_allow_all_unix_sockets: Some(true), allowed_domains: Some(vec!["api.openai.com".to_string()]), denied_domains: Some(vec!["example.com".to_string()]), allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 8632023d486..841d7174196 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -135,6 +135,7 @@ pub struct NetworkRequirementsToml { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_non_loopback_admin: Option, + pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, pub denied_domains: Option>, pub allow_unix_sockets: Option>, @@ -150,6 +151,7 @@ pub struct NetworkConstraints { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_non_loopback_admin: Option, + pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, pub denied_domains: Option>, pub allow_unix_sockets: Option>, @@ -165,6 +167,7 @@ impl From for NetworkConstraints { allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_non_loopback_admin, + dangerously_allow_all_unix_sockets, allowed_domains, denied_domains, allow_unix_sockets, @@ -177,6 +180,7 @@ impl From for NetworkConstraints { allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_non_loopback_admin, + dangerously_allow_all_unix_sockets, allowed_domains, denied_domains, allow_unix_sockets, @@ -1039,6 +1043,7 @@ mod tests { [experimental_network] enabled = true allow_upstream_proxy = false + dangerously_allow_all_unix_sockets = true allowed_domains = ["api.example.com", "*.openai.com"] denied_domains = ["blocked.example.com"] allow_unix_sockets = ["/tmp/example.sock"] @@ -1057,6 +1062,10 @@ mod tests { assert_eq!(sourced_network.source, source); assert_eq!(sourced_network.value.enabled, Some(true)); assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false)); + assert_eq!( + sourced_network.value.dangerously_allow_all_unix_sockets, + Some(true) + ); assert_eq!( sourced_network.value.allowed_domains.as_ref(), Some(&vec![ diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index ee3e8ebed1e..b61ae982659 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -149,6 +149,13 @@ impl NetworkProxySpec { constraints.dangerously_allow_non_loopback_admin = Some(dangerously_allow_non_loopback_admin); } + if let Some(dangerously_allow_all_unix_sockets) = + requirements.dangerously_allow_all_unix_sockets + { + config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets; + constraints.dangerously_allow_all_unix_sockets = + Some(dangerously_allow_all_unix_sockets); + } if let Some(allowed_domains) = requirements.allowed_domains.clone() { config.network.allowed_domains = allowed_domains.clone(); constraints.allowed_domains = Some(allowed_domains); diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index 3e06452740c..fa169d2fdc2 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -127,6 +127,12 @@ fn network_constraints_from_trusted_layers( constraints.dangerously_allow_non_loopback_admin = Some(dangerously_allow_non_loopback_admin); } + if let Some(dangerously_allow_all_unix_sockets) = + partial.network.dangerously_allow_all_unix_sockets + { + constraints.dangerously_allow_all_unix_sockets = + Some(dangerously_allow_all_unix_sockets); + } if let Some(allowed_domains) = partial.network.allowed_domains { constraints.allowed_domains = Some(allowed_domains); diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index e2061b52dcd..656745e38d3 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -1,6 +1,5 @@ #![cfg(target_os = "macos")] -use codex_network_proxy::ALLOW_LOCAL_BINDING_ENV_KEY; use codex_network_proxy::NetworkProxy; use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_network_proxy::has_proxy_url_env_vars; @@ -101,18 +100,13 @@ fn proxy_loopback_ports_from_env(env: &HashMap) -> Vec { ports.into_iter().collect() } -fn local_binding_enabled(env: &HashMap) -> bool { - env.get(ALLOW_LOCAL_BINDING_ENV_KEY).is_some_and(|value| { - let trimmed = value.trim(); - trimmed == "1" || trimmed.eq_ignore_ascii_case("true") - }) -} - #[derive(Debug, Default)] struct ProxyPolicyInputs { ports: Vec, has_proxy_config: bool, allow_local_binding: bool, + allow_unix_sockets: Vec, + dangerously_allow_all_unix_sockets: bool, } fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { @@ -122,13 +116,48 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { return ProxyPolicyInputs { ports: proxy_loopback_ports_from_env(&env), has_proxy_config: has_proxy_url_env_vars(&env), - allow_local_binding: local_binding_enabled(&env), + allow_local_binding: network.allow_local_binding(), + allow_unix_sockets: network.allow_unix_sockets().to_vec(), + dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets(), }; } ProxyPolicyInputs::default() } +fn normalize_path_for_sandbox(path: &Path) -> Option { + if !path.is_absolute() { + return None; + } + Some(path.canonicalize().unwrap_or_else(|_| path.to_path_buf())) +} + +fn escape_seatbelt_string(value: &str) -> String { + value.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String { + if proxy.dangerously_allow_all_unix_sockets { + return "(allow network* (subpath \"/\"))\n".to_string(); + } + + let mut paths = BTreeSet::new(); + for socket_path in &proxy.allow_unix_sockets { + let Some(normalized_path) = normalize_path_for_sandbox(Path::new(socket_path)) else { + continue; + }; + paths.insert(normalized_path); + } + + paths + .into_iter() + .map(|path| { + let escaped_path = escape_seatbelt_string(path.to_string_lossy().as_ref()); + format!("(allow network* (subpath \"{escaped_path}\"))\n") + }) + .collect() +} + fn dynamic_network_policy( sandbox_policy: &SandboxPolicy, enforce_managed_network: bool, @@ -148,6 +177,11 @@ fn dynamic_network_policy( "(allow network-outbound (remote ip \"localhost:{port}\"))\n" )); } + let unix_socket_policy = unix_socket_policy(proxy); + if !unix_socket_policy.is_empty() { + policy.push_str("; allow unix domain sockets for local IPC\n"); + policy.push_str(&unix_socket_policy); + } return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}"); } @@ -323,6 +357,7 @@ mod tests { ports: vec![43128, 48081], has_proxy_config: true, allow_local_binding: false, + ..ProxyPolicyInputs::default() }, ); @@ -357,6 +392,7 @@ mod tests { ports: vec![43128], has_proxy_config: true, allow_local_binding: true, + ..ProxyPolicyInputs::default() }, ); @@ -392,6 +428,7 @@ mod tests { ports: vec![], has_proxy_config: true, allow_local_binding: false, + ..ProxyPolicyInputs::default() }, ); @@ -419,12 +456,73 @@ mod tests { ports: vec![], has_proxy_config: false, allow_local_binding: false, + ..ProxyPolicyInputs::default() }, ); assert_eq!(policy, ""); } + #[test] + fn create_seatbelt_args_allowlists_unix_socket_paths() { + let policy = dynamic_network_policy( + &SandboxPolicy::ReadOnly, + false, + &ProxyPolicyInputs { + ports: vec![43128], + has_proxy_config: true, + allow_local_binding: false, + allow_unix_sockets: vec!["/tmp/example.sock".to_string()], + dangerously_allow_all_unix_sockets: false, + }, + ); + + assert!( + policy.contains("(allow network* (subpath \"/tmp/example.sock\"))"), + "policy should allow explicitly configured unix sockets:\n{policy}" + ); + } + + #[test] + fn create_seatbelt_args_ignores_relative_unix_socket_paths() { + let policy = dynamic_network_policy( + &SandboxPolicy::ReadOnly, + false, + &ProxyPolicyInputs { + ports: vec![43128], + has_proxy_config: true, + allow_local_binding: false, + allow_unix_sockets: vec!["relative.sock".to_string()], + dangerously_allow_all_unix_sockets: false, + }, + ); + + assert!( + !policy.contains("relative.sock"), + "policy should ignore relative unix socket paths:\n{policy}" + ); + } + + #[test] + fn create_seatbelt_args_allows_all_unix_sockets_when_enabled() { + let policy = dynamic_network_policy( + &SandboxPolicy::ReadOnly, + false, + &ProxyPolicyInputs { + ports: vec![43128], + has_proxy_config: true, + allow_local_binding: false, + allow_unix_sockets: vec![], + dangerously_allow_all_unix_sockets: true, + }, + ); + + assert!( + policy.contains("(allow network* (subpath \"/\"))"), + "policy should allow all unix sockets when flag is enabled:\n{policy}" + ); + } + #[test] fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { let policy = dynamic_network_policy( @@ -439,6 +537,7 @@ mod tests { ports: vec![43128], has_proxy_config: true, allow_local_binding: false, + ..ProxyPolicyInputs::default() }, ); diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 7ffed9008d7..eb55521b2bf 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -47,6 +47,9 @@ allow_local_binding = true # macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`. allow_unix_sockets = ["/tmp/example.sock"] +# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any +# absolute socket path from `x-unix-socket`. +dangerously_allow_all_unix_sockets = false ``` ### 2) Run the proxy @@ -111,8 +114,9 @@ let handle = proxy.run().await?; handle.shutdown().await?; ``` -When unix socket proxying is enabled, HTTP/admin bind overrides are still clamped to loopback -to avoid turning the proxy into a remote bridge to local daemons. +When unix socket proxying is enabled (`allow_unix_sockets` or +`dangerously_allow_all_unix_sockets`), HTTP/admin bind overrides are still clamped to loopback to +avoid turning the proxy into a remote bridge to local daemons. ### Policy hook (exec-policy mapping) @@ -171,6 +175,8 @@ what it can reasonably guarantee. `dangerously_allow_non_loopback_proxy` - when unix socket proxying is enabled, both listeners are forced to loopback to avoid turning the proxy into a remote bridge into local daemons. +- `dangerously_allow_all_unix_sockets = true` bypasses the unix socket allowlist entirely (still + macOS-only and absolute-path-only). Use only in tightly controlled environments. - `enabled` is enforced at runtime; when false the proxy no-ops and does not bind listeners. Limitations: diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 31197a1c96d..80ff5d58a32 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -33,6 +33,8 @@ pub struct NetworkProxySettings { #[serde(default)] pub dangerously_allow_non_loopback_admin: bool, #[serde(default)] + pub dangerously_allow_all_unix_sockets: bool, + #[serde(default)] pub mode: NetworkMode, #[serde(default)] pub allowed_domains: Vec, @@ -55,6 +57,7 @@ impl Default for NetworkProxySettings { allow_upstream_proxy: true, dangerously_allow_non_loopback_proxy: false, dangerously_allow_non_loopback_admin: false, + dangerously_allow_all_unix_sockets: false, mode: NetworkMode::default(), allowed_domains: Vec::new(), denied_domains: Vec::new(), @@ -136,7 +139,7 @@ pub(crate) fn clamp_bind_addrs( cfg.dangerously_allow_non_loopback_admin, "admin API", ); - if cfg.allow_unix_sockets.is_empty() { + if cfg.allow_unix_sockets.is_empty() && !cfg.dangerously_allow_all_unix_sockets { return (http_addr, socks_addr, admin_addr); } @@ -526,4 +529,24 @@ mod tests { assert_eq!(socks_addr, "127.0.0.1:8081".parse::().unwrap()); assert_eq!(admin_addr, "127.0.0.1:8080".parse::().unwrap()); } + + #[test] + fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled() { + let cfg = NetworkProxySettings { + dangerously_allow_non_loopback_proxy: true, + dangerously_allow_non_loopback_admin: true, + dangerously_allow_all_unix_sockets: true, + ..Default::default() + }; + let http_addr = "0.0.0.0:3128".parse::().unwrap(); + let socks_addr = "0.0.0.0:8081".parse::().unwrap(); + let admin_addr = "0.0.0.0:8080".parse::().unwrap(); + + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg); + + assert_eq!(http_addr, "127.0.0.1:3128".parse::().unwrap()); + assert_eq!(socks_addr, "127.0.0.1:8081".parse::().unwrap()); + assert_eq!(admin_addr, "127.0.0.1:8080".parse::().unwrap()); + } } diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index e59980cc5f5..fc2256f78c8 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -374,8 +374,8 @@ async fn http_plain_proxy( }; // `x-unix-socket` is an escape hatch for talking to local daemons. We keep it tightly scoped: - // macOS-only + explicit allowlist, to avoid turning the proxy into a general local capability - // escalation mechanism. + // macOS-only + explicit allowlist by default, to avoid turning the proxy into a general local + // capability escalation mechanism. if let Some(unix_socket_header) = req.headers().get("x-unix-socket") { let socket_path = match unix_socket_header.to_str() { Ok(value) => value.to_string(), diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 566ac0da258..aec1fa46a62 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -184,6 +184,10 @@ impl NetworkProxyBuilder { socks_addr, socks_enabled: current_cfg.network.enable_socks5, allow_local_binding: current_cfg.network.allow_local_binding, + allow_unix_sockets: current_cfg.network.allow_unix_sockets.clone(), + dangerously_allow_all_unix_sockets: current_cfg + .network + .dangerously_allow_all_unix_sockets, admin_addr, reserved_listeners, policy_decider: self.policy_decider, @@ -218,6 +222,8 @@ pub struct NetworkProxy { socks_addr: SocketAddr, socks_enabled: bool, allow_local_binding: bool, + allow_unix_sockets: Vec, + dangerously_allow_all_unix_sockets: bool, admin_addr: SocketAddr, reserved_listeners: Option>, policy_decider: Option>, @@ -386,6 +392,18 @@ impl NetworkProxy { self.admin_addr } + pub fn allow_local_binding(&self) -> bool { + self.allow_local_binding + } + + pub fn allow_unix_sockets(&self) -> &[String] { + &self.allow_unix_sockets + } + + pub fn dangerously_allow_all_unix_sockets(&self) -> bool { + self.dangerously_allow_all_unix_sockets + } + pub fn apply_to_env(&self, env: &mut HashMap) { // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. @@ -408,7 +426,9 @@ impl NetworkProxy { ensure_rustls_crypto_provider(); if !unix_socket_permissions_supported() { - warn!("allowUnixSockets is macOS-only; requests will be rejected on this platform"); + warn!( + "allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform" + ); } let reserved_listeners = self.reserved_listeners.as_ref(); diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 029c7b5f6ce..c3e18b6f9ff 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -308,6 +308,10 @@ impl NetworkProxyState { } let guard = self.state.read().await; + if guard.config.network.dangerously_allow_all_unix_sockets { + return Ok(true); + } + // Normalize the path while keeping the absolute-path requirement explicit. let requested_abs = match AbsolutePathBuf::from_absolute_path(requested_path) { Ok(path) => path, @@ -874,6 +878,43 @@ mod tests { assert!(validate_policy_against_constraints(&config, &constraints).is_ok()); } + #[test] + fn validate_policy_against_constraints_disallows_allow_all_unix_sockets_without_managed_opt_in() + { + let constraints = NetworkProxyConstraints { + dangerously_allow_all_unix_sockets: Some(false), + ..NetworkProxyConstraints::default() + }; + + let config = NetworkProxyConfig { + network: NetworkProxySettings { + enabled: true, + dangerously_allow_all_unix_sockets: true, + ..NetworkProxySettings::default() + }, + }; + + assert!(validate_policy_against_constraints(&config, &constraints).is_err()); + } + + #[test] + fn validate_policy_against_constraints_allows_allow_all_unix_sockets_with_managed_opt_in() { + let constraints = NetworkProxyConstraints { + dangerously_allow_all_unix_sockets: Some(true), + ..NetworkProxyConstraints::default() + }; + + let config = NetworkProxyConfig { + network: NetworkProxySettings { + enabled: true, + dangerously_allow_all_unix_sockets: true, + ..NetworkProxySettings::default() + }, + }; + + assert!(validate_policy_against_constraints(&config, &constraints).is_ok()); + } + #[test] fn compile_globset_is_case_insensitive() { let patterns = vec!["ExAmPle.CoM".to_string()]; @@ -971,6 +1012,19 @@ mod tests { assert!(state.is_unix_socket_allowed(&link_s).await.unwrap()); } + #[cfg(target_os = "macos")] + #[tokio::test] + async fn unix_socket_allow_all_flag_bypasses_allowlist() { + let state = network_proxy_state_for_policy(NetworkProxySettings { + allowed_domains: vec!["example.com".to_string()], + dangerously_allow_all_unix_sockets: true, + ..NetworkProxySettings::default() + }); + + assert!(state.is_unix_socket_allowed("/tmp/any.sock").await.unwrap()); + assert!(!state.is_unix_socket_allowed("relative.sock").await.unwrap()); + } + #[cfg(not(target_os = "macos"))] #[tokio::test] async fn unix_socket_allowlist_is_rejected_on_non_macos() { @@ -978,6 +1032,7 @@ mod tests { let state = network_proxy_state_for_policy(NetworkProxySettings { allowed_domains: vec!["example.com".to_string()], allow_unix_sockets: vec![socket_path.clone()], + dangerously_allow_all_unix_sockets: true, ..NetworkProxySettings::default() }); diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 509ada760e5..aa3804a52ec 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -19,6 +19,7 @@ pub struct NetworkProxyConstraints { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_non_loopback_admin: Option, + pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, pub denied_domains: Option>, pub allow_unix_sockets: Option>, @@ -38,6 +39,7 @@ pub struct PartialNetworkConfig { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_non_loopback_admin: Option, + pub dangerously_allow_all_unix_sockets: Option, #[serde(default)] pub allowed_domains: Option>, #[serde(default)] @@ -172,6 +174,25 @@ pub fn validate_policy_against_constraints( }, )?; + let allow_all_unix_sockets = constraints.dangerously_allow_all_unix_sockets; + validate( + config.network.dangerously_allow_all_unix_sockets, + move |candidate| match allow_all_unix_sockets { + Some(true) | None => Ok(()), + Some(false) => { + if *candidate { + Err(invalid_value( + "network.dangerously_allow_all_unix_sockets", + "true", + "false (disabled by managed config)", + )) + } else { + Ok(()) + } + } + }, + )?; + if let Some(allow_local_binding) = constraints.allow_local_binding { validate(config.network.allow_local_binding, move |candidate| { if *candidate && !allow_local_binding { diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index ddc155da0c8..07a7855bd56 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -331,6 +331,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String { allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_non_loopback_admin, + dangerously_allow_all_unix_sockets, allowed_domains, denied_domains, allow_unix_sockets, @@ -359,6 +360,11 @@ fn format_network_constraints(network: &NetworkConstraints) -> String { "dangerously_allow_non_loopback_admin={dangerously_allow_non_loopback_admin}" )); } + if let Some(dangerously_allow_all_unix_sockets) = dangerously_allow_all_unix_sockets { + parts.push(format!( + "dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}" + )); + } if let Some(allowed_domains) = allowed_domains { parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", "))); } From 3a64fd433ddc7588f249245425bf9899dd7a8ccf Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:26:14 -0800 Subject: [PATCH 2/7] refactor(core): use AbsolutePathBuf for seatbelt unix socket inputs --- codex-rs/core/src/seatbelt.rs | 55 ++++++++++++++++------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 656745e38d3..a60b7959730 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -4,6 +4,7 @@ use codex_network_proxy::NetworkProxy; use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_network_proxy::has_proxy_url_env_vars; use codex_network_proxy::proxy_url_env_value; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::BTreeSet; use std::collections::HashMap; use std::ffi::CStr; @@ -105,7 +106,7 @@ struct ProxyPolicyInputs { ports: Vec, has_proxy_config: bool, allow_local_binding: bool, - allow_unix_sockets: Vec, + allow_unix_sockets: Vec, dangerously_allow_all_unix_sockets: bool, } @@ -117,7 +118,11 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { ports: proxy_loopback_ports_from_env(&env), has_proxy_config: has_proxy_url_env_vars(&env), allow_local_binding: network.allow_local_binding(), - allow_unix_sockets: network.allow_unix_sockets().to_vec(), + allow_unix_sockets: network + .allow_unix_sockets() + .iter() + .filter_map(|path| normalize_path_for_sandbox(Path::new(path))) + .collect(), dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets(), }; } @@ -125,11 +130,12 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { ProxyPolicyInputs::default() } -fn normalize_path_for_sandbox(path: &Path) -> Option { +fn normalize_path_for_sandbox(path: &Path) -> Option { if !path.is_absolute() { return None; } - Some(path.canonicalize().unwrap_or_else(|_| path.to_path_buf())) + let normalized = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + AbsolutePathBuf::from_absolute_path(normalized).ok() } fn escape_seatbelt_string(value: &str) -> String { @@ -141,18 +147,16 @@ fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String { return "(allow network* (subpath \"/\"))\n".to_string(); } - let mut paths = BTreeSet::new(); - for socket_path in &proxy.allow_unix_sockets { - let Some(normalized_path) = normalize_path_for_sandbox(Path::new(socket_path)) else { - continue; - }; - paths.insert(normalized_path); - } + let paths: BTreeSet = proxy + .allow_unix_sockets + .iter() + .map(|path| path.as_path().to_string_lossy().to_string()) + .collect(); paths .into_iter() .map(|path| { - let escaped_path = escape_seatbelt_string(path.to_string_lossy().as_ref()); + let escaped_path = escape_seatbelt_string(path.as_str()); format!("(allow network* (subpath \"{escaped_path}\"))\n") }) .collect() @@ -329,8 +333,10 @@ mod tests { use super::create_seatbelt_command_args; use super::dynamic_network_policy; use super::macos_dir_params; + use super::normalize_path_for_sandbox; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -348,6 +354,10 @@ mod tests { ); } + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(Path::new(path)).expect("absolute path") + } + #[test] fn create_seatbelt_args_routes_network_through_proxy_ports() { let policy = dynamic_network_policy( @@ -472,7 +482,7 @@ mod tests { ports: vec![43128], has_proxy_config: true, allow_local_binding: false, - allow_unix_sockets: vec!["/tmp/example.sock".to_string()], + allow_unix_sockets: vec![absolute_path("/tmp/example.sock")], dangerously_allow_all_unix_sockets: false, }, ); @@ -484,23 +494,8 @@ mod tests { } #[test] - fn create_seatbelt_args_ignores_relative_unix_socket_paths() { - let policy = dynamic_network_policy( - &SandboxPolicy::ReadOnly, - false, - &ProxyPolicyInputs { - ports: vec![43128], - has_proxy_config: true, - allow_local_binding: false, - allow_unix_sockets: vec!["relative.sock".to_string()], - dangerously_allow_all_unix_sockets: false, - }, - ); - - assert!( - !policy.contains("relative.sock"), - "policy should ignore relative unix socket paths:\n{policy}" - ); + fn normalize_path_for_sandbox_rejects_relative_paths() { + assert_eq!(normalize_path_for_sandbox(Path::new("relative.sock")), None); } #[test] From faff6d5d08391bbebfed49bdfabf00c25418095e Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:30:52 -0800 Subject: [PATCH 3/7] refactor(core): use AbsolutePathBuf normalization in seatbelt unix sockets --- codex-rs/core/src/seatbelt.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index a60b7959730..e89a5e7a6a9 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -131,11 +131,13 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { } fn normalize_path_for_sandbox(path: &Path) -> Option { - if !path.is_absolute() { - return None; - } - let normalized = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); - AbsolutePathBuf::from_absolute_path(normalized).ok() + let absolute_path = AbsolutePathBuf::from_absolute_path(path).ok()?; + let normalized_path = absolute_path + .as_path() + .canonicalize() + .ok() + .and_then(|canonical_path| AbsolutePathBuf::from_absolute_path(canonical_path).ok()); + normalized_path.or(Some(absolute_path)) } fn escape_seatbelt_string(value: &str) -> String { @@ -494,8 +496,14 @@ mod tests { } #[test] - fn normalize_path_for_sandbox_rejects_relative_paths() { - assert_eq!(normalize_path_for_sandbox(Path::new("relative.sock")), None); + fn normalize_path_for_sandbox_resolves_relative_paths() { + let normalized = + normalize_path_for_sandbox(Path::new("relative.sock")).expect("relative path"); + assert!(normalized.as_path().is_absolute()); + assert!( + normalized.as_path().ends_with("relative.sock"), + "normalized path should preserve input suffix" + ); } #[test] From ddc771a544efe6c3867b95e88d74039b6caa4d5f Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:33:33 -0800 Subject: [PATCH 4/7] refactor(core): parameterize seatbelt unix socket allowlist paths --- codex-rs/core/src/seatbelt.rs | 76 ++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index e89a5e7a6a9..89234b9c3c8 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -5,6 +5,7 @@ use codex_network_proxy::PROXY_URL_ENV_KEYS; use codex_network_proxy::has_proxy_url_env_vars; use codex_network_proxy::proxy_url_env_value; use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::ffi::CStr; @@ -140,8 +141,30 @@ fn normalize_path_for_sandbox(path: &Path) -> Option { normalized_path.or(Some(absolute_path)) } -fn escape_seatbelt_string(value: &str) -> String { - value.replace('\\', "\\\\").replace('"', "\\\"") +fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<(String, AbsolutePathBuf)> { + if proxy.dangerously_allow_all_unix_sockets { + return vec![]; + } + + let mut deduped_paths: BTreeMap = BTreeMap::new(); + for path in &proxy.allow_unix_sockets { + deduped_paths + .entry(path.to_string_lossy().to_string()) + .or_insert_with(|| path.clone()); + } + + deduped_paths + .into_values() + .enumerate() + .map(|(index, path)| (format!("UNIX_SOCKET_PATH_{index}"), path)) + .collect() +} + +fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> { + unix_socket_path_params(proxy) + .into_iter() + .map(|(key, path)| (key, path.into_path_buf())) + .collect() } fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String { @@ -149,18 +172,9 @@ fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String { return "(allow network* (subpath \"/\"))\n".to_string(); } - let paths: BTreeSet = proxy - .allow_unix_sockets + unix_socket_path_params(proxy) .iter() - .map(|path| path.as_path().to_string_lossy().to_string()) - .collect(); - - paths - .into_iter() - .map(|path| { - let escaped_path = escape_seatbelt_string(path.as_str()); - format!("(allow network* (subpath \"{escaped_path}\"))\n") - }) + .map(|(key, _)| format!("(allow network* (subpath (param \"{key}\")))\n")) .collect() } @@ -290,7 +304,12 @@ pub(crate) fn create_seatbelt_command_args( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" ); - let dir_params = [file_write_dir_params, macos_dir_params()].concat(); + let dir_params = [ + file_write_dir_params, + macos_dir_params(), + unix_socket_dir_params(&proxy), + ] + .concat(); let mut seatbelt_args: Vec = vec!["-p".to_string(), full_policy]; let definition_args = dir_params @@ -336,6 +355,7 @@ mod tests { use super::dynamic_network_policy; use super::macos_dir_params; use super::normalize_path_for_sandbox; + use super::unix_socket_dir_params; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use codex_utils_absolute_path::AbsolutePathBuf; @@ -490,11 +510,37 @@ mod tests { ); assert!( - policy.contains("(allow network* (subpath \"/tmp/example.sock\"))"), + policy.contains("(allow network* (subpath (param \"UNIX_SOCKET_PATH_0\")))"), "policy should allow explicitly configured unix sockets:\n{policy}" ); } + #[test] + fn unix_socket_dir_params_use_stable_param_names() { + let params = unix_socket_dir_params(&ProxyPolicyInputs { + allow_unix_sockets: vec![ + absolute_path("/tmp/b.sock"), + absolute_path("/tmp/a.sock"), + absolute_path("/tmp/a.sock"), + ], + ..ProxyPolicyInputs::default() + }); + + assert_eq!( + params, + vec![ + ( + "UNIX_SOCKET_PATH_0".to_string(), + PathBuf::from("/tmp/a.sock") + ), + ( + "UNIX_SOCKET_PATH_1".to_string(), + PathBuf::from("/tmp/b.sock") + ), + ] + ); + } + #[test] fn normalize_path_for_sandbox_resolves_relative_paths() { let normalized = From cec92006f69d3ca964eda9dd4860b7baf03ba60a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:36:09 -0800 Subject: [PATCH 5/7] docs(core): document unix socket seatbelt newline invariant --- codex-rs/core/src/seatbelt.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 89234b9c3c8..6190d631a0d 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -167,6 +167,9 @@ fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> { .collect() } +/// Returns zero or more complete Seatbelt policy lines for unix socket rules. +/// When non-empty, the returned string is newline-terminated so callers can +/// append it directly to larger policy blocks. fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String { if proxy.dangerously_allow_all_unix_sockets { return "(allow network* (subpath \"/\"))\n".to_string(); @@ -356,6 +359,7 @@ mod tests { use super::macos_dir_params; use super::normalize_path_for_sandbox; use super::unix_socket_dir_params; + use super::unix_socket_policy; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use codex_utils_absolute_path::AbsolutePathBuf; @@ -515,6 +519,27 @@ mod tests { ); } + #[test] + fn unix_socket_policy_non_empty_output_is_newline_terminated() { + let allowlist_policy = unix_socket_policy(&ProxyPolicyInputs { + allow_unix_sockets: vec![absolute_path("/tmp/example.sock")], + ..ProxyPolicyInputs::default() + }); + assert!( + allowlist_policy.ends_with('\n'), + "allowlist unix socket policy should end with a newline:\n{allowlist_policy}" + ); + + let allow_all_policy = unix_socket_policy(&ProxyPolicyInputs { + dangerously_allow_all_unix_sockets: true, + ..ProxyPolicyInputs::default() + }); + assert!( + allow_all_policy.ends_with('\n'), + "allow-all unix socket policy should end with a newline:\n{allow_all_policy}" + ); + } + #[test] fn unix_socket_dir_params_use_stable_param_names() { let params = unix_socket_dir_params(&ProxyPolicyInputs { From b9d540b1b6ee0c9a07c332dfd59b90d19202305a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:37:37 -0800 Subject: [PATCH 6/7] chore(core): log skipped unix socket allowlist entries --- codex-rs/core/src/seatbelt.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 6190d631a0d..b8b6b29556d 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -12,6 +12,7 @@ use std::ffi::CStr; use std::path::Path; use std::path::PathBuf; use tokio::process::Child; +use tracing::warn; use url::Url; use crate::protocol::SandboxPolicy; @@ -115,15 +116,22 @@ fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs { if let Some(network) = network { let mut env = HashMap::new(); network.apply_to_env(&mut env); + let mut allow_unix_sockets = Vec::new(); + for socket_path in network.allow_unix_sockets() { + match normalize_path_for_sandbox(Path::new(socket_path)) { + Some(path) => allow_unix_sockets.push(path), + None => { + warn!( + "ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}" + ); + } + } + } return ProxyPolicyInputs { ports: proxy_loopback_ports_from_env(&env), has_proxy_config: has_proxy_url_env_vars(&env), allow_local_binding: network.allow_local_binding(), - allow_unix_sockets: network - .allow_unix_sockets() - .iter() - .filter_map(|path| normalize_path_for_sandbox(Path::new(path))) - .collect(), + allow_unix_sockets, dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets(), }; } From fe9a424a76ad1161d0cfc27056ca75c324e767e9 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Feb 2026 10:56:25 -0800 Subject: [PATCH 7/7] test(network-proxy): include unix socket allow-all in default settings baseline --- codex-rs/network-proxy/src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 80ff5d58a32..cb390cac486 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -343,6 +343,7 @@ mod tests { allow_upstream_proxy: true, dangerously_allow_non_loopback_proxy: false, dangerously_allow_non_loopback_admin: false, + dangerously_allow_all_unix_sockets: false, mode: NetworkMode::Full, allowed_domains: Vec::new(), denied_domains: Vec::new(),