mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03:00
chore: refactor network permissions to use explicit domain and unix socket rule maps (#15120)
## Summary This PR replaces the legacy network allow/deny list model with explicit rule maps for domains and unix sockets across managed requirements, permissions profiles, the network proxy config, and the app server protocol. Concretely, it: - introduces typed domain (`allow` / `deny`) and unix socket permission (`allow` / `none`) entries instead of separate `allowed_domains`, `denied_domains`, and `allow_unix_sockets` lists - updates config loading, managed requirements merging, and exec-policy overlays to read and upsert rule entries consistently - exposes the new shape through protocol/schema outputs, debug surfaces, and app-server config APIs - rejects the legacy list-based keys and updates docs/tests to reflect the new config format ## Why The previous representation split related network policy across multiple parallel lists, which made merging and overriding rules harder to reason about. Moving to explicit keyed permission maps gives us a single source of truth per host/socket entry, makes allow/deny precedence clearer, and gives protocol consumers access to the full rule state instead of derived projections only. ## Backward Compatibility ### Backward compatible - Managed requirements still accept the legacy `experimental_network.allowed_domains`, `experimental_network.denied_domains`, and `experimental_network.allow_unix_sockets` fields. They are normalized into the new canonical `domains` and `unix_sockets` maps internally. - App-server v2 still deserializes legacy `allowedDomains`, `deniedDomains`, and `allowUnixSockets` payloads, so older clients can continue reading managed network requirements. - App-server v2 responses still populate `allowedDomains`, `deniedDomains`, and `allowUnixSockets` as legacy compatibility views derived from the canonical maps. - `managed_allowed_domains_only` keeps the same behavior after normalization. Legacy managed allowlists still participate in the same enforcement path as canonical `domains` entries. ### Not backward compatible - Permissions profiles under `[permissions.<profile>.network]` no longer accept the legacy list-based keys. Those configs must use the canonical `[domains]` and `[unix_sockets]` tables instead of `allowed_domains`, `denied_domains`, or `allow_unix_sockets`. - Managed `experimental_network` config cannot mix canonical and legacy forms in the same block. For example, `domains` cannot be combined with `allowed_domains` or `denied_domains`, and `unix_sockets` cannot be combined with `allow_unix_sockets`. - The canonical format can express explicit `"none"` entries for unix sockets, but those entries do not round-trip through the legacy compatibility fields because the legacy fields only represent allow/deny lists. ## Testing `/target/debug/codex sandbox macos --log-denials /bin/zsh -c 'curl https://www.example.com' ` gives 200 with config ``` [permissions.workspace.network.domains] "www.example.com" = "allow" ``` and fails when set to deny: `curl: (56) CONNECT tunnel failed, response 403`. Also tested backward compatibility path by verifying that adding the following to `/etc/codex/requirements.toml` works: ``` [experimental_network] allowed_domains = ["www.example.com"] ```
This commit is contained in:
@@ -5,6 +5,8 @@ use codex_core::config_loader::ConfigLayerEntry;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -333,10 +335,9 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
unix_sockets,
|
||||
allow_local_binding,
|
||||
} = network;
|
||||
|
||||
@@ -362,21 +363,24 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
"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(", ")));
|
||||
if let Some(domains) = domains {
|
||||
parts.push(format!(
|
||||
"domains={}",
|
||||
format_network_permission_entries(&domains.entries, format_network_domain_permission)
|
||||
));
|
||||
}
|
||||
if let Some(managed_allowed_domains_only) = managed_allowed_domains_only {
|
||||
parts.push(format!(
|
||||
"managed_allowed_domains_only={managed_allowed_domains_only}"
|
||||
));
|
||||
}
|
||||
if let Some(denied_domains) = denied_domains {
|
||||
parts.push(format!("denied_domains=[{}]", denied_domains.join(", ")));
|
||||
}
|
||||
if let Some(allow_unix_sockets) = allow_unix_sockets {
|
||||
if let Some(unix_sockets) = unix_sockets {
|
||||
parts.push(format!(
|
||||
"allow_unix_sockets=[{}]",
|
||||
allow_unix_sockets.join(", ")
|
||||
"unix_sockets={}",
|
||||
format_network_permission_entries(
|
||||
&unix_sockets.entries,
|
||||
format_network_unix_socket_permission,
|
||||
)
|
||||
));
|
||||
}
|
||||
if let Some(allow_local_binding) = allow_local_binding {
|
||||
@@ -386,6 +390,33 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
fn format_network_permission_entries<T: Copy>(
|
||||
entries: &std::collections::BTreeMap<String, T>,
|
||||
format_value: impl Fn(T) -> &'static str,
|
||||
) -> String {
|
||||
let parts = entries
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={}", format_value(*value)))
|
||||
.collect::<Vec<_>>();
|
||||
format!("{{{}}}", parts.join(", "))
|
||||
}
|
||||
|
||||
fn format_network_domain_permission(permission: NetworkDomainPermissionToml) -> &'static str {
|
||||
match permission {
|
||||
NetworkDomainPermissionToml::Allow => "allow",
|
||||
NetworkDomainPermissionToml::Deny => "deny",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_network_unix_socket_permission(
|
||||
permission: NetworkUnixSocketPermissionToml,
|
||||
) -> &'static str {
|
||||
match permission {
|
||||
NetworkUnixSocketPermissionToml::Allow => "allow",
|
||||
NetworkUnixSocketPermissionToml::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_debug_config_lines;
|
||||
@@ -400,6 +431,8 @@ mod tests {
|
||||
use codex_core::config_loader::McpServerIdentity;
|
||||
use codex_core::config_loader::McpServerRequirement;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -516,7 +549,12 @@ mod tests {
|
||||
network: Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
@@ -580,7 +618,7 @@ mod tests {
|
||||
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
|
||||
assert!(rendered.contains("enforce_residency: us (source: cloud requirements)"));
|
||||
assert!(rendered.contains(
|
||||
"experimental_network: enabled=true, allowed_domains=[example.com] (source: cloud requirements)"
|
||||
"experimental_network: enabled=true, domains={example.com=allow} (source: cloud requirements)"
|
||||
));
|
||||
assert!(!rendered.contains(" - rules:"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user