[codex] Add danger-full-access denylist-only network mode (#16946)

## Summary

This adds `experimental_network.danger_full_access_denylist_only` for
orgs that want yolo / danger-full-access sessions to keep full network
access while still enforcing centrally managed deny rules.

When the flag is true and the session sandbox is `danger-full-access`,
the network proxy starts with:

- domain allowlist set to `*`
- managed domain `deny` entries enforced
- upstream proxy use allowed
- all Unix sockets allowed
- local/private binding allowed

Caveat: the denylist is best effort only. In yolo / danger-full-access
mode, Codex or the model can use an allowed socket or other
local/private network path to bypass the proxy denylist, so this should
not be treated as a hard security boundary.

The flag is intentionally scoped to `SandboxPolicy::DangerFullAccess`.
Read-only and workspace-write modes keep the existing managed/user
allowlist, denylist, Unix socket, and local-binding behavior. This does
not enable the non-loopback proxy listener setting; that still requires
its own explicit config.

This also threads the new field through config requirements parsing,
app-server protocol/schema output, config API mapping, and the TUI debug
config output.

## How to use

Add the flag under `[experimental_network]` in the network policy config
that is delivered to Codex. The setting is not under `[permissions]`.

```toml
[experimental_network]
enabled = true
danger_full_access_denylist_only = true

[experimental_network.domains]
"blocked.example.com" = "deny"
"*.blocked.example.com" = "deny"
```

With that configuration, yolo / danger-full-access sessions get broad
network access except for the managed denied domains above. The denylist
remains a best-effort proxy policy because the session may still use
allowed sockets to bypass it. Other sandbox modes do not get the
wildcard domain allowlist or the socket/local-binding relaxations from
this flag.

## Verification

- `cargo test -p codex-config network_requirements`
- `cargo test -p codex-core network_proxy_spec`
- `cargo test -p codex-app-server map_requirements_toml_to_api`
- `cargo test -p codex-tui debug_config_output`
- `cargo test -p codex-app-server-protocol`
- `just write-app-server-schema`
- `just fmt`
- `just fix -p codex-config -p codex-core -p codex-app-server-protocol
-p codex-app-server -p codex-tui`
- `just fix -p codex-core -p codex-config`
- `git diff --check`
- `cargo clean`
This commit is contained in:
viyatb-oai
2026-04-06 19:38:51 -07:00
committed by GitHub
parent 806e5f7c69
commit 9d13d29acd
11 changed files with 256 additions and 30 deletions
+55 -27
View File
@@ -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,
@@ -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<Item = (&'static str, NetworkDomainPermissionToml)>,
@@ -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();