mirror of
https://github.com/openai/codex.git
synced 2026-04-27 08:05:51 +00: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:
@@ -866,12 +866,38 @@ pub struct NetworkRequirements {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
/// Canonical network permission map for `experimental_network`.
|
||||
pub domains: Option<BTreeMap<String, NetworkDomainPermission>>,
|
||||
/// When true, only managed allowlist entries are respected while managed
|
||||
/// network enforcement is active.
|
||||
pub managed_allowed_domains_only: Option<bool>,
|
||||
/// Legacy compatibility view derived from `domains`.
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// Legacy compatibility view derived from `domains`.
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
/// Canonical unix socket permission map for `experimental_network`.
|
||||
pub unix_sockets: Option<BTreeMap<String, NetworkUnixSocketPermission>>,
|
||||
/// Legacy compatibility view derived from `unix_sockets`.
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum NetworkDomainPermission {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum NetworkUnixSocketPermission {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -7487,6 +7513,94 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_deserializes_legacy_fields() {
|
||||
let requirements: NetworkRequirements = serde_json::from_value(json!({
|
||||
"allowedDomains": ["api.openai.com"],
|
||||
"deniedDomains": ["blocked.example.com"],
|
||||
"allowUnixSockets": ["/tmp/proxy.sock"]
|
||||
}))
|
||||
.expect("legacy network requirements should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
requirements,
|
||||
NetworkRequirements {
|
||||
enabled: None,
|
||||
http_port: None,
|
||||
socks_port: None,
|
||||
allow_upstream_proxy: None,
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
domains: None,
|
||||
managed_allowed_domains_only: None,
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: None,
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_serializes_canonical_and_legacy_fields() {
|
||||
let requirements = NetworkRequirements {
|
||||
enabled: Some(true),
|
||||
http_port: Some(8080),
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
domains: Some(BTreeMap::from([
|
||||
("api.openai.com".to_string(), NetworkDomainPermission::Allow),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermission::Deny,
|
||||
),
|
||||
])),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: Some(BTreeMap::from([
|
||||
(
|
||||
"/tmp/proxy.sock".to_string(),
|
||||
NetworkUnixSocketPermission::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/ignored.sock".to_string(),
|
||||
NetworkUnixSocketPermission::None,
|
||||
),
|
||||
])),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(requirements).expect("network requirements should serialize"),
|
||||
json!({
|
||||
"enabled": true,
|
||||
"httpPort": 8080,
|
||||
"socksPort": 1080,
|
||||
"allowUpstreamProxy": false,
|
||||
"dangerouslyAllowNonLoopbackProxy": false,
|
||||
"dangerouslyAllowAllUnixSockets": true,
|
||||
"domains": {
|
||||
"api.openai.com": "allow",
|
||||
"blocked.example.com": "deny"
|
||||
},
|
||||
"managedAllowedDomainsOnly": true,
|
||||
"allowedDomains": ["api.openai.com"],
|
||||
"deniedDomains": ["blocked.example.com"],
|
||||
"unixSockets": {
|
||||
"/tmp/ignored.sock": "none",
|
||||
"/tmp/proxy.sock": "allow"
|
||||
},
|
||||
"allowUnixSockets": ["/tmp/proxy.sock"],
|
||||
"allowLocalBinding": true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
let user_item = TurnItem::UserMessage(UserMessageItem {
|
||||
|
||||
Reference in New Issue
Block a user