mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
21a03f1671
commit
dd30c8eedd
@@ -1029,8 +1029,16 @@ impl TurnContext {
|
||||
.network
|
||||
.as_ref()?;
|
||||
Some(TurnContextNetworkItem {
|
||||
allowed_domains: network.allowed_domains.clone().unwrap_or_default(),
|
||||
denied_domains: network.denied_domains.clone().unwrap_or_default(),
|
||||
allowed_domains: network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
denied_domains: network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::config::test_config;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::NetworkConstraints;
|
||||
use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
@@ -471,8 +473,8 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
|
||||
|
||||
let current_cfg = started_proxy.proxy().current_cfg().await?;
|
||||
assert_eq!(
|
||||
current_cfg.network.allowed_domains,
|
||||
vec!["example.com".to_string()]
|
||||
current_cfg.network.allowed_domains(),
|
||||
Some(vec!["example.com".to_string()])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -483,7 +485,12 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
|
||||
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
Some(NetworkConstraints {
|
||||
allowed_domains: Some(vec!["managed.example.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([(
|
||||
"managed.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -510,8 +517,8 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
|
||||
|
||||
let current_cfg = started_proxy.proxy().current_cfg().await?;
|
||||
assert_eq!(
|
||||
current_cfg.network.allowed_domains,
|
||||
vec!["managed.example.com".to_string()]
|
||||
current_cfg.network.allowed_domains(),
|
||||
Some(vec!["managed.example.com".to_string()])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3687,8 +3694,18 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
let mut requirements = config.config_layer_stack.requirements().clone();
|
||||
requirements.network = Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
allowed_domains: Some(vec!["api.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([
|
||||
(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
|
||||
@@ -306,7 +306,9 @@ enabled = true
|
||||
proxy_url = "http://127.0.0.1:43128"
|
||||
enable_socks5 = false
|
||||
allow_upstream_proxy = false
|
||||
allowed_domains = ["openai.com"]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"openai.com" = "allow"
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
@@ -343,9 +345,13 @@ allowed_domains = ["openai.com"]
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
mode: None,
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
denied_domains: None,
|
||||
allow_unix_sockets: None,
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
}),
|
||||
},
|
||||
@@ -421,7 +427,12 @@ fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() -> st
|
||||
)]),
|
||||
}),
|
||||
network: Some(NetworkToml {
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -123,9 +123,14 @@ pub use network_proxy_spec::NetworkProxySpec;
|
||||
pub use network_proxy_spec::StartedNetworkProxy;
|
||||
pub use permissions::FilesystemPermissionToml;
|
||||
pub use permissions::FilesystemPermissionsToml;
|
||||
pub use permissions::NetworkDomainPermissionToml;
|
||||
pub use permissions::NetworkDomainPermissionsToml;
|
||||
pub use permissions::NetworkToml;
|
||||
pub use permissions::NetworkUnixSocketPermissionToml;
|
||||
pub use permissions::NetworkUnixSocketPermissionsToml;
|
||||
pub use permissions::PermissionProfileToml;
|
||||
pub use permissions::PermissionsToml;
|
||||
pub(crate) use permissions::overlay_network_domain_permissions;
|
||||
pub(crate) use permissions::resolve_permission_profile;
|
||||
pub use service::ConfigService;
|
||||
pub use service::ConfigServiceError;
|
||||
|
||||
@@ -226,33 +226,63 @@ impl NetworkProxySpec {
|
||||
Some(dangerously_allow_all_unix_sockets);
|
||||
}
|
||||
let managed_allowed_domains = if hard_deny_allowlist_misses {
|
||||
Some(requirements.allowed_domains.clone().unwrap_or_default())
|
||||
Some(
|
||||
requirements
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
requirements.allowed_domains.clone()
|
||||
requirements
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
};
|
||||
if let Some(allowed_domains) = managed_allowed_domains {
|
||||
if let Some(managed_allowed_domains) = managed_allowed_domains {
|
||||
// Managed requirements seed the baseline allowlist. User additions
|
||||
// can extend that baseline unless managed-only mode pins the
|
||||
// effective allowlist to the managed set.
|
||||
config.network.allowed_domains = if allowlist_expansion_enabled {
|
||||
Self::merge_domain_lists(allowed_domains.clone(), &config.network.allowed_domains)
|
||||
let effective_allowed_domains = if allowlist_expansion_enabled {
|
||||
Self::merge_domain_lists(
|
||||
managed_allowed_domains.clone(),
|
||||
config.network.allowed_domains().as_deref().unwrap_or(&[]),
|
||||
)
|
||||
} else {
|
||||
allowed_domains.clone()
|
||||
managed_allowed_domains.clone()
|
||||
};
|
||||
constraints.allowed_domains = Some(allowed_domains);
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(effective_allowed_domains);
|
||||
constraints.allowed_domains = Some(managed_allowed_domains);
|
||||
constraints.allowlist_expansion_enabled = Some(allowlist_expansion_enabled);
|
||||
}
|
||||
if let Some(denied_domains) = requirements.denied_domains.clone() {
|
||||
config.network.denied_domains = if denylist_expansion_enabled {
|
||||
Self::merge_domain_lists(denied_domains.clone(), &config.network.denied_domains)
|
||||
let managed_denied_domains = requirements
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains);
|
||||
if let Some(managed_denied_domains) = managed_denied_domains {
|
||||
let effective_denied_domains = if denylist_expansion_enabled {
|
||||
Self::merge_domain_lists(
|
||||
managed_denied_domains.clone(),
|
||||
config.network.denied_domains().as_deref().unwrap_or(&[]),
|
||||
)
|
||||
} else {
|
||||
denied_domains.clone()
|
||||
managed_denied_domains.clone()
|
||||
};
|
||||
constraints.denied_domains = Some(denied_domains);
|
||||
config.network.set_denied_domains(effective_denied_domains);
|
||||
constraints.denied_domains = Some(managed_denied_domains);
|
||||
constraints.denylist_expansion_enabled = Some(denylist_expansion_enabled);
|
||||
}
|
||||
if let Some(allow_unix_sockets) = requirements.allow_unix_sockets.clone() {
|
||||
config.network.allow_unix_sockets = allow_unix_sockets.clone();
|
||||
if requirements.unix_sockets.is_some() {
|
||||
let allow_unix_sockets = requirements
|
||||
.unix_sockets
|
||||
.as_ref()
|
||||
.map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.unwrap_or_default();
|
||||
config
|
||||
.network
|
||||
.set_allow_unix_sockets(allow_unix_sockets.clone());
|
||||
constraints.allow_unix_sockets = Some(allow_unix_sockets);
|
||||
}
|
||||
if let Some(allow_local_binding) = requirements.allow_local_binding {
|
||||
@@ -299,37 +329,25 @@ impl NetworkProxySpec {
|
||||
|
||||
fn apply_exec_policy_network_rules(config: &mut NetworkProxyConfig, exec_policy: &Policy) {
|
||||
let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
|
||||
upsert_network_domains(
|
||||
&mut config.network.allowed_domains,
|
||||
&mut config.network.denied_domains,
|
||||
allowed_domains,
|
||||
);
|
||||
upsert_network_domains(
|
||||
&mut config.network.denied_domains,
|
||||
&mut config.network.allowed_domains,
|
||||
denied_domains,
|
||||
);
|
||||
upsert_network_domains(config, allowed_domains, /*allow*/ true);
|
||||
upsert_network_domains(config, denied_domains, /*allow*/ false);
|
||||
}
|
||||
|
||||
fn upsert_network_domains(
|
||||
target: &mut Vec<String>,
|
||||
opposite: &mut Vec<String>,
|
||||
hosts: Vec<String>,
|
||||
) {
|
||||
fn upsert_network_domains(config: &mut NetworkProxyConfig, hosts: Vec<String>, allow: bool) {
|
||||
let mut incoming = HashSet::new();
|
||||
let mut deduped_hosts = Vec::new();
|
||||
for host in hosts {
|
||||
if incoming.insert(host.clone()) {
|
||||
deduped_hosts.push(host);
|
||||
config.network.upsert_domain_permission(
|
||||
host,
|
||||
if allow {
|
||||
codex_network_proxy::NetworkDomainPermission::Allow
|
||||
} else {
|
||||
codex_network_proxy::NetworkDomainPermission::Deny
|
||||
},
|
||||
normalize_host,
|
||||
);
|
||||
}
|
||||
}
|
||||
if incoming.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
opposite.retain(|entry| !incoming.contains(&normalize_host(entry)));
|
||||
target.retain(|entry| !incoming.contains(&normalize_host(entry)));
|
||||
target.extend(deduped_hosts);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
use super::*;
|
||||
use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use codex_network_proxy::NetworkDomainPermission;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn domain_permissions(
|
||||
entries: impl IntoIterator<Item = (&'static str, NetworkDomainPermissionToml)>,
|
||||
) -> NetworkDomainPermissionsToml {
|
||||
NetworkDomainPermissionsToml {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(pattern, permission)| (pattern.to_string(), permission))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_state_with_audit_metadata_threads_metadata_to_state() {
|
||||
let spec = NetworkProxySpec {
|
||||
@@ -24,9 +38,14 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
|
||||
#[test]
|
||||
fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["api.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
allowed_domains: Some(vec!["*.example.com".to_string()]),
|
||||
domains: Some(domain_permissions([(
|
||||
"*.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -38,8 +57,11 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
|
||||
.expect("config should stay within the managed allowlist");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains,
|
||||
vec!["*.example.com".to_string(), "api.example.com".to_string()]
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"*.example.com".to_string(),
|
||||
"api.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.constraints.allowed_domains,
|
||||
@@ -48,14 +70,92 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"api.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)
|
||||
.expect("managed allowlist should not erase a user deny");
|
||||
|
||||
assert_eq!(spec.config.network.allowed_domains(), None);
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.constraints.allowed_domains,
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"*.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)
|
||||
.expect("managed baseline should still allow user edits");
|
||||
|
||||
let mut candidate = spec.config.clone();
|
||||
candidate.network.upsert_domain_permission(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermission::Deny,
|
||||
normalize_host,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
candidate.network.allowed_domains(),
|
||||
Some(vec!["*.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
candidate.network.denied_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
validate_policy_against_constraints(&candidate, &spec.constraints)
|
||||
.expect("user allowlist entries should not become managed constraints");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["evil.com".to_string()];
|
||||
config.network.denied_domains = vec!["more-blocked.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["evil.com".to_string()]);
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["more-blocked.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
allowed_domains: Some(vec!["*.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
domains: Some(domain_permissions([
|
||||
("*.example.com", NetworkDomainPermissionToml::Allow),
|
||||
("blocked.example.com", NetworkDomainPermissionToml::Deny),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -67,12 +167,12 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
|
||||
.expect("yolo mode should pin the effective policy to the managed baseline");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains,
|
||||
vec!["*.example.com".to_string()]
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["*.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains,
|
||||
vec!["blocked.example.com".to_string()]
|
||||
spec.config.network.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
|
||||
assert_eq!(spec.constraints.denylist_expansion_enabled, Some(false));
|
||||
@@ -81,9 +181,14 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
|
||||
#[test]
|
||||
fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["api.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
allowed_domains: Some(vec!["*.example.com".to_string()]),
|
||||
domains: Some(domain_permissions([(
|
||||
"*.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -96,8 +201,8 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
|
||||
.expect("managed baseline should still load");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains,
|
||||
vec!["*.example.com".to_string()]
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["*.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
|
||||
}
|
||||
@@ -105,9 +210,14 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
|
||||
#[test]
|
||||
fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["api.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
allowed_domains: Some(vec!["managed.example.com".to_string()]),
|
||||
domains: Some(domain_permissions([(
|
||||
"managed.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -120,8 +230,8 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses()
|
||||
.expect("managed-only allowlist should still load");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains,
|
||||
vec!["managed.example.com".to_string()]
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["managed.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.constraints.allowed_domains,
|
||||
@@ -134,7 +244,9 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses()
|
||||
#[test]
|
||||
fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domains() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["api.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
@@ -147,7 +259,7 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain
|
||||
)
|
||||
.expect("managed-only mode should treat missing managed allowlist as empty");
|
||||
|
||||
assert!(spec.config.network.allowed_domains.is_empty());
|
||||
assert_eq!(spec.config.network.allowed_domains(), None);
|
||||
assert_eq!(spec.constraints.allowed_domains, Some(Vec::new()));
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
|
||||
assert!(spec.hard_deny_allowlist_misses);
|
||||
@@ -156,7 +268,9 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain
|
||||
#[test]
|
||||
fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_managed_list() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["api.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
managed_allowed_domains_only: Some(true),
|
||||
..Default::default()
|
||||
@@ -169,18 +283,89 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m
|
||||
)
|
||||
.expect("managed-only mode should treat missing managed allowlist as empty");
|
||||
|
||||
assert!(spec.config.network.allowed_domains.is_empty());
|
||||
assert_eq!(spec.config.network.allowed_domains(), None);
|
||||
assert_eq!(spec.constraints.allowed_domains, Some(Vec::new()));
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
|
||||
assert!(spec.hard_deny_allowlist_misses);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"managed-blocked.example.com",
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.expect("deny-only requirements should not constrain the allowlist");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(spec.constraints.allowed_domains, None);
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, None);
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains(),
|
||||
Some(vec!["managed-blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["blocked.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"managed.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.expect("allow-only requirements should not constrain the denylist");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["managed.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(spec.constraints.denied_domains, None);
|
||||
assert_eq!(spec.constraints.denylist_expansion_enabled, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_denied_domains_are_a_baseline_for_default_mode() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.denied_domains = vec!["blocked.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["blocked.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
denied_domains: Some(vec!["managed-blocked.example.com".to_string()]),
|
||||
domains: Some(domain_permissions([(
|
||||
"managed-blocked.example.com",
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -192,11 +377,55 @@ fn requirements_denied_domains_are_a_baseline_for_default_mode() {
|
||||
.expect("default mode should merge managed and user deny entries");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.denied_domains,
|
||||
vec![
|
||||
spec.config.network.denied_domains(),
|
||||
Some(vec![
|
||||
"managed-blocked.example.com".to_string(),
|
||||
"blocked.example.com".to_string()
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
spec.constraints.denied_domains,
|
||||
Some(vec!["managed-blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(spec.constraints.denylist_expansion_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_denylist_expansion_keeps_user_entries_mutable() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["blocked.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"managed-blocked.example.com",
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)
|
||||
.expect("managed baseline should still allow user edits");
|
||||
|
||||
let mut candidate = spec.config.clone();
|
||||
candidate.network.upsert_domain_permission(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermission::Allow,
|
||||
normalize_host,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
candidate.network.allowed_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
candidate.network.denied_domains(),
|
||||
Some(vec!["managed-blocked.example.com".to_string()])
|
||||
);
|
||||
validate_policy_against_constraints(&candidate, &spec.constraints)
|
||||
.expect("user denylist entries should not become managed constraints");
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -56,6 +59,98 @@ pub enum FilesystemPermissionToml {
|
||||
Scoped(BTreeMap<String, FileSystemAccessMode>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct NetworkDomainPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, NetworkDomainPermissionToml>,
|
||||
}
|
||||
|
||||
impl NetworkDomainPermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn allowed_domains(&self) -> Option<Vec<String>> {
|
||||
let allowed_domains: Vec<String> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Allow))
|
||||
.map(|(pattern, _)| pattern.clone())
|
||||
.collect();
|
||||
(!allowed_domains.is_empty()).then_some(allowed_domains)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn denied_domains(&self) -> Option<Vec<String>> {
|
||||
let denied_domains: Vec<String> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Deny))
|
||||
.map(|(pattern, _)| pattern.clone())
|
||||
.collect();
|
||||
(!denied_domains.is_empty()).then_some(denied_domains)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkDomainPermissionToml {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NetworkDomainPermissionToml {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let permission = match self {
|
||||
Self::Allow => "allow",
|
||||
Self::Deny => "deny",
|
||||
};
|
||||
f.write_str(permission)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct NetworkUnixSocketPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, NetworkUnixSocketPermissionToml>,
|
||||
}
|
||||
|
||||
impl NetworkUnixSocketPermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn allow_unix_sockets(&self) -> Vec<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|(_, permission)| matches!(permission, NetworkUnixSocketPermissionToml::Allow))
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkUnixSocketPermissionToml {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NetworkUnixSocketPermissionToml {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let permission = match self {
|
||||
Self::Allow => "allow",
|
||||
Self::None => "none",
|
||||
};
|
||||
f.write_str(permission)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct NetworkToml {
|
||||
@@ -69,9 +164,8 @@ pub struct NetworkToml {
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[schemars(with = "Option<NetworkModeSchema>")]
|
||||
pub mode: Option<NetworkMode>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -114,14 +208,22 @@ impl NetworkToml {
|
||||
if let Some(mode) = self.mode {
|
||||
config.network.mode = mode;
|
||||
}
|
||||
if let Some(allowed_domains) = self.allowed_domains.as_ref() {
|
||||
config.network.allowed_domains = allowed_domains.clone();
|
||||
if let Some(domains) = self.domains.as_ref() {
|
||||
overlay_network_domain_permissions(config, domains);
|
||||
}
|
||||
if let Some(denied_domains) = self.denied_domains.as_ref() {
|
||||
config.network.denied_domains = denied_domains.clone();
|
||||
}
|
||||
if let Some(allow_unix_sockets) = self.allow_unix_sockets.as_ref() {
|
||||
config.network.allow_unix_sockets = allow_unix_sockets.clone();
|
||||
if let Some(unix_sockets) = self.unix_sockets.as_ref() {
|
||||
let mut proxy_unix_sockets = config.network.unix_sockets.take().unwrap_or_default();
|
||||
for (path, permission) in &unix_sockets.entries {
|
||||
let permission = match permission {
|
||||
NetworkUnixSocketPermissionToml::Allow => {
|
||||
ProxyNetworkUnixSocketPermission::Allow
|
||||
}
|
||||
NetworkUnixSocketPermissionToml::None => ProxyNetworkUnixSocketPermission::None,
|
||||
};
|
||||
proxy_unix_sockets.entries.insert(path.clone(), permission);
|
||||
}
|
||||
config.network.unix_sockets =
|
||||
(!proxy_unix_sockets.entries.is_empty()).then_some(proxy_unix_sockets);
|
||||
}
|
||||
if let Some(allow_local_binding) = self.allow_local_binding {
|
||||
config.network.allow_local_binding = allow_local_binding;
|
||||
@@ -135,6 +237,21 @@ impl NetworkToml {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn overlay_network_domain_permissions(
|
||||
config: &mut NetworkProxyConfig,
|
||||
domains: &NetworkDomainPermissionsToml,
|
||||
) {
|
||||
for (pattern, permission) in &domains.entries {
|
||||
let permission = match permission {
|
||||
NetworkDomainPermissionToml::Allow => ProxyNetworkDomainPermission::Allow,
|
||||
NetworkDomainPermissionToml::Deny => ProxyNetworkDomainPermission::Deny,
|
||||
};
|
||||
config
|
||||
.network
|
||||
.upsert_domain_permission(pattern.clone(), permission, normalize_host);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn network_proxy_config_from_profile_network(
|
||||
network: Option<&NetworkToml>,
|
||||
) -> NetworkProxyConfig {
|
||||
|
||||
@@ -76,3 +76,132 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()>
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_toml_ignores_legacy_network_list_keys() {
|
||||
let parsed = toml::from_str::<NetworkToml>(
|
||||
r#"
|
||||
allowed_domains = ["openai.com"]
|
||||
"#,
|
||||
)
|
||||
.expect("legacy network list keys should be ignored");
|
||||
|
||||
assert_eq!(parsed, NetworkToml::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_permission_containers_project_allowed_and_denied_entries() {
|
||||
let domains = NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"*.openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
]),
|
||||
};
|
||||
let unix_sockets = NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"/tmp/example.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/ignored.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::None,
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
domains.allowed_domains(),
|
||||
Some(vec![
|
||||
"*.openai.com".to_string(),
|
||||
"api.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
domains.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}
|
||||
.denied_domains(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
unix_sockets.allow_unix_sockets(),
|
||||
vec!["/tmp/example.sock".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_toml_overlays_unix_socket_permissions_by_path() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
NetworkToml {
|
||||
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"/tmp/base.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/override.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.apply_to_network_proxy_config(&mut config);
|
||||
|
||||
NetworkToml {
|
||||
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"/tmp/extra.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/override.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::None,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.apply_to_network_proxy_config(&mut config);
|
||||
|
||||
assert_eq!(
|
||||
config.network.unix_sockets,
|
||||
Some(codex_network_proxy::NetworkUnixSocketPermissions {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"/tmp/base.sock".to_string(),
|
||||
ProxyNetworkUnixSocketPermission::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/extra.sock".to_string(),
|
||||
ProxyNetworkUnixSocketPermission::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/override.sock".to_string(),
|
||||
ProxyNetworkUnixSocketPermission::None,
|
||||
),
|
||||
]),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,11 @@ pub use codex_config::LoaderOverrides;
|
||||
pub use codex_config::McpServerIdentity;
|
||||
pub use codex_config::McpServerRequirement;
|
||||
pub use codex_config::NetworkConstraints;
|
||||
pub use codex_config::NetworkDomainPermissionToml;
|
||||
pub use codex_config::NetworkDomainPermissionsToml;
|
||||
pub use codex_config::NetworkRequirementsToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionsToml;
|
||||
pub use codex_config::RequirementSource;
|
||||
pub use codex_config::ResidencyRequirement;
|
||||
pub use codex_config::SandboxModeRequirement;
|
||||
|
||||
@@ -130,8 +130,16 @@ impl EnvironmentContext {
|
||||
.as_ref()?;
|
||||
|
||||
Some(NetworkContext {
|
||||
allowed_domains: network.allowed_domains.clone().unwrap_or_default(),
|
||||
denied_domains: network.denied_domains.clone().unwrap_or_default(),
|
||||
allowed_domains: network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
denied_domains: network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::config::test_config;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::FeatureRequirementsToml;
|
||||
use crate::config_loader::NetworkConstraints;
|
||||
use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
@@ -971,7 +973,12 @@ fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
NetworkProxyConfig::default(),
|
||||
Some(NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["github.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([(
|
||||
"github.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
parent_config.permissions.sandbox_policy.get(),
|
||||
@@ -1027,7 +1034,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
let mut parent_config = test_config();
|
||||
let mut parent_network = NetworkProxyConfig::default();
|
||||
parent_network.network.enabled = true;
|
||||
parent_network.network.allowed_domains = vec!["parent.example".to_string()];
|
||||
parent_network
|
||||
.network
|
||||
.set_allowed_domains(vec!["parent.example".to_string()]);
|
||||
parent_config.permissions.network = Some(
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
parent_network,
|
||||
@@ -1039,7 +1048,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
|
||||
let mut live_network = NetworkProxyConfig::default();
|
||||
live_network.network.enabled = true;
|
||||
live_network.network.allowed_domains = vec!["github.com".to_string()];
|
||||
live_network
|
||||
.network
|
||||
.set_allowed_domains(vec!["github.com".to_string()]);
|
||||
|
||||
let guardian_config = build_guardian_review_session_config_for_test(
|
||||
&parent_config,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::NetworkToml;
|
||||
use crate::config::PermissionsToml;
|
||||
use crate::config::find_codex_home;
|
||||
use crate::config::overlay_network_domain_permissions;
|
||||
use crate::config::resolve_permission_profile;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
@@ -150,13 +151,20 @@ fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProx
|
||||
if let Some(dangerously_allow_all_unix_sockets) = network.dangerously_allow_all_unix_sockets {
|
||||
constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets);
|
||||
}
|
||||
if let Some(allowed_domains) = network.allowed_domains {
|
||||
constraints.allowed_domains = Some(allowed_domains);
|
||||
if let Some(domains) = network.domains.as_ref() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
if let Some(allowed_domains) = constraints.allowed_domains.take() {
|
||||
config.network.set_allowed_domains(allowed_domains);
|
||||
}
|
||||
if let Some(denied_domains) = constraints.denied_domains.take() {
|
||||
config.network.set_denied_domains(denied_domains);
|
||||
}
|
||||
overlay_network_domain_permissions(&mut config, domains);
|
||||
constraints.allowed_domains = config.network.allowed_domains();
|
||||
constraints.denied_domains = config.network.denied_domains();
|
||||
}
|
||||
if let Some(denied_domains) = network.denied_domains {
|
||||
constraints.denied_domains = Some(denied_domains);
|
||||
}
|
||||
if let Some(allow_unix_sockets) = network.allow_unix_sockets {
|
||||
if let Some(unix_sockets) = network.unix_sockets.as_ref() {
|
||||
let allow_unix_sockets = unix_sockets.allow_unix_sockets();
|
||||
constraints.allow_unix_sockets = Some(allow_unix_sockets);
|
||||
}
|
||||
if let Some(allow_local_binding) = network.allow_local_binding {
|
||||
@@ -220,24 +228,28 @@ fn apply_exec_policy_network_rules(
|
||||
let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
|
||||
for host in allowed_domains {
|
||||
upsert_network_domain(
|
||||
&mut config.network.allowed_domains,
|
||||
&mut config.network.denied_domains,
|
||||
config,
|
||||
host,
|
||||
codex_network_proxy::NetworkDomainPermission::Allow,
|
||||
);
|
||||
}
|
||||
for host in denied_domains {
|
||||
upsert_network_domain(
|
||||
&mut config.network.denied_domains,
|
||||
&mut config.network.allowed_domains,
|
||||
config,
|
||||
host,
|
||||
codex_network_proxy::NetworkDomainPermission::Deny,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_network_domain(target: &mut Vec<String>, opposite: &mut Vec<String>, host: String) {
|
||||
opposite.retain(|entry| normalize_host(entry) != host);
|
||||
target.retain(|entry| normalize_host(entry) != host);
|
||||
target.push(host);
|
||||
fn upsert_network_domain(
|
||||
config: &mut NetworkProxyConfig,
|
||||
host: String,
|
||||
permission: codex_network_proxy::NetworkDomainPermission,
|
||||
) {
|
||||
config
|
||||
.network
|
||||
.upsert_domain_permission(host, permission, normalize_host);
|
||||
}
|
||||
|
||||
fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
|
||||
|
||||
@@ -6,13 +6,16 @@ use codex_execpolicy::Policy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_beats_lower_profile_network() {
|
||||
fn higher_precedence_profile_network_overlays_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
allowed_domains = ["lower.example.com"]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
"blocked.example.com" = "deny"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
@@ -21,7 +24,9 @@ allowed_domains = ["lower.example.com"]
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
allowed_domains = ["higher.example.com"]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
@@ -38,14 +43,76 @@ allowed_domains = ["higher.example.com"]
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
|
||||
assert_eq!(config.network.allowed_domains, vec!["higher.example.com"]);
|
||||
assert_eq!(
|
||||
config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"lower.example.com".to_string(),
|
||||
"higher.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.denied_domains(),
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_matching_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"shared.example.com" = "deny"
|
||||
"other.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"shared.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
|
||||
assert_eq!(
|
||||
config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"other.example.com".to_string(),
|
||||
"shared.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(config.network.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execpolicy_network_rules_overlay_network_lists() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["config.example.com".to_string()];
|
||||
config.network.denied_domains = vec!["blocked.example.com".to_string()];
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["config.example.com".to_string()]);
|
||||
config
|
||||
.network
|
||||
.set_denied_domains(vec!["blocked.example.com".to_string()]);
|
||||
|
||||
let mut exec_policy = Policy::empty();
|
||||
exec_policy
|
||||
@@ -68,15 +135,15 @@ fn execpolicy_network_rules_overlay_network_lists() {
|
||||
apply_exec_policy_network_rules(&mut config, &exec_policy);
|
||||
|
||||
assert_eq!(
|
||||
config.network.allowed_domains,
|
||||
vec![
|
||||
config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"config.example.com".to_string(),
|
||||
"blocked.example.com".to_string()
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.denied_domains,
|
||||
vec!["api.example.com".to_string()]
|
||||
config.network.denied_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,3 +169,82 @@ dangerously_allow_all_unix_sockets = true
|
||||
|
||||
assert_eq!(constraints.dangerously_allow_all_unix_sockets, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_network_constraints_skips_empty_domain_sides() {
|
||||
let config: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"managed.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("permissions profile should parse");
|
||||
let network = selected_network_from_tables(
|
||||
network_tables_from_toml(&config).expect("permissions profile should deserialize"),
|
||||
)
|
||||
.expect("permissions profile should select a network table")
|
||||
.expect("network table should be present");
|
||||
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
apply_network_constraints(network, &mut constraints);
|
||||
|
||||
assert_eq!(
|
||||
constraints.allowed_domains,
|
||||
Some(vec!["managed.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(constraints.denied_domains, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_network_constraints_overlay_domain_entries() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"blocked.example.com" = "deny"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"api.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let lower_network = selected_network_from_tables(
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should select a network table")
|
||||
.expect("lower network table should be present");
|
||||
let higher_network = selected_network_from_tables(
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should select a network table")
|
||||
.expect("higher network table should be present");
|
||||
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
apply_network_constraints(lower_network, &mut constraints);
|
||||
apply_network_constraints(higher_network, &mut constraints);
|
||||
|
||||
assert_eq!(
|
||||
constraints.allowed_domains,
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
constraints.denied_domains,
|
||||
Some(vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user