From f74951488bbfa0afd66ccad2e99fa55c2146096f Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Sat, 16 May 2026 01:34:43 -0700 Subject: [PATCH] Preserve trusted MITM hook constraints --- codex-rs/core/src/network_proxy_loader.rs | 7 +- .../core/src/network_proxy_loader_tests.rs | 66 +++++++++++++++++++ codex-rs/network-proxy/src/runtime.rs | 55 ++++++++++++++++ codex-rs/network-proxy/src/state.rs | 13 ++++ 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index c40f56b4e9..bce5094620 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -122,6 +122,7 @@ fn network_constraints_from_trusted_layers( layers: &ConfigLayerStack, ) -> Result { let mut constraints = NetworkProxyConstraints::default(); + let mut trusted_network = NetworkConfigAccumulator::default(); for layer in layers.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, @@ -132,9 +133,13 @@ fn network_constraints_from_trusted_layers( let parsed = network_tables_from_toml(&layer.config)?; if let Some(network) = selected_network_from_tables(parsed)? { - apply_network_constraints(network, &mut constraints); + apply_network_constraints(network.clone(), &mut constraints); + trusted_network.apply_network(network); } } + let trusted_mitm_hooks = trusted_network.finish()?.network.mitm_hooks; + constraints.required_mitm_hook_prefix = + (!trusted_mitm_hooks.is_empty()).then_some(trusted_mitm_hooks); Ok(constraints) } diff --git a/codex-rs/core/src/network_proxy_loader_tests.rs b/codex-rs/core/src/network_proxy_loader_tests.rs index b54f387370..68c8dafa9d 100644 --- a/codex-rs/core/src/network_proxy_loader_tests.rs +++ b/codex-rs/core/src/network_proxy_loader_tests.rs @@ -174,6 +174,72 @@ strip_request_headers = ["x-api-key"] ); } +#[test] +fn trusted_named_mitm_actions_become_required_hook_constraints() { + let lower_network: toml::Value = toml::from_str( + r#" +default_permissions = "workspace" + +[permissions.workspace.network] +mode = "full" + +[permissions.workspace.network.mitm.hooks.github_write] +host = "api.github.com" +methods = ["POST"] +path_prefixes = ["/repos/openai/"] +action = ["strip_auth"] + +[permissions.workspace.network.mitm.actions.strip_auth] +strip_request_headers = ["authorization"] +"#, + ) + .expect("lower layer should parse"); + let higher_network: toml::Value = toml::from_str( + r#" +default_permissions = "workspace" + +[permissions.workspace.network] +mode = "full" + +[permissions.workspace.network.mitm.actions.strip_auth] +strip_request_headers = ["x-api-key"] +"#, + ) + .expect("higher layer should parse"); + + let mut trusted_network = NetworkConfigAccumulator::default(); + trusted_network + .apply_network_tables( + network_tables_from_toml(&lower_network).expect("lower layer should deserialize"), + ) + .expect("lower layer should apply"); + trusted_network + .apply_network_tables( + network_tables_from_toml(&higher_network).expect("higher layer should deserialize"), + ) + .expect("higher layer should apply"); + + let constraints = NetworkProxyConstraints { + required_mitm_hook_prefix: Some( + trusted_network + .finish() + .expect("trusted network should build") + .network + .mitm_hooks, + ), + ..NetworkProxyConstraints::default() + }; + + assert_eq!( + constraints + .required_mitm_hook_prefix + .expect("trusted hooks should be constrained")[0] + .actions + .strip_request_headers, + vec!["x-api-key"] + ); +} + #[test] fn execpolicy_network_rules_overlay_network_lists() { let mut config = NetworkProxyConfig::default(); diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 60894d5d5b..0b2a88953c 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -1529,6 +1529,61 @@ mod tests { assert!(validate_policy_against_constraints(&config, &constraints).is_err()); } + #[test] + fn validate_policy_against_constraints_preserves_managed_mitm_hooks() { + let managed_hook = crate::mitm_hook::MitmHookConfig { + host: "api.github.com".to_string(), + matcher: crate::mitm_hook::MitmHookMatchConfig { + methods: vec!["POST".to_string()], + path_prefixes: vec!["/repos/openai/".to_string()], + ..crate::mitm_hook::MitmHookMatchConfig::default() + }, + actions: crate::mitm_hook::MitmHookActionsConfig { + strip_request_headers: vec!["authorization".to_string()], + ..crate::mitm_hook::MitmHookActionsConfig::default() + }, + }; + let extra_hook = crate::mitm_hook::MitmHookConfig { + host: "api.example.com".to_string(), + matcher: crate::mitm_hook::MitmHookMatchConfig { + methods: vec!["GET".to_string()], + path_prefixes: vec!["/".to_string()], + ..crate::mitm_hook::MitmHookMatchConfig::default() + }, + ..crate::mitm_hook::MitmHookConfig::default() + }; + let constraints = NetworkProxyConstraints { + required_mitm_hook_prefix: Some(vec![managed_hook.clone()]), + ..NetworkProxyConstraints::default() + }; + + let config = NetworkProxyConfig { + network: NetworkProxySettings { + enabled: true, + mitm: true, + mitm_hooks: vec![managed_hook.clone(), extra_hook], + ..NetworkProxySettings::default() + }, + }; + assert!(validate_policy_against_constraints(&config, &constraints).is_ok()); + + let config = NetworkProxyConfig { + network: NetworkProxySettings { + enabled: true, + mitm: true, + mitm_hooks: vec![crate::mitm_hook::MitmHookConfig { + actions: crate::mitm_hook::MitmHookActionsConfig { + strip_request_headers: vec!["x-api-key".to_string()], + ..crate::mitm_hook::MitmHookActionsConfig::default() + }, + ..managed_hook + }], + ..NetworkProxySettings::default() + }, + }; + assert!(validate_policy_against_constraints(&config, &constraints).is_err()); + } + #[test] fn validate_policy_against_constraints_allows_narrowing_wildcard_allowlist() { let constraints = NetworkProxyConstraints { diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 32cdfab149..0f3b235436 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -36,6 +36,7 @@ pub struct NetworkProxyConstraints { pub denylist_expansion_enabled: Option, pub allow_unix_sockets: Option>, pub allow_local_binding: Option, + pub required_mitm_hook_prefix: Option>, } #[derive(Debug, Clone, Deserialize)] @@ -126,6 +127,18 @@ pub fn validate_policy_against_constraints( let config_allow_unix_sockets = config.network.allow_unix_sockets(); validate_mitm_hook_config(config).map_err(invalid_mitm_hook_configuration)?; validate_non_global_wildcard_domain_patterns("network.denied_domains", &config_denied_domains)?; + if let Some(required_mitm_hook_prefix) = constraints.required_mitm_hook_prefix.as_ref() + && !config + .network + .mitm_hooks + .starts_with(required_mitm_hook_prefix) + { + return Err(invalid_value( + "network.mitm_hooks", + "managed MITM hooks were replaced or reordered", + "managed MITM hooks preserved before lower-trust hooks", + )); + } if let Some(max_enabled) = constraints.enabled { validate(enabled, move |candidate| { if *candidate && !max_enabled {