diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index a58990303236..d465c2466d54 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9473,6 +9473,12 @@ "null" ] }, + "dangerFullAccessDenylistOnly": { + "type": [ + "boolean", + "null" + ] + }, "dangerouslyAllowAllUnixSockets": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index f041f8aae839..36bc57460a2a 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6296,6 +6296,12 @@ "null" ] }, + "dangerFullAccessDenylistOnly": { + "type": [ + "boolean", + "null" + ] + }, "dangerouslyAllowAllUnixSockets": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 614575a95553..ae6eb1dc7d3f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -151,6 +151,12 @@ "null" ] }, + "dangerFullAccessDenylistOnly": { + "type": [ + "boolean", + "null" + ] + }, "dangerouslyAllowAllUnixSockets": { "type": [ "boolean", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts index 5fc942bef51c..c685b9539c56 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts @@ -29,4 +29,4 @@ unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null, /** * Legacy compatibility view derived from `unix_sockets`. */ -allowUnixSockets: Array | null, allowLocalBinding: boolean | null, }; +allowUnixSockets: Array | null, allowLocalBinding: boolean | null, dangerFullAccessDenylistOnly: boolean | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index cdc78647a1b1..b371841c4f53 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -884,6 +884,7 @@ pub struct NetworkRequirements { /// Legacy compatibility view derived from `unix_sockets`. pub allow_unix_sockets: Option>, pub allow_local_binding: Option, + pub danger_full_access_denylist_only: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] @@ -7782,6 +7783,7 @@ mod tests { dangerously_allow_all_unix_sockets: None, domains: None, managed_allowed_domains_only: None, + danger_full_access_denylist_only: None, allowed_domains: Some(vec!["api.openai.com".to_string()]), denied_domains: Some(vec!["blocked.example.com".to_string()]), unix_sockets: None, @@ -7808,6 +7810,7 @@ mod tests { ), ])), managed_allowed_domains_only: Some(true), + danger_full_access_denylist_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([ @@ -7838,6 +7841,7 @@ mod tests { "blocked.example.com": "deny" }, "managedAllowedDomainsOnly": true, + "dangerFullAccessDenylistOnly": true, "allowedDomains": ["api.openai.com"], "deniedDomains": ["blocked.example.com"], "unixSockets": { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c24c3fd7e860..9d43a5ea1ca1 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -194,7 +194,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home). - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index ec2cf82cc16a..4f9a800243a6 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -449,6 +449,7 @@ fn map_network_requirements_to_api( .collect() }), managed_allowed_domains_only: network.managed_allowed_domains_only, + danger_full_access_denylist_only: network.danger_full_access_denylist_only, allowed_domains, denied_domains, unix_sockets: network.unix_sockets.map(|unix_sockets| { @@ -594,6 +595,7 @@ mod tests { ]), }), managed_allowed_domains_only: Some(false), + danger_full_access_denylist_only: Some(true), unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml { entries: std::collections::BTreeMap::from([( "/tmp/proxy.sock".to_string(), @@ -653,6 +655,7 @@ mod tests { ("example.com".to_string(), NetworkDomainPermission::Deny), ])), managed_allowed_domains_only: Some(false), + danger_full_access_denylist_only: Some(true), allowed_domains: Some(vec!["api.openai.com".to_string()]), denied_domains: Some(vec!["example.com".to_string()]), unix_sockets: Some(std::collections::BTreeMap::from([( @@ -687,6 +690,7 @@ mod tests { dangerously_allow_all_unix_sockets: None, domains: None, managed_allowed_domains_only: None, + danger_full_access_denylist_only: None, unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml { entries: std::collections::BTreeMap::from([( "/tmp/ignored.sock".to_string(), @@ -710,6 +714,7 @@ mod tests { dangerously_allow_all_unix_sockets: None, domains: None, managed_allowed_domains_only: None, + danger_full_access_denylist_only: None, allowed_domains: None, denied_domains: None, unix_sockets: Some(std::collections::BTreeMap::from([( diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 7f6b5bc1e582..eb1c1033a5fa 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -237,6 +237,8 @@ pub struct NetworkRequirementsToml { /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. pub managed_allowed_domains_only: Option, + /// In danger-full-access mode, allow all network access and enforce managed deny entries. + pub danger_full_access_denylist_only: Option, pub unix_sockets: Option, pub allow_local_binding: Option, } @@ -255,6 +257,8 @@ struct RawNetworkRequirementsToml { /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. managed_allowed_domains_only: Option, + /// In danger-full-access mode, allow all network access and enforce managed deny entries. + danger_full_access_denylist_only: Option, #[serde(default)] denied_domains: Option>, unix_sockets: Option, @@ -279,6 +283,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml { domains, allowed_domains, managed_allowed_domains_only, + danger_full_access_denylist_only, denied_domains, unix_sockets, allow_unix_sockets, @@ -307,6 +312,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml { domains: domains .or_else(|| legacy_domain_permissions_from_lists(allowed_domains, denied_domains)), managed_allowed_domains_only, + danger_full_access_denylist_only, unix_sockets: unix_sockets .or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)), allow_local_binding, @@ -359,6 +365,8 @@ pub struct NetworkConstraints { /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. pub managed_allowed_domains_only: Option, + /// In danger-full-access mode, allow all network access and enforce managed deny entries. + pub danger_full_access_denylist_only: Option, pub unix_sockets: Option, pub allow_local_binding: Option, } @@ -384,6 +392,7 @@ impl From for NetworkConstraints { dangerously_allow_all_unix_sockets, domains, managed_allowed_domains_only, + danger_full_access_denylist_only, unix_sockets, allow_local_binding, } = value; @@ -396,6 +405,7 @@ impl From for NetworkConstraints { dangerously_allow_all_unix_sockets, domains, managed_allowed_domains_only, + danger_full_access_denylist_only, unix_sockets, allow_local_binding, } @@ -1808,6 +1818,7 @@ allowed_approvals_reviewers = ["user"] allow_upstream_proxy = false dangerously_allow_all_unix_sockets = true managed_allowed_domains_only = true + danger_full_access_denylist_only = true allow_local_binding = false [experimental_network.domains] @@ -1858,6 +1869,10 @@ allowed_approvals_reviewers = ["user"] sourced_network.value.managed_allowed_domains_only, Some(true) ); + assert_eq!( + sourced_network.value.danger_full_access_denylist_only, + Some(true) + ); assert_eq!( sourced_network.value.unix_sockets.as_ref(), Some(&NetworkUnixSocketPermissionsToml { @@ -1881,6 +1896,7 @@ allowed_approvals_reviewers = ["user"] dangerously_allow_all_unix_sockets = true allowed_domains = ["api.example.com", "*.openai.com"] managed_allowed_domains_only = true + danger_full_access_denylist_only = true denied_domains = ["blocked.example.com"] allow_unix_sockets = ["/tmp/example.sock"] allow_local_binding = false @@ -1925,6 +1941,10 @@ allowed_approvals_reviewers = ["user"] sourced_network.value.managed_allowed_domains_only, Some(true) ); + assert_eq!( + sourced_network.value.danger_full_access_denylist_only, + Some(true) + ); assert_eq!( sourced_network.value.unix_sockets.as_ref(), Some(&NetworkUnixSocketPermissionsToml { diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index 93b59cf5f57c..0a37f46cdfb8 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -20,6 +20,8 @@ use codex_protocol::protocol::SandboxPolicy; use std::collections::HashSet; use std::sync::Arc; +const GLOBAL_ALLOWLIST_PATTERN: &str = "*"; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct NetworkProxySpec { config: NetworkProxyConfig, @@ -195,6 +197,8 @@ impl NetworkProxySpec { let allowlist_expansion_enabled = Self::allowlist_expansion_enabled(sandbox_policy, hard_deny_allowlist_misses); let denylist_expansion_enabled = Self::denylist_expansion_enabled(sandbox_policy); + let danger_full_access_denylist_only = + Self::danger_full_access_denylist_only_enabled(requirements, sandbox_policy); if let Some(enabled) = requirements.enabled { config.network.enabled = enabled; @@ -225,37 +229,43 @@ impl NetworkProxySpec { constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets); } - let managed_allowed_domains = if hard_deny_allowlist_misses { - Some( + if danger_full_access_denylist_only { + config + .network + .set_allowed_domains(vec![GLOBAL_ALLOWLIST_PATTERN.to_string()]); + } else { + let managed_allowed_domains = if hard_deny_allowlist_misses { + Some( + requirements + .domains + .as_ref() + .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains) + .unwrap_or_default(), + ) + } else { requirements .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains) - .unwrap_or_default(), - ) - } else { - requirements - .domains - .as_ref() - .and_then(codex_config::NetworkDomainPermissionsToml::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. - 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 { - managed_allowed_domains.clone() }; - 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(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. + 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 { + managed_allowed_domains.clone() + }; + config + .network + .set_allowed_domains(effective_allowed_domains); + constraints.allowed_domains = Some(managed_allowed_domains); + constraints.allowlist_expansion_enabled = Some(allowlist_expansion_enabled); + } } let managed_denied_domains = requirements .domains @@ -274,7 +284,7 @@ impl NetworkProxySpec { constraints.denied_domains = Some(managed_denied_domains); constraints.denylist_expansion_enabled = Some(denylist_expansion_enabled); } - if requirements.unix_sockets.is_some() { + if requirements.unix_sockets.is_some() && !danger_full_access_denylist_only { let allow_unix_sockets = requirements .unix_sockets .as_ref() @@ -289,6 +299,14 @@ impl NetworkProxySpec { config.network.allow_local_binding = allow_local_binding; constraints.allow_local_binding = Some(allow_local_binding); } + if danger_full_access_denylist_only { + config.network.allow_upstream_proxy = true; + constraints.allow_upstream_proxy = Some(true); + config.network.dangerously_allow_all_unix_sockets = true; + constraints.dangerously_allow_all_unix_sockets = Some(true); + config.network.allow_local_binding = true; + constraints.allow_local_binding = Some(true); + } (config, constraints) } @@ -307,6 +325,16 @@ impl NetworkProxySpec { requirements.managed_allowed_domains_only.unwrap_or(false) } + fn danger_full_access_denylist_only_enabled( + requirements: &NetworkConstraints, + sandbox_policy: &SandboxPolicy, + ) -> bool { + matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) + && requirements + .danger_full_access_denylist_only + .unwrap_or(false) + } + fn denylist_expansion_enabled(sandbox_policy: &SandboxPolicy) -> bool { matches!( sandbox_policy, diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index 77007885a4ae..742512d600b6 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -1,8 +1,11 @@ use super::*; use crate::config_loader::NetworkDomainPermissionToml; use crate::config_loader::NetworkDomainPermissionsToml; +use crate::config_loader::NetworkUnixSocketPermissionToml; +use crate::config_loader::NetworkUnixSocketPermissionsToml; use codex_network_proxy::NetworkDomainPermission; use pretty_assertions::assert_eq; +use std::collections::BTreeMap; fn domain_permissions( entries: impl IntoIterator, @@ -178,6 +181,147 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() { assert_eq!(spec.constraints.denylist_expansion_enabled, Some(false)); } +#[test] +fn danger_full_access_denylist_only_allows_all_domains_and_enforces_managed_denies() { + let mut config = NetworkProxyConfig::default(); + 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 { + allow_upstream_proxy: Some(false), + dangerously_allow_all_unix_sockets: Some(false), + domains: Some(domain_permissions([ + ("*.example.com", NetworkDomainPermissionToml::Allow), + ("blocked.example.com", NetworkDomainPermissionToml::Deny), + ])), + danger_full_access_denylist_only: Some(true), + unix_sockets: Some(NetworkUnixSocketPermissionsToml { + entries: BTreeMap::from([( + "/tmp/managed.sock".to_string(), + NetworkUnixSocketPermissionToml::Allow, + )]), + }), + allow_local_binding: Some(false), + ..Default::default() + }; + + let spec = NetworkProxySpec::from_config_and_constraints( + config, + Some(requirements), + &SandboxPolicy::DangerFullAccess, + ) + .expect("denylist-only yolo mode should allow all domains except managed denies"); + + assert_eq!( + spec.config.network.allowed_domains(), + Some(vec!["*".to_string()]) + ); + assert_eq!( + spec.config.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + assert!(spec.config.network.allow_upstream_proxy); + assert!(spec.config.network.dangerously_allow_all_unix_sockets); + assert!(spec.config.network.allow_local_binding); + assert_eq!(spec.constraints.allow_upstream_proxy, Some(true)); + assert_eq!( + spec.constraints.dangerously_allow_all_unix_sockets, + Some(true) + ); + assert_eq!(spec.constraints.allow_unix_sockets, None); + assert_eq!(spec.constraints.allow_local_binding, Some(true)); + assert_eq!(spec.constraints.allowed_domains, None); + assert_eq!(spec.constraints.allowlist_expansion_enabled, None); + assert_eq!( + spec.constraints.denied_domains, + Some(vec!["blocked.example.com".to_string()]) + ); + assert_eq!(spec.constraints.denylist_expansion_enabled, Some(false)); +} + +#[test] +fn danger_full_access_denylist_only_does_not_change_workspace_write_behavior() { + let mut config = NetworkProxyConfig::default(); + config + .network + .set_allowed_domains(vec!["api.example.com".to_string()]); + config + .network + .set_denied_domains(vec!["blocked.example.com".to_string()]); + let requirements = NetworkConstraints { + allow_upstream_proxy: Some(false), + dangerously_allow_all_unix_sockets: Some(false), + domains: Some(domain_permissions([ + ("*.example.com", NetworkDomainPermissionToml::Allow), + ( + "managed-blocked.example.com", + NetworkDomainPermissionToml::Deny, + ), + ])), + danger_full_access_denylist_only: Some(true), + unix_sockets: Some(NetworkUnixSocketPermissionsToml { + entries: BTreeMap::from([( + "/tmp/managed.sock".to_string(), + NetworkUnixSocketPermissionToml::Allow, + )]), + }), + allow_local_binding: Some(false), + ..Default::default() + }; + + let spec = NetworkProxySpec::from_config_and_constraints( + config, + Some(requirements), + &SandboxPolicy::new_workspace_write_policy(), + ) + .expect("denylist-only yolo flag should not affect workspace-write mode"); + + assert_eq!( + spec.config.network.allowed_domains(), + Some(vec![ + "*.example.com".to_string(), + "api.example.com".to_string() + ]) + ); + assert_eq!( + spec.config.network.denied_domains(), + Some(vec![ + "managed-blocked.example.com".to_string(), + "blocked.example.com".to_string() + ]) + ); + assert!(!spec.config.network.allow_upstream_proxy); + assert!(!spec.config.network.dangerously_allow_all_unix_sockets); + assert_eq!( + spec.config.network.allow_unix_sockets(), + vec!["/tmp/managed.sock".to_string()] + ); + assert!(!spec.config.network.allow_local_binding); + assert_eq!(spec.constraints.allow_upstream_proxy, Some(false)); + assert_eq!( + spec.constraints.dangerously_allow_all_unix_sockets, + Some(false) + ); + assert_eq!( + spec.constraints.allow_unix_sockets, + Some(vec!["/tmp/managed.sock".to_string()]) + ); + assert_eq!(spec.constraints.allow_local_binding, Some(false)); + assert_eq!( + spec.constraints.allowed_domains, + Some(vec!["*.example.com".to_string()]) + ); + assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true)); + 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 managed_allowed_domains_only_disables_default_mode_allowlist_expansion() { let mut config = NetworkProxyConfig::default(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 86d9949c589e..bc723049746d 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -337,6 +337,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String { dangerously_allow_all_unix_sockets, domains, managed_allowed_domains_only, + danger_full_access_denylist_only, unix_sockets, allow_local_binding, } = network; @@ -374,6 +375,11 @@ fn format_network_constraints(network: &NetworkConstraints) -> String { "managed_allowed_domains_only={managed_allowed_domains_only}" )); } + if let Some(danger_full_access_denylist_only) = danger_full_access_denylist_only { + parts.push(format!( + "danger_full_access_denylist_only={danger_full_access_denylist_only}" + )); + } if let Some(unix_sockets) = unix_sockets { parts.push(format!( "unix_sockets={}", @@ -557,6 +563,7 @@ mod tests { NetworkDomainPermissionToml::Allow, )]), }), + danger_full_access_denylist_only: Some(true), ..Default::default() }, RequirementSource::CloudRequirements, @@ -621,7 +628,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, domains={example.com=allow} (source: cloud requirements)" + "experimental_network: enabled=true, domains={example.com=allow}, danger_full_access_denylist_only=true (source: cloud requirements)" )); assert!(!rendered.contains(" - rules:")); }