diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index 912073b1b4..a959faf4a5 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -178,6 +178,13 @@ fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProx if let Some(allow_local_binding) = network.allow_local_binding { constraints.allow_local_binding = Some(allow_local_binding); } + if let Some(dangerously_allow_plaintext_credential_injection) = network + .mitm + .and_then(|mitm| mitm.dangerously_allow_plaintext_credential_injection) + { + constraints.dangerously_allow_plaintext_credential_injection = + Some(dangerously_allow_plaintext_credential_injection); + } } #[derive(Debug, Clone, Default, Deserialize)] diff --git a/codex-rs/core/src/network_proxy_loader_tests.rs b/codex-rs/core/src/network_proxy_loader_tests.rs index 879d826920..654329842c 100644 --- a/codex-rs/core/src/network_proxy_loader_tests.rs +++ b/codex-rs/core/src/network_proxy_loader_tests.rs @@ -214,6 +214,55 @@ dangerously_allow_plaintext_credential_injection = true ); } +#[test] +fn managed_config_can_disable_plaintext_credential_injection() { + let managed_layer = ConfigLayerEntry::new( + ConfigLayerSource::System { + file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/system.toml")) + .expect("system config path should be absolute"), + }, + toml::toml! { + default_permissions = "workspace" + + [permissions.workspace.network.mitm] + dangerously_allow_plaintext_credential_injection = false + } + .into(), + ); + let user_layer = ConfigLayerEntry::new( + ConfigLayerSource::User { + file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/config.toml")) + .expect("user config path should be absolute"), + profile: None, + }, + toml::toml! { + [permissions.workspace.network.mitm] + dangerously_allow_plaintext_credential_injection = true + } + .into(), + ); + let layers = ConfigLayerStack::new( + vec![managed_layer, user_layer], + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + ) + .expect("layer stack should be valid"); + let config = config_from_layers(&layers, &Policy::empty()) + .expect("merged network proxy config should load"); + + assert!( + config + .network + .dangerously_allow_plaintext_credential_injection + ); + let err = enforce_trusted_constraints(&layers, &config) + .expect_err("managed config should disable plaintext credential injection"); + assert_eq!( + format!("{err:#}"), + "network proxy constraints: invalid value for network.dangerously_allow_plaintext_credential_injection: true (allowed false (disabled by managed config))" + ); +} + #[test] fn execpolicy_network_rules_overlay_network_lists() { let mut config = NetworkProxyConfig::default(); diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 9a5287dac4..024177895a 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -30,6 +30,7 @@ pub struct NetworkProxyConstraints { pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_all_unix_sockets: Option, + pub dangerously_allow_plaintext_credential_injection: Option, pub allowed_domains: Option>, pub allowlist_expansion_enabled: Option, pub denied_domains: Option>, @@ -198,6 +199,27 @@ pub fn validate_policy_against_constraints( }, )?; + if let Some(allow_plaintext_credential_injection) = + constraints.dangerously_allow_plaintext_credential_injection + { + validate( + config + .network + .dangerously_allow_plaintext_credential_injection, + move |candidate| { + if *candidate && !allow_plaintext_credential_injection { + Err(invalid_value( + "network.dangerously_allow_plaintext_credential_injection", + "true", + "false (disabled by managed config)", + )) + } else { + Ok(()) + } + }, + )?; + } + let allow_all_unix_sockets = constraints .dangerously_allow_all_unix_sockets .unwrap_or(constraints.allow_unix_sockets.is_none());