Support header injections in network requirements (#42173)

## What changed

- Parse `experimental_network.header_injections` rules with host, method, path-prefix, and header mappings and carry them into `NetworkConstraints`.
- Export the rule type from `codex_config`.
- Summarize configured rule counts and hosts in the TUI debug configuration without exposing header values.

## Testing

- Cover managed configuration parsing and redaction of header values from debug output.

GitOrigin-RevId: 623b047e6c6111a9f2ccbe79c9ace054631a3c67
This commit is contained in:
andrewhuang-oai
2026-09-02 01:50:51 +00:00
committed by copyberry
parent 12ed76b09b
commit 671d5d1b35
5 changed files with 80 additions and 3 deletions
@@ -428,6 +428,28 @@ pub struct NetworkRequirementsToml {
pub managed_allowed_domains_only: Option<bool>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
pub allow_local_binding: Option<bool>,
/// Requirements-only header injections. These annotate matching requests
/// without changing whether non-matching requests are allowed.
pub header_injections: Option<Vec<NetworkHeaderInjectionToml>>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct NetworkHeaderInjectionToml {
pub host: String,
pub methods: Vec<String>,
pub path_prefixes: Vec<String>,
pub headers: BTreeMap<String, String>,
}
impl std::fmt::Debug for NetworkHeaderInjectionToml {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NetworkHeaderInjectionToml")
.field("host", &self.host)
.field("methods", &self.methods)
.field("path_prefixes", &self.path_prefixes)
.field("header_names", &self.headers.keys().collect::<Vec<_>>())
.finish()
}
}
#[derive(Deserialize)]
@@ -450,6 +472,7 @@ struct RawNetworkRequirementsToml {
#[serde(default)]
allow_unix_sockets: Option<Vec<String>>,
allow_local_binding: Option<bool>,
header_injections: Option<Vec<NetworkHeaderInjectionToml>>,
}
impl<'de> Deserialize<'de> for NetworkRequirementsToml {
@@ -472,6 +495,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml {
unix_sockets,
allow_unix_sockets,
allow_local_binding,
header_injections,
} = raw;
if domains.is_some() && (allowed_domains.is_some() || denied_domains.is_some()) {
@@ -499,6 +523,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml {
unix_sockets: unix_sockets
.or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)),
allow_local_binding,
header_injections,
})
}
}
@@ -550,6 +575,7 @@ pub struct NetworkConstraints {
pub managed_allowed_domains_only: Option<bool>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
pub allow_local_binding: Option<bool>,
pub header_injections: Option<Vec<NetworkHeaderInjectionToml>>,
}
impl<'de> Deserialize<'de> for NetworkConstraints {
@@ -575,6 +601,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
managed_allowed_domains_only,
unix_sockets,
allow_local_binding,
header_injections,
} = value;
Self {
enabled,
@@ -587,6 +614,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
managed_allowed_domains_only,
unix_sockets,
allow_local_binding,
header_injections,
}
}
}
@@ -4231,6 +4259,14 @@ command = "python3 /enterprise/hooks/pre.py"
[experimental_network.unix_sockets]
"/tmp/example.sock" = "allow"
"/tmp/blocked.sock" = "deny"
[[experimental_network.header_injections]]
host = "api.example.com"
methods = ["POST"]
path_prefixes = ["/console/v1"]
[experimental_network.header_injections.headers]
"x-statsig-change-source" = "codex"
"#;
let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
@@ -4288,6 +4324,21 @@ command = "python3 /enterprise/hooks/pre.py"
})
);
assert_eq!(sourced_network.value.allow_local_binding, Some(false));
assert_eq!(
sourced_network.value.header_injections,
Some(vec![NetworkHeaderInjectionToml {
host: "api.example.com".to_string(),
methods: vec!["POST".to_string()],
path_prefixes: vec!["/console/v1".to_string()],
headers: BTreeMap::from([(
"x-statsig-change-source".to_string(),
"codex".to_string(),
)]),
}])
);
let debug = format!("{:?}", sourced_network.value.header_injections);
assert!(debug.contains("x-statsig-change-source"));
assert!(!debug.contains("codex"));
Ok(())
}
+1
View File
@@ -102,6 +102,7 @@ pub use config_requirements::ModelsRequirementsToml;
pub use config_requirements::NetworkConstraints;
pub use config_requirements::NetworkDomainPermissionToml;
pub use config_requirements::NetworkDomainPermissionsToml;
pub use config_requirements::NetworkHeaderInjectionToml;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::NetworkUnixSocketPermissionToml;
pub use config_requirements::NetworkUnixSocketPermissionsToml;
@@ -3065,7 +3065,7 @@ fn fresh_session_applies_requested_name() -> Result<()> {
#[test]
fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;
const TEST_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
std::thread::Builder::new()
.name("tui-session-lifecycle-requests".to_string())
+26 -1
View File
@@ -568,6 +568,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
managed_allowed_domains_only,
unix_sockets,
allow_local_binding,
header_injections,
} = network;
if let Some(enabled) = enabled {
@@ -615,6 +616,19 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
if let Some(allow_local_binding) = allow_local_binding {
parts.push(format!("allow_local_binding={allow_local_binding}"));
}
if let Some(header_injections) = header_injections {
let hosts = header_injections
.iter()
.map(|rule| rule.host.as_str())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join(", ");
parts.push(format!(
"header_injections={} hosts={{{hosts}}}",
header_injections.len()
));
}
join_or_empty(parts)
}
@@ -675,6 +689,7 @@ mod tests {
use codex_config::NetworkConstraints;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_config::NetworkHeaderInjectionToml;
use codex_config::NetworkUnixSocketPermissionToml;
use codex_config::NetworkUnixSocketPermissionsToml;
use codex_config::RequirementSource;
@@ -914,6 +929,15 @@ interrupt_message = false
NetworkDomainPermissionToml::Allow,
)]),
}),
header_injections: Some(vec![NetworkHeaderInjectionToml {
host: "api.example.com".to_string(),
methods: vec!["POST".to_string()],
path_prefixes: vec!["/v1".to_string()],
headers: BTreeMap::from([(
"x-managed-source".to_string(),
"secret-looking-value".to_string(),
)]),
}]),
..Default::default()
},
RequirementSource::LegacyManagedConfigTomlFromMdm,
@@ -1047,8 +1071,9 @@ interrupt_message = false
"enforce_residency: us (source: {requirements_source})"
)));
assert!(rendered.contains(&format!(
"experimental_network: enabled=true, domains={{example.com=allow}} (source: {requirements_source})"
"experimental_network: enabled=true, domains={{example.com=allow}}, header_injections=1 hosts={{api.example.com}} (source: {requirements_source})"
)));
assert!(!rendered.contains("secret-looking-value"));
assert!(
rendered.contains(
format!(
@@ -26,5 +26,5 @@ Requirements:
- features: guardian_approval=true (source: MDM managed_config.toml (legacy))
- mcp_servers: docs (source: MDM managed_config.toml (legacy))
- enforce_residency: us (source: MDM managed_config.toml (legacy))
- experimental_network: enabled=true, domains={example.com=allow} (source: MDM managed_config.toml (legacy))
- experimental_network: enabled=true, domains={example.com=allow}, header_injections=1 hosts={api.example.com} (source: MDM managed_config.toml (legacy))
- permissions.filesystem.deny_read: /home/alice/.gitconfig (source: /etc/codex/requirements.toml)