From a7533e144758d0dbd42eb3aa1e47ca9e1b99276d Mon Sep 17 00:00:00 2001 From: rreichel3-oai Date: Mon, 23 Mar 2026 15:46:47 -0400 Subject: [PATCH 1/5] Allow global network allowlist wildcard Update network-proxy glob compilation and validation so the global * wildcard is accepted for allowed_domains but still rejected for denied_domains. Add host-policy and config-state tests covering denylist-only behavior with a global allowlist wildcard, and refresh the network-proxy README to document the new mode. --- codex-rs/network-proxy/README.md | 5 ++- codex-rs/network-proxy/src/policy.rs | 25 +++++++++--- codex-rs/network-proxy/src/runtime.rs | 55 +++++++++++++++++++------ codex-rs/network-proxy/src/state.rs | 58 +++++++++++++++++++++------ 4 files changed, 110 insertions(+), 33 deletions(-) diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 6fb689490a0..0347e6ddf25 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -39,7 +39,8 @@ mitm = false # Hosts must match the allowlist (unless denied). # Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`. -# The global `*` wildcard is rejected. +# The global `*` wildcard is allowed in `allowed_domains` to delegate public-host filtering to +# `denied_domains`. # If `allowed_domains` is empty, the proxy blocks requests until an allowlist is configured. allowed_domains = ["*.openai.com", "localhost", "127.0.0.1", "::1"] denied_domains = ["evil.example"] @@ -189,7 +190,7 @@ This section documents the protections implemented by `codex-network-proxy`, and what it can reasonably guarantee. - Allowlist-first policy: if `allowed_domains` is empty, requests are blocked until an allowlist is configured. -- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported; the global `*` wildcard is rejected. +- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported. A global `*` wildcard is allowed in `allowed_domains` to permit all public hosts by default, while `denied_domains` remains field-specific and still rejects global `*`. - Deny wins: entries in `denied_domains` always override the allowlist. - Local/private network protection: when `allow_local_binding = false`, the proxy blocks loopback and common private/link-local ranges. Explicit allowlisting of local IP literals (or `localhost`) diff --git a/codex-rs/network-proxy/src/policy.rs b/codex-rs/network-proxy/src/policy.rs index 17927767f9f..f1efb61e252 100644 --- a/codex-rs/network-proxy/src/policy.rs +++ b/codex-rs/network-proxy/src/policy.rs @@ -151,12 +151,12 @@ pub(crate) fn is_global_wildcard_domain_pattern(pattern: &str) -> bool { .any(|candidate| candidate == "*") } -pub(crate) fn compile_globset(patterns: &[String]) -> Result { +pub(crate) fn compile_globset(patterns: &[String], allow_global_wildcard: bool) -> Result { let mut builder = GlobSetBuilder::new(); let mut seen = HashSet::new(); for pattern in patterns { ensure!( - !is_global_wildcard_domain_pattern(pattern), + allow_global_wildcard || !is_global_wildcard_domain_pattern(pattern), "unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com" ); let pattern = normalize_pattern(pattern); @@ -164,6 +164,7 @@ pub(crate) fn compile_globset(patterns: &[String]) -> Result { // - "example.com": match the exact host // - "*.example.com": match any subdomain (not the apex) // - "**.example.com": match the apex and any subdomain + // - "*": match every host when explicitly enabled for allowlist compilation for candidate in expand_domain_pattern(&pattern) { if !seen.insert(candidate.clone()) { continue; @@ -333,7 +334,11 @@ mod tests { #[test] fn compile_globset_normalizes_trailing_dots() { - let set = compile_globset(&["Example.COM.".to_string()]).unwrap(); + let set = compile_globset( + &["Example.COM.".to_string()], + /*allow_global_wildcard*/ false, + ) + .unwrap(); assert_eq!(true, set.is_match("example.com")); assert_eq!(false, set.is_match("api.example.com")); @@ -341,7 +346,11 @@ mod tests { #[test] fn compile_globset_normalizes_wildcards() { - let set = compile_globset(&["*.Example.COM.".to_string()]).unwrap(); + let set = compile_globset( + &["*.Example.COM.".to_string()], + /*allow_global_wildcard*/ false, + ) + .unwrap(); assert_eq!(true, set.is_match("api.example.com")); assert_eq!(false, set.is_match("example.com")); @@ -349,7 +358,11 @@ mod tests { #[test] fn compile_globset_normalizes_apex_and_subdomains() { - let set = compile_globset(&["**.Example.COM.".to_string()]).unwrap(); + let set = compile_globset( + &["**.Example.COM.".to_string()], + /*allow_global_wildcard*/ false, + ) + .unwrap(); assert_eq!(true, set.is_match("example.com")); assert_eq!(true, set.is_match("api.example.com")); @@ -357,7 +370,7 @@ mod tests { #[test] fn compile_globset_normalizes_bracketed_ipv6_literals() { - let set = compile_globset(&["[::1]".to_string()]).unwrap(); + let set = compile_globset(&["[::1]".to_string()], /*allow_global_wildcard*/ false).unwrap(); assert_eq!(true, set.is_match("::1")); } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index b634a8630e3..c23fcbab520 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -1089,6 +1089,28 @@ mod tests { ); } + #[tokio::test] + async fn host_blocked_global_wildcard_allowlist_allows_public_hosts_except_denylist() { + let state = network_proxy_state_for_policy(NetworkProxySettings { + allowed_domains: vec!["*".to_string()], + denied_domains: vec!["evil.example".to_string()], + ..NetworkProxySettings::default() + }); + + assert_eq!( + state.host_blocked("example.com", 80).await.unwrap(), + HostBlockDecision::Allowed + ); + assert_eq!( + state.host_blocked("api.openai.com", 443).await.unwrap(), + HostBlockDecision::Allowed + ); + assert_eq!( + state.host_blocked("evil.example", 80).await.unwrap(), + HostBlockDecision::Blocked(HostBlockReason::Denied) + ); + } + #[tokio::test] async fn host_blocked_rejects_loopback_when_local_binding_disabled() { let state = network_proxy_state_for_policy(NetworkProxySettings { @@ -1484,7 +1506,7 @@ mod tests { #[test] fn compile_globset_is_case_insensitive() { let patterns = vec!["ExAmPle.CoM".to_string()]; - let set = compile_globset(&patterns).unwrap(); + let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); assert!(set.is_match("example.com")); assert!(set.is_match("EXAMPLE.COM")); } @@ -1492,7 +1514,7 @@ mod tests { #[test] fn compile_globset_excludes_apex_for_subdomain_patterns() { let patterns = vec!["*.openai.com".to_string()]; - let set = compile_globset(&patterns).unwrap(); + let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); assert!(set.is_match("api.openai.com")); assert!(!set.is_match("openai.com")); assert!(!set.is_match("evilopenai.com")); @@ -1501,7 +1523,7 @@ mod tests { #[test] fn compile_globset_includes_apex_for_double_wildcard_patterns() { let patterns = vec!["**.openai.com".to_string()]; - let set = compile_globset(&patterns).unwrap(); + let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); assert!(set.is_match("openai.com")); assert!(set.is_match("api.openai.com")); assert!(!set.is_match("evilopenai.com")); @@ -1510,25 +1532,34 @@ mod tests { #[test] fn compile_globset_rejects_global_wildcard() { let patterns = vec!["*".to_string()]; - assert!(compile_globset(&patterns).is_err()); + assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); + } + + #[test] + fn compile_globset_allows_global_wildcard_when_enabled() { + let patterns = vec!["*".to_string()]; + let set = compile_globset(&patterns, /*allow_global_wildcard*/ true).unwrap(); + assert!(set.is_match("example.com")); + assert!(set.is_match("api.openai.com")); + assert!(set.is_match("localhost")); } #[test] fn compile_globset_rejects_bracketed_global_wildcard() { let patterns = vec!["[*]".to_string()]; - assert!(compile_globset(&patterns).is_err()); + assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); } #[test] fn compile_globset_rejects_double_wildcard_bracketed_global_wildcard() { let patterns = vec!["**.[*]".to_string()]; - assert!(compile_globset(&patterns).is_err()); + assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); } #[test] fn compile_globset_dedupes_patterns_without_changing_behavior() { let patterns = vec!["example.com".to_string(), "example.com".to_string()]; - let set = compile_globset(&patterns).unwrap(); + let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); assert!(set.is_match("example.com")); assert!(set.is_match("EXAMPLE.COM")); assert!(!set.is_match("not-example.com")); @@ -1537,11 +1568,11 @@ mod tests { #[test] fn compile_globset_rejects_invalid_patterns() { let patterns = vec!["[".to_string()]; - assert!(compile_globset(&patterns).is_err()); + assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); } #[test] - fn build_config_state_rejects_global_wildcard_allowed_domains() { + fn build_config_state_allows_global_wildcard_allowed_domains() { let config = NetworkProxyConfig { network: NetworkProxySettings { enabled: true, @@ -1550,11 +1581,11 @@ mod tests { }, }; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err()); + assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok()); } #[test] - fn build_config_state_rejects_bracketed_global_wildcard_allowed_domains() { + fn build_config_state_allows_bracketed_global_wildcard_allowed_domains() { let config = NetworkProxyConfig { network: NetworkProxySettings { enabled: true, @@ -1563,7 +1594,7 @@ mod tests { }, }; - assert!(build_config_state(config, NetworkProxyConstraints::default()).is_err()); + assert!(build_config_state(config, NetworkProxyConstraints::default()).is_ok()); } #[test] diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 612e6c5b5cf..fd314f1e7b0 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -59,12 +59,26 @@ pub fn build_config_state( constraints: NetworkProxyConstraints, ) -> anyhow::Result { crate::config::validate_unix_socket_allowlist_paths(&config)?; - validate_domain_patterns("network.allowed_domains", &config.network.allowed_domains) - .map_err(NetworkProxyConstraintError::into_anyhow)?; - validate_domain_patterns("network.denied_domains", &config.network.denied_domains) - .map_err(NetworkProxyConstraintError::into_anyhow)?; - let deny_set = compile_globset(&config.network.denied_domains)?; - let allow_set = compile_globset(&config.network.allowed_domains)?; + validate_domain_patterns( + "network.allowed_domains", + &config.network.allowed_domains, + /*allow_global_wildcard*/ true, + ) + .map_err(NetworkProxyConstraintError::into_anyhow)?; + validate_domain_patterns( + "network.denied_domains", + &config.network.denied_domains, + /*allow_global_wildcard*/ false, + ) + .map_err(NetworkProxyConstraintError::into_anyhow)?; + let deny_set = compile_globset( + &config.network.denied_domains, + /*allow_global_wildcard*/ false, + )?; + let allow_set = compile_globset( + &config.network.allowed_domains, + /*allow_global_wildcard*/ true, + )?; let mitm = if config.network.mitm { Some(Arc::new(MitmState::new( config.network.allow_upstream_proxy, @@ -107,8 +121,16 @@ pub fn validate_policy_against_constraints( } let enabled = config.network.enabled; - validate_domain_patterns("network.allowed_domains", &config.network.allowed_domains)?; - validate_domain_patterns("network.denied_domains", &config.network.denied_domains)?; + validate_domain_patterns( + "network.allowed_domains", + &config.network.allowed_domains, + /*allow_global_wildcard*/ true, + )?; + validate_domain_patterns( + "network.denied_domains", + &config.network.denied_domains, + /*allow_global_wildcard*/ false, + )?; if let Some(max_enabled) = constraints.enabled { validate(enabled, move |candidate| { if *candidate && !max_enabled { @@ -208,7 +230,11 @@ pub fn validate_policy_against_constraints( } if let Some(allowed_domains) = &constraints.allowed_domains { - validate_domain_patterns("network.allowed_domains", allowed_domains)?; + validate_domain_patterns( + "network.allowed_domains", + allowed_domains, + /*allow_global_wildcard*/ true, + )?; match constraints.allowlist_expansion_enabled { Some(true) => { let required_set: HashSet = allowed_domains @@ -288,7 +314,11 @@ pub fn validate_policy_against_constraints( } if let Some(denied_domains) = &constraints.denied_domains { - validate_domain_patterns("network.denied_domains", denied_domains)?; + validate_domain_patterns( + "network.denied_domains", + denied_domains, + /*allow_global_wildcard*/ false, + )?; let required_set: HashSet = denied_domains .iter() .map(|s| s.to_ascii_lowercase()) @@ -367,10 +397,12 @@ pub fn validate_policy_against_constraints( fn validate_domain_patterns( field_name: &'static str, patterns: &[String], + allow_global_wildcard: bool, ) -> Result<(), NetworkProxyConstraintError> { - if let Some(pattern) = patterns - .iter() - .find(|pattern| is_global_wildcard_domain_pattern(pattern)) + if !allow_global_wildcard + && let Some(pattern) = patterns + .iter() + .find(|pattern| is_global_wildcard_domain_pattern(pattern)) { return Err(NetworkProxyConstraintError::InvalidValue { field_name, From fe2863d572607569abc4d32e8f913fdf10d0142e Mon Sep 17 00:00:00 2001 From: rreichel3-oai Date: Mon, 23 Mar 2026 16:05:41 -0400 Subject: [PATCH 2/5] Cover denylist updates with global allow wildcard Add a network-proxy regression test that starts from allowed_domains = ["*"], adds a new denied host that has no exact allowlist entry, and verifies the host is then blocked while the global allowlist remains. --- codex-rs/network-proxy/src/runtime.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index c23fcbab520..16854f686ac 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -894,6 +894,29 @@ mod tests { ); } + #[tokio::test] + async fn add_denied_domain_forces_block_with_global_wildcard_allowlist() { + let state = network_proxy_state_for_policy(NetworkProxySettings { + allowed_domains: vec!["*".to_string()], + ..NetworkProxySettings::default() + }); + + assert_eq!( + state.host_blocked("evil.example", 80).await.unwrap(), + HostBlockDecision::Allowed + ); + + state.add_denied_domain("evil.example").await.unwrap(); + + let (allowed, denied) = state.current_patterns().await.unwrap(); + assert_eq!(allowed, vec!["*".to_string()]); + assert_eq!(denied, vec!["evil.example".to_string()]); + assert_eq!( + state.host_blocked("evil.example", 80).await.unwrap(), + HostBlockDecision::Blocked(HostBlockReason::Denied) + ); + } + #[tokio::test] async fn add_allowed_domain_succeeds_when_managed_baseline_allows_expansion() { let config = NetworkProxyConfig { From 745c593b57a21f47c0df10b88fdd94c5f35f35f9 Mon Sep 17 00:00:00 2001 From: rreichel3-oai Date: Mon, 23 Mar 2026 16:20:17 -0400 Subject: [PATCH 3/5] Replace wildcard mode bool with named helpers Use allowlist- and denylist-specific glob compilation helpers instead of a positional boolean, and keep wildcard rejection validation only on the denylist path. --- codex-rs/network-proxy/src/policy.rs | 42 +++++++++++-------- codex-rs/network-proxy/src/runtime.rs | 21 +++++----- codex-rs/network-proxy/src/state.rs | 59 ++++++--------------------- 3 files changed, 47 insertions(+), 75 deletions(-) diff --git a/codex-rs/network-proxy/src/policy.rs b/codex-rs/network-proxy/src/policy.rs index f1efb61e252..5b44cc9e2a3 100644 --- a/codex-rs/network-proxy/src/policy.rs +++ b/codex-rs/network-proxy/src/policy.rs @@ -151,12 +151,30 @@ pub(crate) fn is_global_wildcard_domain_pattern(pattern: &str) -> bool { .any(|candidate| candidate == "*") } -pub(crate) fn compile_globset(patterns: &[String], allow_global_wildcard: bool) -> Result { +#[derive(Clone, Copy)] +enum GlobalWildcard { + Allow, + Reject, +} + +pub(crate) fn compile_allowlist_globset(patterns: &[String]) -> Result { + compile_globset_with_policy(patterns, GlobalWildcard::Allow) +} + +pub(crate) fn compile_denylist_globset(patterns: &[String]) -> Result { + compile_globset_with_policy(patterns, GlobalWildcard::Reject) +} + +fn compile_globset_with_policy( + patterns: &[String], + global_wildcard: GlobalWildcard, +) -> Result { let mut builder = GlobSetBuilder::new(); let mut seen = HashSet::new(); for pattern in patterns { ensure!( - allow_global_wildcard || !is_global_wildcard_domain_pattern(pattern), + matches!(global_wildcard, GlobalWildcard::Allow) + || !is_global_wildcard_domain_pattern(pattern), "unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com" ); let pattern = normalize_pattern(pattern); @@ -334,11 +352,7 @@ mod tests { #[test] fn compile_globset_normalizes_trailing_dots() { - let set = compile_globset( - &["Example.COM.".to_string()], - /*allow_global_wildcard*/ false, - ) - .unwrap(); + let set = compile_denylist_globset(&["Example.COM.".to_string()]).unwrap(); assert_eq!(true, set.is_match("example.com")); assert_eq!(false, set.is_match("api.example.com")); @@ -346,11 +360,7 @@ mod tests { #[test] fn compile_globset_normalizes_wildcards() { - let set = compile_globset( - &["*.Example.COM.".to_string()], - /*allow_global_wildcard*/ false, - ) - .unwrap(); + let set = compile_denylist_globset(&["*.Example.COM.".to_string()]).unwrap(); assert_eq!(true, set.is_match("api.example.com")); assert_eq!(false, set.is_match("example.com")); @@ -358,11 +368,7 @@ mod tests { #[test] fn compile_globset_normalizes_apex_and_subdomains() { - let set = compile_globset( - &["**.Example.COM.".to_string()], - /*allow_global_wildcard*/ false, - ) - .unwrap(); + let set = compile_denylist_globset(&["**.Example.COM.".to_string()]).unwrap(); assert_eq!(true, set.is_match("example.com")); assert_eq!(true, set.is_match("api.example.com")); @@ -370,7 +376,7 @@ mod tests { #[test] fn compile_globset_normalizes_bracketed_ipv6_literals() { - let set = compile_globset(&["[::1]".to_string()], /*allow_global_wildcard*/ false).unwrap(); + let set = compile_denylist_globset(&["[::1]".to_string()]).unwrap(); assert_eq!(true, set.is_match("::1")); } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 16854f686ac..9381fab7f42 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -819,7 +819,8 @@ mod tests { use crate::config::NetworkProxyConfig; use crate::config::NetworkProxySettings; - use crate::policy::compile_globset; + use crate::policy::compile_allowlist_globset; + use crate::policy::compile_denylist_globset; use crate::state::NetworkProxyConstraints; use crate::state::build_config_state; use crate::state::validate_policy_against_constraints; @@ -1529,7 +1530,7 @@ mod tests { #[test] fn compile_globset_is_case_insensitive() { let patterns = vec!["ExAmPle.CoM".to_string()]; - let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); + let set = compile_denylist_globset(&patterns).unwrap(); assert!(set.is_match("example.com")); assert!(set.is_match("EXAMPLE.COM")); } @@ -1537,7 +1538,7 @@ mod tests { #[test] fn compile_globset_excludes_apex_for_subdomain_patterns() { let patterns = vec!["*.openai.com".to_string()]; - let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); + let set = compile_denylist_globset(&patterns).unwrap(); assert!(set.is_match("api.openai.com")); assert!(!set.is_match("openai.com")); assert!(!set.is_match("evilopenai.com")); @@ -1546,7 +1547,7 @@ mod tests { #[test] fn compile_globset_includes_apex_for_double_wildcard_patterns() { let patterns = vec!["**.openai.com".to_string()]; - let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); + let set = compile_denylist_globset(&patterns).unwrap(); assert!(set.is_match("openai.com")); assert!(set.is_match("api.openai.com")); assert!(!set.is_match("evilopenai.com")); @@ -1555,13 +1556,13 @@ mod tests { #[test] fn compile_globset_rejects_global_wildcard() { let patterns = vec!["*".to_string()]; - assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); + assert!(compile_denylist_globset(&patterns).is_err()); } #[test] fn compile_globset_allows_global_wildcard_when_enabled() { let patterns = vec!["*".to_string()]; - let set = compile_globset(&patterns, /*allow_global_wildcard*/ true).unwrap(); + let set = compile_allowlist_globset(&patterns).unwrap(); assert!(set.is_match("example.com")); assert!(set.is_match("api.openai.com")); assert!(set.is_match("localhost")); @@ -1570,19 +1571,19 @@ mod tests { #[test] fn compile_globset_rejects_bracketed_global_wildcard() { let patterns = vec!["[*]".to_string()]; - assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); + assert!(compile_denylist_globset(&patterns).is_err()); } #[test] fn compile_globset_rejects_double_wildcard_bracketed_global_wildcard() { let patterns = vec!["**.[*]".to_string()]; - assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); + assert!(compile_denylist_globset(&patterns).is_err()); } #[test] fn compile_globset_dedupes_patterns_without_changing_behavior() { let patterns = vec!["example.com".to_string(), "example.com".to_string()]; - let set = compile_globset(&patterns, /*allow_global_wildcard*/ false).unwrap(); + let set = compile_denylist_globset(&patterns).unwrap(); assert!(set.is_match("example.com")); assert!(set.is_match("EXAMPLE.COM")); assert!(!set.is_match("not-example.com")); @@ -1591,7 +1592,7 @@ mod tests { #[test] fn compile_globset_rejects_invalid_patterns() { let patterns = vec!["[".to_string()]; - assert!(compile_globset(&patterns, /*allow_global_wildcard*/ false).is_err()); + assert!(compile_denylist_globset(&patterns).is_err()); } #[test] diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index fd314f1e7b0..435e5beab45 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -2,7 +2,8 @@ use crate::config::NetworkMode; use crate::config::NetworkProxyConfig; use crate::mitm::MitmState; use crate::policy::DomainPattern; -use crate::policy::compile_globset; +use crate::policy::compile_allowlist_globset; +use crate::policy::compile_denylist_globset; use crate::policy::is_global_wildcard_domain_pattern; use crate::runtime::ConfigState; use serde::Deserialize; @@ -59,26 +60,10 @@ pub fn build_config_state( constraints: NetworkProxyConstraints, ) -> anyhow::Result { crate::config::validate_unix_socket_allowlist_paths(&config)?; - validate_domain_patterns( - "network.allowed_domains", - &config.network.allowed_domains, - /*allow_global_wildcard*/ true, - ) - .map_err(NetworkProxyConstraintError::into_anyhow)?; - validate_domain_patterns( - "network.denied_domains", - &config.network.denied_domains, - /*allow_global_wildcard*/ false, - ) - .map_err(NetworkProxyConstraintError::into_anyhow)?; - let deny_set = compile_globset( - &config.network.denied_domains, - /*allow_global_wildcard*/ false, - )?; - let allow_set = compile_globset( - &config.network.allowed_domains, - /*allow_global_wildcard*/ true, - )?; + validate_denylist_domain_patterns("network.denied_domains", &config.network.denied_domains) + .map_err(NetworkProxyConstraintError::into_anyhow)?; + let deny_set = compile_denylist_globset(&config.network.denied_domains)?; + let allow_set = compile_allowlist_globset(&config.network.allowed_domains)?; let mitm = if config.network.mitm { Some(Arc::new(MitmState::new( config.network.allow_upstream_proxy, @@ -121,16 +106,7 @@ pub fn validate_policy_against_constraints( } let enabled = config.network.enabled; - validate_domain_patterns( - "network.allowed_domains", - &config.network.allowed_domains, - /*allow_global_wildcard*/ true, - )?; - validate_domain_patterns( - "network.denied_domains", - &config.network.denied_domains, - /*allow_global_wildcard*/ false, - )?; + validate_denylist_domain_patterns("network.denied_domains", &config.network.denied_domains)?; if let Some(max_enabled) = constraints.enabled { validate(enabled, move |candidate| { if *candidate && !max_enabled { @@ -230,11 +206,6 @@ pub fn validate_policy_against_constraints( } if let Some(allowed_domains) = &constraints.allowed_domains { - validate_domain_patterns( - "network.allowed_domains", - allowed_domains, - /*allow_global_wildcard*/ true, - )?; match constraints.allowlist_expansion_enabled { Some(true) => { let required_set: HashSet = allowed_domains @@ -314,11 +285,7 @@ pub fn validate_policy_against_constraints( } if let Some(denied_domains) = &constraints.denied_domains { - validate_domain_patterns( - "network.denied_domains", - denied_domains, - /*allow_global_wildcard*/ false, - )?; + validate_denylist_domain_patterns("network.denied_domains", denied_domains)?; let required_set: HashSet = denied_domains .iter() .map(|s| s.to_ascii_lowercase()) @@ -394,15 +361,13 @@ pub fn validate_policy_against_constraints( Ok(()) } -fn validate_domain_patterns( +fn validate_denylist_domain_patterns( field_name: &'static str, patterns: &[String], - allow_global_wildcard: bool, ) -> Result<(), NetworkProxyConstraintError> { - if !allow_global_wildcard - && let Some(pattern) = patterns - .iter() - .find(|pattern| is_global_wildcard_domain_pattern(pattern)) + if let Some(pattern) = patterns + .iter() + .find(|pattern| is_global_wildcard_domain_pattern(pattern)) { return Err(NetworkProxyConstraintError::InvalidValue { field_name, From 5449aa0512a90d322cf5109fe011f77e527d3f35 Mon Sep 17 00:00:00 2001 From: rreichel3-oai Date: Mon, 23 Mar 2026 17:16:30 -0400 Subject: [PATCH 4/5] Clarify global wildcard rejection branch Apply review feedback in network-proxy glob compilation by spelling the reject case as an explicit if/return Err branch instead of a negated ensure condition. --- codex-rs/network-proxy/src/policy.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/codex-rs/network-proxy/src/policy.rs b/codex-rs/network-proxy/src/policy.rs index 5b44cc9e2a3..c586ee9c11e 100644 --- a/codex-rs/network-proxy/src/policy.rs +++ b/codex-rs/network-proxy/src/policy.rs @@ -2,6 +2,7 @@ use crate::config::NetworkMode; use anyhow::Context; use anyhow::Result; +use anyhow::bail; use anyhow::ensure; use globset::GlobBuilder; use globset::GlobSet; @@ -151,7 +152,7 @@ pub(crate) fn is_global_wildcard_domain_pattern(pattern: &str) -> bool { .any(|candidate| candidate == "*") } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] enum GlobalWildcard { Allow, Reject, @@ -172,11 +173,11 @@ fn compile_globset_with_policy( let mut builder = GlobSetBuilder::new(); let mut seen = HashSet::new(); for pattern in patterns { - ensure!( - matches!(global_wildcard, GlobalWildcard::Allow) - || !is_global_wildcard_domain_pattern(pattern), - "unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com" - ); + if global_wildcard == GlobalWildcard::Reject && is_global_wildcard_domain_pattern(pattern) { + bail!( + "unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com" + ); + } let pattern = normalize_pattern(pattern); // Supported domain patterns: // - "example.com": match the exact host From e3f593c59569d663023b0cc77e57e933b954fea9 Mon Sep 17 00:00:00 2001 From: rreichel3-oai Date: Tue, 24 Mar 2026 09:48:08 -0400 Subject: [PATCH 5/5] Increase rust-ci test timeout to 40 minutes Raise the rust-ci tests job timeout from 30 to 40 minutes so slower Windows ARM runs have more headroom before GitHub cancels the job. --- .github/workflows/rust-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 9db72b1d21a..277f9dba707 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -547,7 +547,7 @@ jobs: tests: name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }} runs-on: ${{ matrix.runs_on || matrix.runner }} - timeout-minutes: 30 + timeout-minutes: 40 needs: changed if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }} defaults: