Skip to content

Commit 9d13d29

Browse files
authored
[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`
1 parent 806e5f7 commit 9d13d29

File tree

11 files changed

+256
-30
lines changed

11 files changed

+256
-30
lines changed

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9552,6 +9552,12 @@
95529552
"null"
95539553
]
95549554
},
9555+
"dangerFullAccessDenylistOnly": {
9556+
"type": [
9557+
"boolean",
9558+
"null"
9559+
]
9560+
},
95559561
"dangerouslyAllowAllUnixSockets": {
95569562
"type": [
95579563
"boolean",

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6375,6 +6375,12 @@
63756375
"null"
63766376
]
63776377
},
6378+
"dangerFullAccessDenylistOnly": {
6379+
"type": [
6380+
"boolean",
6381+
"null"
6382+
]
6383+
},
63786384
"dangerouslyAllowAllUnixSockets": {
63796385
"type": [
63806386
"boolean",

codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@
151151
"null"
152152
]
153153
},
154+
"dangerFullAccessDenylistOnly": {
155+
"type": [
156+
"boolean",
157+
"null"
158+
]
159+
},
154160
"dangerouslyAllowAllUnixSockets": {
155161
"type": [
156162
"boolean",

codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null,
2929
/**
3030
* Legacy compatibility view derived from `unix_sockets`.
3131
*/
32-
allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
32+
allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, dangerFullAccessDenylistOnly: boolean | null, };

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ pub struct NetworkRequirements {
885885
/// Legacy compatibility view derived from `unix_sockets`.
886886
pub allow_unix_sockets: Option<Vec<String>>,
887887
pub allow_local_binding: Option<bool>,
888+
pub danger_full_access_denylist_only: Option<bool>,
888889
}
889890

890891
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
@@ -7820,6 +7821,7 @@ mod tests {
78207821
dangerously_allow_all_unix_sockets: None,
78217822
domains: None,
78227823
managed_allowed_domains_only: None,
7824+
danger_full_access_denylist_only: None,
78237825
allowed_domains: Some(vec!["api.openai.com".to_string()]),
78247826
denied_domains: Some(vec!["blocked.example.com".to_string()]),
78257827
unix_sockets: None,
@@ -7846,6 +7848,7 @@ mod tests {
78467848
),
78477849
])),
78487850
managed_allowed_domains_only: Some(true),
7851+
danger_full_access_denylist_only: Some(true),
78497852
allowed_domains: Some(vec!["api.openai.com".to_string()]),
78507853
denied_domains: Some(vec!["blocked.example.com".to_string()]),
78517854
unix_sockets: Some(BTreeMap::from([
@@ -7876,6 +7879,7 @@ mod tests {
78767879
"blocked.example.com": "deny"
78777880
},
78787881
"managedAllowedDomainsOnly": true,
7882+
"dangerFullAccessDenylistOnly": true,
78797883
"allowedDomains": ["api.openai.com"],
78807884
"deniedDomains": ["blocked.example.com"],
78817885
"unixSockets": {

codex-rs/app-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Example with notification opt-out:
196196
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
197197
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
198198
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
199-
- `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`.
199+
- `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`.
200200

201201
### Example: Start or resume a thread
202202

codex-rs/app-server/src/config_api.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ fn map_network_requirements_to_api(
449449
.collect()
450450
}),
451451
managed_allowed_domains_only: network.managed_allowed_domains_only,
452+
danger_full_access_denylist_only: network.danger_full_access_denylist_only,
452453
allowed_domains,
453454
denied_domains,
454455
unix_sockets: network.unix_sockets.map(|unix_sockets| {
@@ -594,6 +595,7 @@ mod tests {
594595
]),
595596
}),
596597
managed_allowed_domains_only: Some(false),
598+
danger_full_access_denylist_only: Some(true),
597599
unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml {
598600
entries: std::collections::BTreeMap::from([(
599601
"/tmp/proxy.sock".to_string(),
@@ -653,6 +655,7 @@ mod tests {
653655
("example.com".to_string(), NetworkDomainPermission::Deny),
654656
])),
655657
managed_allowed_domains_only: Some(false),
658+
danger_full_access_denylist_only: Some(true),
656659
allowed_domains: Some(vec!["api.openai.com".to_string()]),
657660
denied_domains: Some(vec!["example.com".to_string()]),
658661
unix_sockets: Some(std::collections::BTreeMap::from([(
@@ -687,6 +690,7 @@ mod tests {
687690
dangerously_allow_all_unix_sockets: None,
688691
domains: None,
689692
managed_allowed_domains_only: None,
693+
danger_full_access_denylist_only: None,
690694
unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml {
691695
entries: std::collections::BTreeMap::from([(
692696
"/tmp/ignored.sock".to_string(),
@@ -710,6 +714,7 @@ mod tests {
710714
dangerously_allow_all_unix_sockets: None,
711715
domains: None,
712716
managed_allowed_domains_only: None,
717+
danger_full_access_denylist_only: None,
713718
allowed_domains: None,
714719
denied_domains: None,
715720
unix_sockets: Some(std::collections::BTreeMap::from([(

codex-rs/config/src/config_requirements.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ pub struct NetworkRequirementsToml {
237237
/// When true, only managed `allowed_domains` are respected while managed
238238
/// network enforcement is active. User allowlist entries are ignored.
239239
pub managed_allowed_domains_only: Option<bool>,
240+
/// In danger-full-access mode, allow all network access and enforce managed deny entries.
241+
pub danger_full_access_denylist_only: Option<bool>,
240242
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
241243
pub allow_local_binding: Option<bool>,
242244
}
@@ -255,6 +257,8 @@ struct RawNetworkRequirementsToml {
255257
/// When true, only managed `allowed_domains` are respected while managed
256258
/// network enforcement is active. User allowlist entries are ignored.
257259
managed_allowed_domains_only: Option<bool>,
260+
/// In danger-full-access mode, allow all network access and enforce managed deny entries.
261+
danger_full_access_denylist_only: Option<bool>,
258262
#[serde(default)]
259263
denied_domains: Option<Vec<String>>,
260264
unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
@@ -279,6 +283,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml {
279283
domains,
280284
allowed_domains,
281285
managed_allowed_domains_only,
286+
danger_full_access_denylist_only,
282287
denied_domains,
283288
unix_sockets,
284289
allow_unix_sockets,
@@ -307,6 +312,7 @@ impl<'de> Deserialize<'de> for NetworkRequirementsToml {
307312
domains: domains
308313
.or_else(|| legacy_domain_permissions_from_lists(allowed_domains, denied_domains)),
309314
managed_allowed_domains_only,
315+
danger_full_access_denylist_only,
310316
unix_sockets: unix_sockets
311317
.or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)),
312318
allow_local_binding,
@@ -359,6 +365,8 @@ pub struct NetworkConstraints {
359365
/// When true, only managed `allowed_domains` are respected while managed
360366
/// network enforcement is active. User allowlist entries are ignored.
361367
pub managed_allowed_domains_only: Option<bool>,
368+
/// In danger-full-access mode, allow all network access and enforce managed deny entries.
369+
pub danger_full_access_denylist_only: Option<bool>,
362370
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
363371
pub allow_local_binding: Option<bool>,
364372
}
@@ -384,6 +392,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
384392
dangerously_allow_all_unix_sockets,
385393
domains,
386394
managed_allowed_domains_only,
395+
danger_full_access_denylist_only,
387396
unix_sockets,
388397
allow_local_binding,
389398
} = value;
@@ -396,6 +405,7 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
396405
dangerously_allow_all_unix_sockets,
397406
domains,
398407
managed_allowed_domains_only,
408+
danger_full_access_denylist_only,
399409
unix_sockets,
400410
allow_local_binding,
401411
}
@@ -1808,6 +1818,7 @@ allowed_approvals_reviewers = ["user"]
18081818
allow_upstream_proxy = false
18091819
dangerously_allow_all_unix_sockets = true
18101820
managed_allowed_domains_only = true
1821+
danger_full_access_denylist_only = true
18111822
allow_local_binding = false
18121823
18131824
[experimental_network.domains]
@@ -1858,6 +1869,10 @@ allowed_approvals_reviewers = ["user"]
18581869
sourced_network.value.managed_allowed_domains_only,
18591870
Some(true)
18601871
);
1872+
assert_eq!(
1873+
sourced_network.value.danger_full_access_denylist_only,
1874+
Some(true)
1875+
);
18611876
assert_eq!(
18621877
sourced_network.value.unix_sockets.as_ref(),
18631878
Some(&NetworkUnixSocketPermissionsToml {
@@ -1881,6 +1896,7 @@ allowed_approvals_reviewers = ["user"]
18811896
dangerously_allow_all_unix_sockets = true
18821897
allowed_domains = ["api.example.com", "*.openai.com"]
18831898
managed_allowed_domains_only = true
1899+
danger_full_access_denylist_only = true
18841900
denied_domains = ["blocked.example.com"]
18851901
allow_unix_sockets = ["/tmp/example.sock"]
18861902
allow_local_binding = false
@@ -1925,6 +1941,10 @@ allowed_approvals_reviewers = ["user"]
19251941
sourced_network.value.managed_allowed_domains_only,
19261942
Some(true)
19271943
);
1944+
assert_eq!(
1945+
sourced_network.value.danger_full_access_denylist_only,
1946+
Some(true)
1947+
);
19281948
assert_eq!(
19291949
sourced_network.value.unix_sockets.as_ref(),
19301950
Some(&NetworkUnixSocketPermissionsToml {

codex-rs/core/src/config/network_proxy_spec.rs

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use codex_protocol::protocol::SandboxPolicy;
2020
use std::collections::HashSet;
2121
use std::sync::Arc;
2222

23+
const GLOBAL_ALLOWLIST_PATTERN: &str = "*";
24+
2325
#[derive(Debug, Clone, PartialEq, Eq)]
2426
pub struct NetworkProxySpec {
2527
config: NetworkProxyConfig,
@@ -195,6 +197,8 @@ impl NetworkProxySpec {
195197
let allowlist_expansion_enabled =
196198
Self::allowlist_expansion_enabled(sandbox_policy, hard_deny_allowlist_misses);
197199
let denylist_expansion_enabled = Self::denylist_expansion_enabled(sandbox_policy);
200+
let danger_full_access_denylist_only =
201+
Self::danger_full_access_denylist_only_enabled(requirements, sandbox_policy);
198202

199203
if let Some(enabled) = requirements.enabled {
200204
config.network.enabled = enabled;
@@ -225,37 +229,43 @@ impl NetworkProxySpec {
225229
constraints.dangerously_allow_all_unix_sockets =
226230
Some(dangerously_allow_all_unix_sockets);
227231
}
228-
let managed_allowed_domains = if hard_deny_allowlist_misses {
229-
Some(
232+
if danger_full_access_denylist_only {
233+
config
234+
.network
235+
.set_allowed_domains(vec![GLOBAL_ALLOWLIST_PATTERN.to_string()]);
236+
} else {
237+
let managed_allowed_domains = if hard_deny_allowlist_misses {
238+
Some(
239+
requirements
240+
.domains
241+
.as_ref()
242+
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
243+
.unwrap_or_default(),
244+
)
245+
} else {
230246
requirements
231247
.domains
232248
.as_ref()
233249
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
234-
.unwrap_or_default(),
235-
)
236-
} else {
237-
requirements
238-
.domains
239-
.as_ref()
240-
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
241-
};
242-
if let Some(managed_allowed_domains) = managed_allowed_domains {
243-
// Managed requirements seed the baseline allowlist. User additions
244-
// can extend that baseline unless managed-only mode pins the
245-
// effective allowlist to the managed set.
246-
let effective_allowed_domains = if allowlist_expansion_enabled {
247-
Self::merge_domain_lists(
248-
managed_allowed_domains.clone(),
249-
config.network.allowed_domains().as_deref().unwrap_or(&[]),
250-
)
251-
} else {
252-
managed_allowed_domains.clone()
253250
};
254-
config
255-
.network
256-
.set_allowed_domains(effective_allowed_domains);
257-
constraints.allowed_domains = Some(managed_allowed_domains);
258-
constraints.allowlist_expansion_enabled = Some(allowlist_expansion_enabled);
251+
if let Some(managed_allowed_domains) = managed_allowed_domains {
252+
// Managed requirements seed the baseline allowlist. User additions
253+
// can extend that baseline unless managed-only mode pins the
254+
// effective allowlist to the managed set.
255+
let effective_allowed_domains = if allowlist_expansion_enabled {
256+
Self::merge_domain_lists(
257+
managed_allowed_domains.clone(),
258+
config.network.allowed_domains().as_deref().unwrap_or(&[]),
259+
)
260+
} else {
261+
managed_allowed_domains.clone()
262+
};
263+
config
264+
.network
265+
.set_allowed_domains(effective_allowed_domains);
266+
constraints.allowed_domains = Some(managed_allowed_domains);
267+
constraints.allowlist_expansion_enabled = Some(allowlist_expansion_enabled);
268+
}
259269
}
260270
let managed_denied_domains = requirements
261271
.domains
@@ -274,7 +284,7 @@ impl NetworkProxySpec {
274284
constraints.denied_domains = Some(managed_denied_domains);
275285
constraints.denylist_expansion_enabled = Some(denylist_expansion_enabled);
276286
}
277-
if requirements.unix_sockets.is_some() {
287+
if requirements.unix_sockets.is_some() && !danger_full_access_denylist_only {
278288
let allow_unix_sockets = requirements
279289
.unix_sockets
280290
.as_ref()
@@ -289,6 +299,14 @@ impl NetworkProxySpec {
289299
config.network.allow_local_binding = allow_local_binding;
290300
constraints.allow_local_binding = Some(allow_local_binding);
291301
}
302+
if danger_full_access_denylist_only {
303+
config.network.allow_upstream_proxy = true;
304+
constraints.allow_upstream_proxy = Some(true);
305+
config.network.dangerously_allow_all_unix_sockets = true;
306+
constraints.dangerously_allow_all_unix_sockets = Some(true);
307+
config.network.allow_local_binding = true;
308+
constraints.allow_local_binding = Some(true);
309+
}
292310

293311
(config, constraints)
294312
}
@@ -307,6 +325,16 @@ impl NetworkProxySpec {
307325
requirements.managed_allowed_domains_only.unwrap_or(false)
308326
}
309327

328+
fn danger_full_access_denylist_only_enabled(
329+
requirements: &NetworkConstraints,
330+
sandbox_policy: &SandboxPolicy,
331+
) -> bool {
332+
matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
333+
&& requirements
334+
.danger_full_access_denylist_only
335+
.unwrap_or(false)
336+
}
337+
310338
fn denylist_expansion_enabled(sandbox_policy: &SandboxPolicy) -> bool {
311339
matches!(
312340
sandbox_policy,

0 commit comments

Comments
 (0)