mirror of
https://github.com/openai/codex.git
synced 2026-03-27 00:53:50 +00:00
Compare commits
8 Commits
pr15933
...
dev/cc/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9abc12af7 | ||
|
|
71c88f94f2 | ||
|
|
1a62e67e7e | ||
|
|
85d53815b1 | ||
|
|
5b5901320d | ||
|
|
9da9d56bf3 | ||
|
|
c7615f7a9b | ||
|
|
4420fe6f41 |
@@ -9302,6 +9302,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkRequirements": {
|
||||
"properties": {
|
||||
"allowLocalBinding": {
|
||||
@@ -9311,6 +9318,7 @@
|
||||
]
|
||||
},
|
||||
"allowUnixSockets": {
|
||||
"description": "Legacy compatibility view derived from `unix_sockets`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9326,6 +9334,7 @@
|
||||
]
|
||||
},
|
||||
"allowedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9347,6 +9356,7 @@
|
||||
]
|
||||
},
|
||||
"deniedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9355,6 +9365,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"domains": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/v2/NetworkDomainPermission"
|
||||
},
|
||||
"description": "Canonical network permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -9376,10 +9396,27 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unixSockets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/v2/NetworkUnixSocketPermission"
|
||||
},
|
||||
"description": "Canonical unix socket permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkUnixSocketPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
|
||||
@@ -6017,6 +6017,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkRequirements": {
|
||||
"properties": {
|
||||
"allowLocalBinding": {
|
||||
@@ -6026,6 +6033,7 @@
|
||||
]
|
||||
},
|
||||
"allowUnixSockets": {
|
||||
"description": "Legacy compatibility view derived from `unix_sockets`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6041,6 +6049,7 @@
|
||||
]
|
||||
},
|
||||
"allowedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6062,6 +6071,7 @@
|
||||
]
|
||||
},
|
||||
"deniedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6070,6 +6080,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"domains": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkDomainPermission"
|
||||
},
|
||||
"description": "Canonical network permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -6091,10 +6111,27 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unixSockets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkUnixSocketPermission"
|
||||
},
|
||||
"description": "Canonical unix socket permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkUnixSocketPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
|
||||
@@ -102,6 +102,13 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkRequirements": {
|
||||
"properties": {
|
||||
"allowLocalBinding": {
|
||||
@@ -111,6 +118,7 @@
|
||||
]
|
||||
},
|
||||
"allowUnixSockets": {
|
||||
"description": "Legacy compatibility view derived from `unix_sockets`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -126,6 +134,7 @@
|
||||
]
|
||||
},
|
||||
"allowedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -147,6 +156,7 @@
|
||||
]
|
||||
},
|
||||
"deniedDomains": {
|
||||
"description": "Legacy compatibility view derived from `domains`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -155,6 +165,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"domains": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkDomainPermission"
|
||||
},
|
||||
"description": "Canonical network permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -176,10 +196,27 @@
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unixSockets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkUnixSocketPermission"
|
||||
},
|
||||
"description": "Canonical unix socket permission map for `experimental_network`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkUnixSocketPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ResidencyRequirement": {
|
||||
"enum": [
|
||||
"us"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NetworkDomainPermission = "allow" | "deny";
|
||||
@@ -1,5 +1,27 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { NetworkDomainPermission } from "./NetworkDomainPermission";
|
||||
import type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission";
|
||||
|
||||
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null,
|
||||
/**
|
||||
* Canonical network permission map for `experimental_network`.
|
||||
*/
|
||||
domains: { [key in string]?: NetworkDomainPermission } | null,
|
||||
/**
|
||||
* Legacy compatibility view derived from `domains`.
|
||||
*/
|
||||
allowedDomains: Array<string> | null,
|
||||
/**
|
||||
* Legacy compatibility view derived from `domains`.
|
||||
*/
|
||||
deniedDomains: Array<string> | null,
|
||||
/**
|
||||
* Canonical unix socket permission map for `experimental_network`.
|
||||
*/
|
||||
unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null,
|
||||
/**
|
||||
* Legacy compatibility view derived from `unix_sockets`.
|
||||
*/
|
||||
allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type NetworkUnixSocketPermission = "allow" | "none";
|
||||
@@ -199,9 +199,11 @@ export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
export type { NetworkDomainPermission } from "./NetworkDomainPermission";
|
||||
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
|
||||
export type { NetworkRequirements } from "./NetworkRequirements";
|
||||
export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission";
|
||||
export type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
|
||||
export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
|
||||
@@ -871,12 +871,35 @@ pub struct NetworkRequirements {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
/// Canonical network permission map for `experimental_network`.
|
||||
pub domains: Option<BTreeMap<String, NetworkDomainPermission>>,
|
||||
/// Legacy compatibility view derived from `domains`.
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// Legacy compatibility view derived from `domains`.
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
/// Canonical unix socket permission map for `experimental_network`.
|
||||
pub unix_sockets: Option<BTreeMap<String, NetworkUnixSocketPermission>>,
|
||||
/// Legacy compatibility view derived from `unix_sockets`.
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum NetworkDomainPermission {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum NetworkUnixSocketPermission {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -7676,6 +7699,91 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_deserializes_legacy_fields() {
|
||||
let requirements: NetworkRequirements = serde_json::from_value(json!({
|
||||
"allowedDomains": ["api.openai.com"],
|
||||
"deniedDomains": ["blocked.example.com"],
|
||||
"allowUnixSockets": ["/tmp/proxy.sock"]
|
||||
}))
|
||||
.expect("legacy network requirements should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
requirements,
|
||||
NetworkRequirements {
|
||||
enabled: None,
|
||||
http_port: None,
|
||||
socks_port: None,
|
||||
allow_upstream_proxy: None,
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
domains: None,
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: None,
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_serializes_canonical_and_legacy_fields() {
|
||||
let requirements = NetworkRequirements {
|
||||
enabled: Some(true),
|
||||
http_port: Some(8080),
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
domains: Some(BTreeMap::from([
|
||||
("api.openai.com".to_string(), NetworkDomainPermission::Allow),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermission::Deny,
|
||||
),
|
||||
])),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: Some(BTreeMap::from([
|
||||
(
|
||||
"/tmp/proxy.sock".to_string(),
|
||||
NetworkUnixSocketPermission::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/ignored.sock".to_string(),
|
||||
NetworkUnixSocketPermission::None,
|
||||
),
|
||||
])),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(requirements).expect("network requirements should serialize"),
|
||||
json!({
|
||||
"enabled": true,
|
||||
"httpPort": 8080,
|
||||
"socksPort": 1080,
|
||||
"allowUpstreamProxy": false,
|
||||
"dangerouslyAllowNonLoopbackProxy": false,
|
||||
"dangerouslyAllowAllUnixSockets": true,
|
||||
"domains": {
|
||||
"api.openai.com": "allow",
|
||||
"blocked.example.com": "deny"
|
||||
},
|
||||
"allowedDomains": ["api.openai.com"],
|
||||
"deniedDomains": ["blocked.example.com"],
|
||||
"unixSockets": {
|
||||
"/tmp/ignored.sock": "none",
|
||||
"/tmp/proxy.sock": "allow"
|
||||
},
|
||||
"allowUnixSockets": ["/tmp/proxy.sock"],
|
||||
"allowLocalBinding": true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
let user_item = TurnItem::UserMessage(UserMessageItem {
|
||||
|
||||
@@ -12,7 +12,9 @@ use codex_app_server_protocol::ConfigWriteResponse;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::NetworkDomainPermission;
|
||||
use codex_app_server_protocol::NetworkRequirements;
|
||||
use codex_app_server_protocol::NetworkUnixSocketPermission;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_core::AnalyticsEventsClient;
|
||||
use codex_core::ThreadManager;
|
||||
@@ -410,6 +412,20 @@ fn map_residency_requirement_to_api(
|
||||
fn map_network_requirements_to_api(
|
||||
network: codex_core::config_loader::NetworkRequirementsToml,
|
||||
) -> NetworkRequirements {
|
||||
let allowed_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::allowed_domains);
|
||||
let denied_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::denied_domains);
|
||||
let allow_unix_sockets = network
|
||||
.unix_sockets
|
||||
.as_ref()
|
||||
.map(codex_core::config_loader::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.filter(|entries| !entries.is_empty());
|
||||
|
||||
NetworkRequirements {
|
||||
enabled: network.enabled,
|
||||
http_port: network.http_port,
|
||||
@@ -417,13 +433,57 @@ fn map_network_requirements_to_api(
|
||||
allow_upstream_proxy: network.allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
|
||||
allowed_domains: network.allowed_domains,
|
||||
denied_domains: network.denied_domains,
|
||||
allow_unix_sockets: network.allow_unix_sockets,
|
||||
domains: network.domains.map(|domains| {
|
||||
domains
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|(pattern, permission)| {
|
||||
(pattern, map_network_domain_permission_to_api(permission))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
unix_sockets: network.unix_sockets.map(|unix_sockets| {
|
||||
unix_sockets
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|(path, permission)| {
|
||||
(path, map_network_unix_socket_permission_to_api(permission))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
allow_unix_sockets,
|
||||
allow_local_binding: network.allow_local_binding,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_network_domain_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkDomainPermissionToml,
|
||||
) -> NetworkDomainPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Allow => {
|
||||
NetworkDomainPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Deny => {
|
||||
NetworkDomainPermission::Deny
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_network_unix_socket_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkUnixSocketPermissionToml,
|
||||
) -> NetworkUnixSocketPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::Allow => {
|
||||
NetworkUnixSocketPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::None => {
|
||||
NetworkUnixSocketPermission::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(err: ConfigServiceError) -> JSONRPCErrorError {
|
||||
if let Some(code) = err.write_error_code() {
|
||||
return config_write_error(code, err.to_string());
|
||||
@@ -452,7 +512,11 @@ mod tests {
|
||||
use codex_core::AnalyticsEventsClient;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml as CoreNetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml as CoreNetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml as CoreNetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionsToml as CoreNetworkUnixSocketPermissionsToml;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -505,10 +569,25 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
domains: Some(CoreNetworkDomainPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([
|
||||
(
|
||||
"api.openai.com".to_string(),
|
||||
CoreNetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"example.com".to_string(),
|
||||
CoreNetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
managed_allowed_domains_only: Some(false),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([(
|
||||
"/tmp/proxy.sock".to_string(),
|
||||
CoreNetworkUnixSocketPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
allow_local_binding: Some(true),
|
||||
}),
|
||||
};
|
||||
@@ -550,14 +629,77 @@ mod tests {
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
domains: Some(std::collections::BTreeMap::from([
|
||||
("api.openai.com".to_string(), NetworkDomainPermission::Allow,),
|
||||
("example.com".to_string(), NetworkDomainPermission::Deny),
|
||||
])),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
unix_sockets: Some(std::collections::BTreeMap::from([(
|
||||
"/tmp/proxy.sock".to_string(),
|
||||
NetworkUnixSocketPermission::Allow,
|
||||
)])),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_requirements_toml_to_api_omits_unix_socket_none_entries_from_legacy_network_fields() {
|
||||
let requirements = ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
guardian_developer_instructions: None,
|
||||
feature_requirements: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: Some(CoreNetworkRequirementsToml {
|
||||
enabled: None,
|
||||
http_port: None,
|
||||
socks_port: None,
|
||||
allow_upstream_proxy: None,
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
domains: None,
|
||||
managed_allowed_domains_only: None,
|
||||
unix_sockets: Some(CoreNetworkUnixSocketPermissionsToml {
|
||||
entries: std::collections::BTreeMap::from([(
|
||||
"/tmp/ignored.sock".to_string(),
|
||||
CoreNetworkUnixSocketPermissionToml::None,
|
||||
)]),
|
||||
}),
|
||||
allow_local_binding: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let mapped = map_requirements_toml_to_api(requirements);
|
||||
|
||||
assert_eq!(
|
||||
mapped.network,
|
||||
Some(NetworkRequirements {
|
||||
enabled: None,
|
||||
http_port: None,
|
||||
socks_port: None,
|
||||
allow_upstream_proxy: None,
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
domains: None,
|
||||
allowed_domains: None,
|
||||
denied_domains: None,
|
||||
unix_sockets: Some(std::collections::BTreeMap::from([(
|
||||
"/tmp/ignored.sock".to_string(),
|
||||
NetworkUnixSocketPermission::None,
|
||||
)])),
|
||||
allow_unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_requirements_toml_to_api_normalizes_allowed_web_search_modes() {
|
||||
let requirements = ConfigRequirementsToml {
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error as _;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
@@ -132,7 +133,93 @@ pub struct McpServerRequirement {
|
||||
pub identity: McpServerIdentity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkDomainPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, NetworkDomainPermissionToml>,
|
||||
}
|
||||
|
||||
impl NetworkDomainPermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub 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)
|
||||
}
|
||||
|
||||
pub 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)]
|
||||
#[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)]
|
||||
pub struct NetworkUnixSocketPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, NetworkUnixSocketPermissionToml>,
|
||||
}
|
||||
|
||||
impl NetworkUnixSocketPermissionsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub 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)]
|
||||
#[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, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkRequirementsToml {
|
||||
pub enabled: Option<bool>,
|
||||
pub http_port: Option<u16>,
|
||||
@@ -140,17 +227,118 @@ pub struct NetworkRequirementsToml {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
/// 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<bool>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawNetworkRequirementsToml {
|
||||
enabled: Option<bool>,
|
||||
http_port: Option<u16>,
|
||||
socks_port: Option<u16>,
|
||||
allow_upstream_proxy: Option<bool>,
|
||||
dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
domains: Option<NetworkDomainPermissionsToml>,
|
||||
#[serde(default)]
|
||||
allowed_domains: Option<Vec<String>>,
|
||||
/// When true, only managed `allowed_domains` are respected while managed
|
||||
/// network enforcement is active. User allowlist entries are ignored.
|
||||
managed_allowed_domains_only: Option<bool>,
|
||||
#[serde(default)]
|
||||
denied_domains: Option<Vec<String>>,
|
||||
unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
#[serde(default)]
|
||||
allow_unix_sockets: Option<Vec<String>>,
|
||||
allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkRequirementsToml {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw = RawNetworkRequirementsToml::deserialize(deserializer)?;
|
||||
let RawNetworkRequirementsToml {
|
||||
enabled,
|
||||
http_port,
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
domains,
|
||||
allowed_domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
unix_sockets,
|
||||
allow_unix_sockets,
|
||||
allow_local_binding,
|
||||
} = raw;
|
||||
|
||||
if domains.is_some() && (allowed_domains.is_some() || denied_domains.is_some()) {
|
||||
return Err(D::Error::custom(
|
||||
"`experimental_network.domains` cannot be combined with legacy `allowed_domains` or `denied_domains`",
|
||||
));
|
||||
}
|
||||
|
||||
if unix_sockets.is_some() && allow_unix_sockets.is_some() {
|
||||
return Err(D::Error::custom(
|
||||
"`experimental_network.unix_sockets` cannot be combined with legacy `allow_unix_sockets`",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
enabled,
|
||||
http_port,
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
domains: domains
|
||||
.or_else(|| legacy_domain_permissions_from_lists(allowed_domains, denied_domains)),
|
||||
managed_allowed_domains_only,
|
||||
unix_sockets: unix_sockets
|
||||
.or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)),
|
||||
allow_local_binding,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_domain_permissions_from_lists(
|
||||
allowed_domains: Option<Vec<String>>,
|
||||
denied_domains: Option<Vec<String>>,
|
||||
) -> Option<NetworkDomainPermissionsToml> {
|
||||
let mut entries = BTreeMap::new();
|
||||
|
||||
for pattern in allowed_domains.unwrap_or_default() {
|
||||
entries.insert(pattern, NetworkDomainPermissionToml::Allow);
|
||||
}
|
||||
|
||||
for pattern in denied_domains.unwrap_or_default() {
|
||||
entries.insert(pattern, NetworkDomainPermissionToml::Deny);
|
||||
}
|
||||
|
||||
(!entries.is_empty()).then_some(NetworkDomainPermissionsToml { entries })
|
||||
}
|
||||
|
||||
fn legacy_unix_socket_permissions_from_list(
|
||||
allow_unix_sockets: Option<Vec<String>>,
|
||||
) -> Option<NetworkUnixSocketPermissionsToml> {
|
||||
let entries = allow_unix_sockets
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|path| (path, NetworkUnixSocketPermissionToml::Allow))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
(!entries.is_empty()).then_some(NetworkUnixSocketPermissionsToml { entries })
|
||||
}
|
||||
|
||||
/// Normalized network constraints derived from requirements TOML.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct NetworkConstraints {
|
||||
pub enabled: Option<bool>,
|
||||
pub http_port: Option<u16>,
|
||||
@@ -158,15 +346,24 @@ pub struct NetworkConstraints {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
/// 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<bool>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkConstraints {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let requirements = NetworkRequirementsToml::deserialize(deserializer)?;
|
||||
Ok(requirements.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
fn from(value: NetworkRequirementsToml) -> Self {
|
||||
let NetworkRequirementsToml {
|
||||
@@ -176,10 +373,9 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
unix_sockets,
|
||||
allow_local_binding,
|
||||
} = value;
|
||||
Self {
|
||||
@@ -189,10 +385,9 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
unix_sockets,
|
||||
allow_local_binding,
|
||||
}
|
||||
}
|
||||
@@ -1470,6 +1665,78 @@ guardian_developer_instructions = """
|
||||
|
||||
#[test]
|
||||
fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
[experimental_network]
|
||||
enabled = true
|
||||
allow_upstream_proxy = false
|
||||
dangerously_allow_all_unix_sockets = true
|
||||
managed_allowed_domains_only = true
|
||||
allow_local_binding = false
|
||||
|
||||
[experimental_network.domains]
|
||||
"api.example.com" = "allow"
|
||||
"*.openai.com" = "allow"
|
||||
"blocked.example.com" = "deny"
|
||||
|
||||
[experimental_network.unix_sockets]
|
||||
"/tmp/example.sock" = "allow"
|
||||
"#;
|
||||
|
||||
let source = RequirementSource::CloudRequirements;
|
||||
let mut requirements_with_sources = ConfigRequirementsWithSources::default();
|
||||
requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?);
|
||||
|
||||
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
|
||||
let sourced_network = requirements
|
||||
.network
|
||||
.expect("network requirements should be preserved as constraints");
|
||||
|
||||
assert_eq!(sourced_network.source, source);
|
||||
assert_eq!(sourced_network.value.enabled, Some(true));
|
||||
assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
|
||||
assert_eq!(
|
||||
sourced_network.value.dangerously_allow_all_unix_sockets,
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.domains.as_ref(),
|
||||
Some(&NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"*.openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
]),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.managed_allowed_domains_only,
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.unix_sockets.as_ref(),
|
||||
Some(&NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"/tmp/example.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
)]),
|
||||
})
|
||||
);
|
||||
assert_eq!(sourced_network.value.allow_local_binding, Some(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
[experimental_network]
|
||||
enabled = true
|
||||
@@ -1499,29 +1766,137 @@ guardian_developer_instructions = """
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.allowed_domains.as_ref(),
|
||||
Some(&vec![
|
||||
"api.example.com".to_string(),
|
||||
"*.openai.com".to_string()
|
||||
])
|
||||
sourced_network.value.domains.as_ref(),
|
||||
Some(&NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"*.openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"api.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
]),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.managed_allowed_domains_only,
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.denied_domains.as_ref(),
|
||||
Some(&vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.allow_unix_sockets.as_ref(),
|
||||
Some(&vec!["/tmp/example.sock".to_string()])
|
||||
sourced_network.value.unix_sockets.as_ref(),
|
||||
Some(&NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"/tmp/example.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
)]),
|
||||
})
|
||||
);
|
||||
assert_eq!(sourced_network.value.allow_local_binding, Some(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_legacy_and_canonical_network_requirements_are_rejected() {
|
||||
let err = from_str::<ConfigRequirementsToml>(
|
||||
r#"
|
||||
[experimental_network]
|
||||
allowed_domains = ["api.example.com"]
|
||||
|
||||
[experimental_network.domains]
|
||||
"*.openai.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect_err("mixed network domain shapes should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("`experimental_network.domains` cannot be combined"),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
|
||||
let err = from_str::<ConfigRequirementsToml>(
|
||||
r#"
|
||||
[experimental_network]
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
|
||||
[experimental_network.unix_sockets]
|
||||
"/tmp/another.sock" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect_err("mixed network unix socket shapes should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("`experimental_network.unix_sockets` cannot be combined"),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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 deserialize_mcp_server_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -25,7 +25,11 @@ pub use config_requirements::FeatureRequirementsToml;
|
||||
pub use config_requirements::McpServerIdentity;
|
||||
pub use config_requirements::McpServerRequirement;
|
||||
pub use config_requirements::NetworkConstraints;
|
||||
pub use config_requirements::NetworkDomainPermissionToml;
|
||||
pub use config_requirements::NetworkDomainPermissionsToml;
|
||||
pub use config_requirements::NetworkRequirementsToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionsToml;
|
||||
pub use config_requirements::RequirementSource;
|
||||
pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
|
||||
@@ -913,6 +913,16 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkDomainPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkDomainPermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkModeSchema": {
|
||||
"enum": [
|
||||
"limited",
|
||||
@@ -926,32 +936,17 @@
|
||||
"allow_local_binding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allow_unix_sockets": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"allow_upstream_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowed_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dangerously_allow_all_unix_sockets": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_non_loopback_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"denied_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
"domains": {
|
||||
"$ref": "#/definitions/NetworkDomainPermissionsToml"
|
||||
},
|
||||
"enable_socks5": {
|
||||
"type": "boolean"
|
||||
@@ -970,10 +965,23 @@
|
||||
},
|
||||
"socks_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"unix_sockets": {
|
||||
"$ref": "#/definitions/NetworkUnixSocketPermissionsToml"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkUnixSocketPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkUnixSocketPermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"Notice": {
|
||||
"description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.",
|
||||
"properties": {
|
||||
|
||||
@@ -1030,8 +1030,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(())
|
||||
}
|
||||
@@ -3712,8 +3719,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()
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -124,9 +124,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()])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,24 +37,29 @@ mode = "full" # default when unset; use "limited" for read-only mode
|
||||
mitm = false
|
||||
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
||||
|
||||
# Hosts must match the allowlist (unless denied).
|
||||
# Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`.
|
||||
# The global `*` wildcard is allowed in `allowed_domains` to delegate public-host filtering to
|
||||
# `denied_domains`.
|
||||
# If `allowed_domains` is empty, the proxy blocks requests until an allowlist is configured.
|
||||
allowed_domains = ["*.openai.com", "localhost", "127.0.0.1", "::1"]
|
||||
denied_domains = ["evil.example"]
|
||||
|
||||
# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
|
||||
# (or `localhost`) is required to permit them.
|
||||
# Hostnames that resolve to local/private IPs are still blocked even if allowlisted.
|
||||
allow_local_binding = false
|
||||
|
||||
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
|
||||
# absolute socket path from `x-unix-socket`.
|
||||
dangerously_allow_all_unix_sockets = false
|
||||
|
||||
# Hosts must match the allowlist (unless denied).
|
||||
# Use exact hosts or scoped wildcards like `*.openai.com` or `**.openai.com`.
|
||||
# The global `*` wildcard is rejected.
|
||||
# If no domain entries are marked `allow`, the proxy blocks requests until an allowlist is configured.
|
||||
[permissions.workspace.network.domains]
|
||||
"*.openai.com" = "allow"
|
||||
"localhost" = "allow"
|
||||
"127.0.0.1" = "allow"
|
||||
"::1" = "allow"
|
||||
"evil.example" = "deny"
|
||||
|
||||
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
||||
[permissions.workspace.network.unix_sockets]
|
||||
"/tmp/example.sock" = "allow"
|
||||
```
|
||||
|
||||
### 2) Run the proxy
|
||||
@@ -124,7 +129,7 @@ let handle = proxy.run().await?;
|
||||
handle.shutdown().await?;
|
||||
```
|
||||
|
||||
When unix socket proxying is enabled (`allow_unix_sockets` or
|
||||
When unix socket proxying is enabled (`unix_sockets` or
|
||||
`dangerously_allow_all_unix_sockets`), proxy bind overrides are still clamped to loopback to
|
||||
avoid turning the proxy into a remote bridge to local daemons.
|
||||
|
||||
@@ -189,9 +194,9 @@ Audit events intentionally avoid logging full URL/path/query data.
|
||||
This section documents the protections implemented by `codex-network-proxy`, and the boundaries of
|
||||
what it can reasonably guarantee.
|
||||
|
||||
- Allowlist-first policy: if `allowed_domains` is empty, requests are blocked until an allowlist is configured.
|
||||
- Domain patterns: exact hosts plus scoped wildcards (`*.example.com`, `**.example.com`) are supported. A global `*` wildcard is allowed in `allowed_domains` to permit all public hosts by default, while `denied_domains` remains field-specific and still rejects global `*`.
|
||||
- Deny wins: entries in `denied_domains` always override the allowlist.
|
||||
- Allowlist-first policy: if `domains` has no `allow` entries, requests are blocked until an allowlist is configured.
|
||||
- Domain patterns: exact hosts are supported, `*.example.com` matches subdomains only, and `**.example.com` matches the apex plus subdomains; the global `*` wildcard is only accepted when explicitly enabled for allowlist compilation and is otherwise rejected.
|
||||
- Deny wins: `domains` entries marked `deny` always override the allowlist.
|
||||
- Local/private network protection: when `allow_local_binding = false`, the proxy blocks loopback
|
||||
and common private/link-local ranges. Explicit allowlisting of local IP literals (or `localhost`)
|
||||
is required to permit them; hostnames that resolve to local/private IPs are still blocked even if
|
||||
|
||||
@@ -3,7 +3,10 @@ use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::IpAddr;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
@@ -16,6 +19,101 @@ pub struct NetworkProxyConfig {
|
||||
pub network: NetworkProxySettings,
|
||||
}
|
||||
|
||||
/// Variant order encodes effective precedence for duplicate patterns:
|
||||
/// `None < Allow < Deny`, so deny wins over allow when entries conflict.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkDomainPermission {
|
||||
None,
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NetworkDomainPermissionEntry {
|
||||
pub pattern: String,
|
||||
pub permission: NetworkDomainPermission,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkDomainPermissions {
|
||||
pub entries: Vec<NetworkDomainPermissionEntry>,
|
||||
}
|
||||
|
||||
impl Serialize for NetworkDomainPermissions {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.effective_entries()
|
||||
.into_iter()
|
||||
.map(|entry| (entry.pattern, entry.permission))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkDomainPermissions {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let entries = BTreeMap::<String, NetworkDomainPermission>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(pattern, permission)| NetworkDomainPermissionEntry {
|
||||
pattern,
|
||||
permission,
|
||||
})
|
||||
.collect();
|
||||
Ok(Self { entries })
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkDomainPermissions {
|
||||
fn effective_entries(&self) -> Vec<NetworkDomainPermissionEntry> {
|
||||
let mut order = Vec::new();
|
||||
let mut effective_permissions = BTreeMap::new();
|
||||
|
||||
for entry in &self.entries {
|
||||
if !effective_permissions.contains_key(&entry.pattern) {
|
||||
order.push(entry.pattern.clone());
|
||||
}
|
||||
|
||||
let permission = effective_permissions
|
||||
.entry(entry.pattern.clone())
|
||||
.or_insert(entry.permission);
|
||||
if entry.permission > *permission {
|
||||
*permission = entry.permission;
|
||||
}
|
||||
}
|
||||
|
||||
order
|
||||
.into_iter()
|
||||
.filter_map(|pattern| {
|
||||
effective_permissions.remove(&pattern).map(|permission| {
|
||||
NetworkDomainPermissionEntry {
|
||||
pattern,
|
||||
permission,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkUnixSocketPermission {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct NetworkUnixSocketPermissions {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, NetworkUnixSocketPermission>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkProxySettings {
|
||||
@@ -35,11 +133,9 @@ pub struct NetworkProxySettings {
|
||||
#[serde(default)]
|
||||
pub mode: NetworkMode,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Vec<String>,
|
||||
pub domains: Option<NetworkDomainPermissions>,
|
||||
#[serde(default)]
|
||||
pub denied_domains: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_unix_sockets: Vec<String>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
|
||||
pub allow_local_binding: bool,
|
||||
#[serde(default)]
|
||||
pub mitm: bool,
|
||||
@@ -57,15 +153,119 @@ impl Default for NetworkProxySettings {
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::default(),
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
allow_unix_sockets: Vec::new(),
|
||||
domains: None,
|
||||
unix_sockets: None,
|
||||
allow_local_binding: false,
|
||||
mitm: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkProxySettings {
|
||||
pub fn allowed_domains(&self) -> Option<Vec<String>> {
|
||||
self.domain_entries(NetworkDomainPermission::Allow)
|
||||
}
|
||||
|
||||
pub fn denied_domains(&self) -> Option<Vec<String>> {
|
||||
self.domain_entries(NetworkDomainPermission::Deny)
|
||||
}
|
||||
|
||||
fn domain_entries(&self, permission: NetworkDomainPermission) -> Option<Vec<String>> {
|
||||
self.domains
|
||||
.as_ref()
|
||||
.map(|domains| {
|
||||
domains
|
||||
.effective_entries()
|
||||
.iter()
|
||||
.filter(|entry| entry.permission == permission)
|
||||
.map(|entry| entry.pattern.clone())
|
||||
.collect()
|
||||
})
|
||||
.filter(|entries: &Vec<String>| !entries.is_empty())
|
||||
}
|
||||
|
||||
pub fn allow_unix_sockets(&self) -> Vec<String> {
|
||||
self.unix_sockets
|
||||
.as_ref()
|
||||
.map(|unix_sockets| {
|
||||
unix_sockets
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, permission)| {
|
||||
matches!(permission, NetworkUnixSocketPermission::Allow)
|
||||
})
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn set_allowed_domains(&mut self, allowed_domains: Vec<String>) {
|
||||
self.set_domain_entries(allowed_domains, NetworkDomainPermission::Allow);
|
||||
}
|
||||
|
||||
pub fn set_denied_domains(&mut self, denied_domains: Vec<String>) {
|
||||
self.set_domain_entries(denied_domains, NetworkDomainPermission::Deny);
|
||||
}
|
||||
|
||||
pub fn upsert_domain_permission(
|
||||
&mut self,
|
||||
host: String,
|
||||
permission: NetworkDomainPermission,
|
||||
normalize: impl Fn(&str) -> String,
|
||||
) {
|
||||
let mut domains = self.domains.take().unwrap_or_default();
|
||||
let normalized_host = normalize(&host);
|
||||
domains
|
||||
.entries
|
||||
.retain(|entry| normalize(&entry.pattern) != normalized_host);
|
||||
domains.entries.push(NetworkDomainPermissionEntry {
|
||||
pattern: host,
|
||||
permission,
|
||||
});
|
||||
self.domains = (!domains.entries.is_empty()).then_some(domains);
|
||||
}
|
||||
|
||||
pub fn set_allow_unix_sockets(&mut self, allow_unix_sockets: Vec<String>) {
|
||||
self.set_unix_socket_entries(allow_unix_sockets, NetworkUnixSocketPermission::Allow);
|
||||
}
|
||||
|
||||
fn set_domain_entries(&mut self, entries: Vec<String>, permission: NetworkDomainPermission) {
|
||||
let mut domains = self.domains.take().unwrap_or_default();
|
||||
domains
|
||||
.entries
|
||||
.retain(|entry| entry.permission != permission);
|
||||
for entry in entries {
|
||||
if !domains
|
||||
.entries
|
||||
.iter()
|
||||
.any(|existing| existing.pattern == entry && existing.permission == permission)
|
||||
{
|
||||
domains.entries.push(NetworkDomainPermissionEntry {
|
||||
pattern: entry,
|
||||
permission,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.domains = (!domains.entries.is_empty()).then_some(domains);
|
||||
}
|
||||
|
||||
fn set_unix_socket_entries(
|
||||
&mut self,
|
||||
entries: Vec<String>,
|
||||
permission: NetworkUnixSocketPermission,
|
||||
) {
|
||||
let mut unix_sockets = self.unix_sockets.take().unwrap_or_default();
|
||||
unix_sockets
|
||||
.entries
|
||||
.retain(|_, existing| *existing != permission);
|
||||
for entry in entries {
|
||||
unix_sockets.entries.insert(entry, permission);
|
||||
}
|
||||
self.unix_sockets = (!unix_sockets.entries.is_empty()).then_some(unix_sockets);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkMode {
|
||||
@@ -136,7 +336,7 @@ pub(crate) fn clamp_bind_addrs(
|
||||
"SOCKS5 proxy",
|
||||
"dangerously_allow_non_loopback_proxy",
|
||||
);
|
||||
if cfg.allow_unix_sockets.is_empty() && !cfg.dangerously_allow_all_unix_sockets {
|
||||
if cfg.allow_unix_sockets().is_empty() && !cfg.dangerously_allow_all_unix_sockets {
|
||||
return (http_addr, socks_addr);
|
||||
}
|
||||
|
||||
@@ -198,7 +398,7 @@ impl ValidatedUnixSocketPath {
|
||||
}
|
||||
|
||||
pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> {
|
||||
for (index, socket_path) in cfg.network.allow_unix_sockets.iter().enumerate() {
|
||||
for (index, socket_path) in cfg.network.allow_unix_sockets().iter().enumerate() {
|
||||
ValidatedUnixSocketPath::parse(socket_path)
|
||||
.with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
|
||||
}
|
||||
@@ -357,6 +557,19 @@ mod tests {
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn settings_with_unix_sockets(unix_sockets: &[&str]) -> NetworkProxySettings {
|
||||
let mut settings = NetworkProxySettings::default();
|
||||
if !unix_sockets.is_empty() {
|
||||
settings.set_allow_unix_sockets(
|
||||
unix_sockets
|
||||
.iter()
|
||||
.map(|path| (*path).to_string())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_proxy_settings_default_matches_local_use_baseline() {
|
||||
assert_eq!(
|
||||
@@ -371,9 +584,8 @@ mod tests {
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::Full,
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
allow_unix_sockets: Vec::new(),
|
||||
domains: None,
|
||||
unix_sockets: None,
|
||||
allow_local_binding: false,
|
||||
mitm: false,
|
||||
}
|
||||
@@ -398,6 +610,53 @@ mod tests {
|
||||
assert_eq!(config.network, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_allowed_domains_preserves_existing_deny_for_same_pattern() {
|
||||
let mut settings = NetworkProxySettings::default();
|
||||
settings.set_denied_domains(vec!["example.com".to_string()]);
|
||||
|
||||
settings.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
|
||||
assert_eq!(settings.allowed_domains(), None);
|
||||
assert_eq!(
|
||||
settings.denied_domains(),
|
||||
Some(vec!["example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_domain_permissions_serialize_to_effective_map_shape() {
|
||||
let mut settings = NetworkProxySettings::default();
|
||||
settings.set_denied_domains(vec!["example.com".to_string()]);
|
||||
settings.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
let config = NetworkProxyConfig { network: settings };
|
||||
|
||||
let value = serde_json::to_value(&config).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!({
|
||||
"network": {
|
||||
"enabled": false,
|
||||
"proxy_url": "http://127.0.0.1:3128",
|
||||
"enable_socks5": true,
|
||||
"socks_url": "http://127.0.0.1:8081",
|
||||
"enable_socks5_udp": true,
|
||||
"allow_upstream_proxy": true,
|
||||
"dangerously_allow_non_loopback_proxy": false,
|
||||
"dangerously_allow_all_unix_sockets": false,
|
||||
"mode": "full",
|
||||
"domains": {
|
||||
"example.com": "deny",
|
||||
},
|
||||
"unix_sockets": null,
|
||||
"allow_local_binding": false,
|
||||
"mitm": false,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_host_port_defaults_for_empty_string() {
|
||||
assert!(parse_host_port("", 1234).is_err());
|
||||
@@ -536,10 +795,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled() {
|
||||
let cfg = NetworkProxySettings {
|
||||
dangerously_allow_non_loopback_proxy: true,
|
||||
allow_unix_sockets: vec!["/tmp/docker.sock".to_string()],
|
||||
..Default::default()
|
||||
let cfg = {
|
||||
let mut settings = settings_with_unix_sockets(&["/tmp/docker.sock"]);
|
||||
settings.dangerously_allow_non_loopback_proxy = true;
|
||||
settings
|
||||
};
|
||||
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
||||
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
|
||||
@@ -569,10 +828,7 @@ mod tests {
|
||||
#[test]
|
||||
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
|
||||
let cfg = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
allow_unix_sockets: vec!["relative.sock".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
network: settings_with_unix_sockets(&["relative.sock"]),
|
||||
};
|
||||
|
||||
let err = match resolve_runtime(&cfg) {
|
||||
@@ -591,10 +847,7 @@ mod tests {
|
||||
#[test]
|
||||
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
|
||||
let cfg = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
allow_unix_sockets: vec!["/private/tmp/example.sock".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
network: settings_with_unix_sockets(&["/private/tmp/example.sock"]),
|
||||
};
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -1002,9 +1002,10 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_connect_accept_blocks_in_limited_mode() {
|
||||
let policy = NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..Default::default()
|
||||
let policy = {
|
||||
let mut policy = NetworkProxySettings::default();
|
||||
policy.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
policy
|
||||
};
|
||||
let state = Arc::new(network_proxy_state_for_policy(policy));
|
||||
state.set_network_mode(NetworkMode::Limited).await.unwrap();
|
||||
@@ -1027,9 +1028,10 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_connect_accept_allows_allowlisted_host_in_full_mode() {
|
||||
let policy = NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..Default::default()
|
||||
let policy = {
|
||||
let mut policy = NetworkProxySettings::default();
|
||||
policy.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
policy
|
||||
};
|
||||
let state = Arc::new(network_proxy_state_for_policy(policy));
|
||||
|
||||
@@ -1062,10 +1064,11 @@ mod tests {
|
||||
let _ = timeout(Duration::from_secs(1), stream.read(&mut buf)).await;
|
||||
});
|
||||
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["127.0.0.1".to_string()],
|
||||
allow_local_binding: true,
|
||||
..NetworkProxySettings::default()
|
||||
let state = Arc::new(network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["127.0.0.1".to_string()]);
|
||||
network.allow_local_binding = true;
|
||||
network
|
||||
}));
|
||||
let listener =
|
||||
StdTcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("proxy listener should bind");
|
||||
@@ -1161,9 +1164,10 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn http_plain_proxy_attempts_allowed_unix_socket_proxy() {
|
||||
let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allow_unix_sockets: vec!["/tmp/test.sock".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
let state = Arc::new(network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allow_unix_sockets(vec!["/tmp/test.sock".to_string()]);
|
||||
network
|
||||
}));
|
||||
|
||||
let mut req = Request::builder()
|
||||
@@ -1180,10 +1184,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_connect_accept_denies_denylisted_host() {
|
||||
let policy = NetworkProxySettings {
|
||||
allowed_domains: vec!["**.openai.com".to_string()],
|
||||
denied_domains: vec!["api.openai.com".to_string()],
|
||||
..Default::default()
|
||||
let policy = {
|
||||
let mut policy = NetworkProxySettings::default();
|
||||
policy.set_allowed_domains(vec!["**.openai.com".to_string()]);
|
||||
policy.set_denied_domains(vec!["api.openai.com".to_string()]);
|
||||
policy
|
||||
};
|
||||
let state = Arc::new(network_proxy_state_for_policy(policy));
|
||||
|
||||
|
||||
@@ -14,8 +14,13 @@ mod socks5;
|
||||
mod state;
|
||||
mod upstream;
|
||||
|
||||
pub use config::NetworkDomainPermission;
|
||||
pub use config::NetworkDomainPermissionEntry;
|
||||
pub use config::NetworkDomainPermissions;
|
||||
pub use config::NetworkMode;
|
||||
pub use config::NetworkProxyConfig;
|
||||
pub use config::NetworkUnixSocketPermission;
|
||||
pub use config::NetworkUnixSocketPermissions;
|
||||
pub use config::host_and_port_from_network_addr;
|
||||
pub use network_policy::NetworkDecision;
|
||||
pub use network_policy::NetworkDecisionSource;
|
||||
|
||||
@@ -26,9 +26,10 @@ fn policy_ctx(
|
||||
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_blocks_disallowed_method_and_records_telemetry() {
|
||||
let app_state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
let app_state = Arc::new(network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
network
|
||||
}));
|
||||
let ctx = policy_ctx(app_state.clone(), NetworkMode::Limited, "example.com", 443);
|
||||
let req = Request::builder()
|
||||
@@ -59,9 +60,10 @@ async fn mitm_policy_blocks_disallowed_method_and_records_telemetry() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_rejects_host_mismatch() {
|
||||
let app_state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
let app_state = Arc::new(network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
network
|
||||
}));
|
||||
let ctx = policy_ctx(app_state.clone(), NetworkMode::Full, "example.com", 443);
|
||||
let req = Request::builder()
|
||||
@@ -82,10 +84,11 @@ async fn mitm_policy_rejects_host_mismatch() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn mitm_policy_rechecks_local_private_target_after_connect() {
|
||||
let app_state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
let app_state = Arc::new(network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
network.allow_local_binding = false;
|
||||
network
|
||||
}));
|
||||
let ctx = policy_ctx(app_state.clone(), NetworkMode::Full, "10.0.0.1", 443);
|
||||
let req = Request::builder()
|
||||
|
||||
@@ -676,10 +676,11 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn evaluate_host_policy_emits_domain_event_for_baseline_deny() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["blocked.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
let state = network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
network.set_denied_domains(vec!["blocked.com".to_string()]);
|
||||
network
|
||||
});
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::Http,
|
||||
@@ -850,10 +851,11 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn evaluate_host_policy_still_denies_not_allowed_local_without_decider_override() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
let state = network_proxy_state_for_policy({
|
||||
let mut network = NetworkProxySettings::default();
|
||||
network.set_allowed_domains(vec!["example.com".to_string()]);
|
||||
network.allow_local_binding = false;
|
||||
network
|
||||
});
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::Http,
|
||||
|
||||
@@ -180,7 +180,7 @@ impl NetworkProxyBuilder {
|
||||
socks_addr,
|
||||
socks_enabled: current_cfg.network.enable_socks5,
|
||||
allow_local_binding: current_cfg.network.allow_local_binding,
|
||||
allow_unix_sockets: current_cfg.network.allow_unix_sockets.clone(),
|
||||
allow_unix_sockets: current_cfg.network.allow_unix_sockets(),
|
||||
dangerously_allow_all_unix_sockets: current_cfg
|
||||
.network
|
||||
.dangerously_allow_all_unix_sockets,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::NetworkDomainPermission;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::config::ValidatedUnixSocketPath;
|
||||
@@ -295,8 +296,8 @@ impl NetworkProxyState {
|
||||
self.reload_if_needed().await?;
|
||||
let guard = self.state.read().await;
|
||||
Ok((
|
||||
guard.config.network.allowed_domains.clone(),
|
||||
guard.config.network.denied_domains.clone(),
|
||||
guard.config.network.allowed_domains().unwrap_or_default(),
|
||||
guard.config.network.denied_domains().unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -340,16 +341,18 @@ impl NetworkProxyState {
|
||||
Ok(host) => host,
|
||||
Err(_) => return Ok(HostBlockDecision::Blocked(HostBlockReason::NotAllowed)),
|
||||
};
|
||||
let (deny_set, allow_set, allow_local_binding, allowed_domains_empty, allowed_domains) = {
|
||||
let (deny_set, allow_set, allow_local_binding, allowed_domains) = {
|
||||
let guard = self.state.read().await;
|
||||
let allowed_domains = guard.config.network.allowed_domains();
|
||||
(
|
||||
guard.deny_set.clone(),
|
||||
guard.allow_set.clone(),
|
||||
guard.config.network.allow_local_binding,
|
||||
guard.config.network.allowed_domains.is_empty(),
|
||||
guard.config.network.allowed_domains.clone(),
|
||||
allowed_domains,
|
||||
)
|
||||
};
|
||||
let allowed_domains_empty = allowed_domains.is_none();
|
||||
let allowed_domains = allowed_domains.unwrap_or_default();
|
||||
|
||||
let host_str = host.as_str();
|
||||
|
||||
@@ -481,7 +484,7 @@ impl NetworkProxyState {
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
let requested_canonical = std::fs::canonicalize(requested_abs.as_path()).ok();
|
||||
for allowed in &guard.config.network.allow_unix_sockets {
|
||||
for allowed in &guard.config.network.allow_unix_sockets() {
|
||||
let allowed_path = match ValidatedUnixSocketPath::parse(allowed) {
|
||||
Ok(ValidatedUnixSocketPath::Native(path)) => path,
|
||||
Ok(ValidatedUnixSocketPath::UnixStyleAbsolute(_)) => continue,
|
||||
@@ -585,7 +588,8 @@ impl NetworkProxyState {
|
||||
};
|
||||
|
||||
let mut candidate = previous_cfg.clone();
|
||||
let (target_entries, opposite_entries) = candidate.split_domain_lists_mut(target);
|
||||
let target_entries = target.entries(&candidate.network);
|
||||
let opposite_entries = target.opposite_entries(&candidate.network);
|
||||
let target_contains = target_entries
|
||||
.iter()
|
||||
.any(|entry| normalize_host(entry) == normalized_host);
|
||||
@@ -596,9 +600,11 @@ impl NetworkProxyState {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
target_entries.retain(|entry| normalize_host(entry) != normalized_host);
|
||||
target_entries.push(normalized_host.clone());
|
||||
opposite_entries.retain(|entry| normalize_host(entry) != normalized_host);
|
||||
candidate.network.upsert_domain_permission(
|
||||
normalized_host.clone(),
|
||||
target.permission(),
|
||||
normalize_host,
|
||||
);
|
||||
|
||||
validate_policy_against_constraints(&candidate, &constraints)
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)
|
||||
@@ -669,22 +675,25 @@ impl DomainListKind {
|
||||
Self::Deny => "network.denied_domains",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkProxyConfig {
|
||||
fn split_domain_lists_mut(
|
||||
&mut self,
|
||||
target: DomainListKind,
|
||||
) -> (&mut Vec<String>, &mut Vec<String>) {
|
||||
match target {
|
||||
DomainListKind::Allow => (
|
||||
&mut self.network.allowed_domains,
|
||||
&mut self.network.denied_domains,
|
||||
),
|
||||
DomainListKind::Deny => (
|
||||
&mut self.network.denied_domains,
|
||||
&mut self.network.allowed_domains,
|
||||
),
|
||||
fn permission(self) -> NetworkDomainPermission {
|
||||
match self {
|
||||
Self::Allow => NetworkDomainPermission::Allow,
|
||||
Self::Deny => NetworkDomainPermission::Deny,
|
||||
}
|
||||
}
|
||||
|
||||
fn entries(self, network: &crate::config::NetworkProxySettings) -> Vec<String> {
|
||||
match self {
|
||||
Self::Allow => network.allowed_domains().unwrap_or_default(),
|
||||
Self::Deny => network.denied_domains().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn opposite_entries(self, network: &crate::config::NetworkProxySettings) -> Vec<String> {
|
||||
match self {
|
||||
Self::Allow => network.denied_domains().unwrap_or_default(),
|
||||
Self::Deny => network.allowed_domains().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,16 +725,16 @@ async fn host_resolves_to_non_public_ip(host: &str, port: u16) -> bool {
|
||||
}
|
||||
|
||||
fn log_policy_changes(previous: &NetworkProxyConfig, next: &NetworkProxyConfig) {
|
||||
let previous_allowed_domains = previous.network.allowed_domains().unwrap_or_default();
|
||||
let next_allowed_domains = next.network.allowed_domains().unwrap_or_default();
|
||||
log_domain_list_changes(
|
||||
"allowlist",
|
||||
&previous.network.allowed_domains,
|
||||
&next.network.allowed_domains,
|
||||
);
|
||||
log_domain_list_changes(
|
||||
"denylist",
|
||||
&previous.network.denied_domains,
|
||||
&next.network.denied_domains,
|
||||
&previous_allowed_domains,
|
||||
&next_allowed_domains,
|
||||
);
|
||||
let previous_denied_domains = previous.network.denied_domains().unwrap_or_default();
|
||||
let next_denied_domains = next.network.denied_domains().unwrap_or_default();
|
||||
log_domain_list_changes("denylist", &previous_denied_domains, &next_denied_domains);
|
||||
}
|
||||
|
||||
fn log_domain_list_changes(list_name: &str, previous: &[String], next: &[String]) {
|
||||
@@ -826,13 +835,37 @@ mod tests {
|
||||
use crate::state::validate_policy_against_constraints;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn strings(entries: &[&str]) -> Vec<String> {
|
||||
entries.iter().map(|entry| (*entry).to_string()).collect()
|
||||
}
|
||||
|
||||
fn network_settings(allowed_domains: &[&str], denied_domains: &[&str]) -> NetworkProxySettings {
|
||||
let mut network = NetworkProxySettings::default();
|
||||
if !allowed_domains.is_empty() {
|
||||
network.set_allowed_domains(strings(allowed_domains));
|
||||
}
|
||||
if !denied_domains.is_empty() {
|
||||
network.set_denied_domains(strings(denied_domains));
|
||||
}
|
||||
network
|
||||
}
|
||||
|
||||
fn network_settings_with_unix_sockets(
|
||||
allowed_domains: &[&str],
|
||||
denied_domains: &[&str],
|
||||
unix_sockets: &[String],
|
||||
) -> NetworkProxySettings {
|
||||
let mut network = network_settings(allowed_domains, denied_domains);
|
||||
if !unix_sockets.is_empty() {
|
||||
network.set_allow_unix_sockets(unix_sockets.to_vec());
|
||||
}
|
||||
network
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_denied_wins_over_allowed() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state =
|
||||
network_proxy_state_for_policy(network_settings(&["example.com"], &["example.com"]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("example.com", 80).await.unwrap(),
|
||||
@@ -842,10 +875,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_requires_allowlist_match() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["example.com"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("example.com", 80).await.unwrap(),
|
||||
@@ -861,10 +891,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_allowed_domain_removes_matching_deny_entry() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
denied_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&[], &["example.com"]));
|
||||
|
||||
state.add_allowed_domain("ExAmPlE.CoM").await.unwrap();
|
||||
|
||||
@@ -879,10 +906,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_denied_domain_removes_matching_allow_entry() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["example.com"], &[]));
|
||||
|
||||
state.add_denied_domain("EXAMPLE.COM").await.unwrap();
|
||||
|
||||
@@ -897,10 +921,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_denied_domain_forces_block_with_global_wildcard_allowlist() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["*"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("evil.example", 80).await.unwrap(),
|
||||
@@ -921,10 +942,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn add_allowed_domain_succeeds_when_managed_baseline_allows_expansion() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["managed.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["managed.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
let constraints = NetworkProxyConstraints {
|
||||
@@ -953,10 +974,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn add_allowed_domain_rejects_expansion_when_managed_baseline_is_fixed() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["managed.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["managed.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
let constraints = NetworkProxyConstraints {
|
||||
@@ -983,10 +1004,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn add_denied_domain_rejects_expansion_when_managed_baseline_is_fixed() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
denied_domains: vec!["managed.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&[], &["managed.example.com"]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
let constraints = NetworkProxyConstraints {
|
||||
@@ -1098,10 +1119,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_subdomain_wildcards_exclude_apex() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*.openai.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["*.openai.com"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("api.openai.com", 80).await.unwrap(),
|
||||
@@ -1115,11 +1133,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_global_wildcard_allowlist_allows_public_hosts_except_denylist() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
denied_domains: vec!["evil.example".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["*"], &["evil.example"]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("example.com", 80).await.unwrap(),
|
||||
@@ -1137,11 +1151,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_loopback_when_local_binding_disabled() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["example.com"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("127.0.0.1", 80).await.unwrap(),
|
||||
@@ -1155,11 +1165,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_allows_loopback_when_explicitly_allowlisted_and_local_binding_disabled() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["localhost".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["localhost"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("localhost", 80).await.unwrap(),
|
||||
@@ -1169,11 +1175,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_allows_private_ip_literal_when_explicitly_allowlisted() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["10.0.0.1".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["10.0.0.1"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("10.0.0.1", 80).await.unwrap(),
|
||||
@@ -1183,11 +1185,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_scoped_ipv6_literal_when_not_allowlisted() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["example.com"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("fe80::1%lo0", 80).await.unwrap(),
|
||||
@@ -1197,11 +1195,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_allows_scoped_ipv6_literal_when_explicitly_allowlisted() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["fe80::1%lo0".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["fe80::1%lo0"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("fe80::1%lo0", 80).await.unwrap(),
|
||||
@@ -1211,11 +1205,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_private_ip_literals_when_local_binding_disabled() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings(&["example.com"], &[]));
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("10.0.0.1", 80).await.unwrap(),
|
||||
@@ -1225,11 +1215,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_blocked_rejects_loopback_when_allowlist_empty() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec![],
|
||||
allow_local_binding: false,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings::default());
|
||||
|
||||
assert_eq!(
|
||||
state.host_blocked("127.0.0.1", 80).await.unwrap(),
|
||||
@@ -1245,10 +1231,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string(), "evil.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["example.com", "evil.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1264,10 +1250,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string(), "api.openai.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["example.com", "api.openai.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1300,10 +1286,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["api.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1318,10 +1304,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["**.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["**.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1336,10 +1322,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["api.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1355,10 +1341,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["api.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1374,10 +1360,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["api.example.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["api.example.com"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1394,7 +1380,6 @@ mod tests {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
denied_domains: vec![],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
@@ -1411,10 +1396,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
denied_domains: vec!["evil.com".to_string(), "more-evil.com".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&[], &["evil.com", "more-evil.com"]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1598,10 +1583,10 @@ mod tests {
|
||||
#[test]
|
||||
fn build_config_state_allows_global_wildcard_allowed_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["*".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["*"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1611,10 +1596,10 @@ mod tests {
|
||||
#[test]
|
||||
fn build_config_state_allows_bracketed_global_wildcard_allowed_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["[*]".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["[*]"], &[]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1624,11 +1609,10 @@ mod tests {
|
||||
#[test]
|
||||
fn build_config_state_rejects_global_wildcard_denied_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["*".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["example.com"], &["*"]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1638,11 +1622,10 @@ mod tests {
|
||||
#[test]
|
||||
fn build_config_state_rejects_bracketed_global_wildcard_denied_domains() {
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
denied_domains: vec!["[*]".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
network: {
|
||||
let mut network = network_settings(&["example.com"], &["[*]"]);
|
||||
network.enabled = true;
|
||||
network
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1653,11 +1636,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allowlist_is_respected_on_macos() {
|
||||
let socket_path = "/tmp/example.sock".to_string();
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_unix_sockets: vec![socket_path.clone()],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings_with_unix_sockets(
|
||||
&["example.com"],
|
||||
&[],
|
||||
std::slice::from_ref(&socket_path),
|
||||
));
|
||||
|
||||
assert!(state.is_unix_socket_allowed(&socket_path).await.unwrap());
|
||||
assert!(
|
||||
@@ -1688,11 +1671,11 @@ mod tests {
|
||||
let real_s = real.to_str().unwrap().to_string();
|
||||
let link_s = link.to_str().unwrap().to_string();
|
||||
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_unix_sockets: vec![real_s],
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
let state = network_proxy_state_for_policy(network_settings_with_unix_sockets(
|
||||
&["example.com"],
|
||||
&[],
|
||||
std::slice::from_ref(&real_s),
|
||||
));
|
||||
|
||||
assert!(state.is_unix_socket_allowed(&link_s).await.unwrap());
|
||||
}
|
||||
@@ -1700,10 +1683,10 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allow_all_flag_bypasses_allowlist() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
let state = network_proxy_state_for_policy({
|
||||
let mut network = network_settings(&["example.com"], &[]);
|
||||
network.dangerously_allow_all_unix_sockets = true;
|
||||
network
|
||||
});
|
||||
|
||||
assert!(state.is_unix_socket_allowed("/tmp/any.sock").await.unwrap());
|
||||
@@ -1714,11 +1697,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allowlist_is_rejected_on_non_macos() {
|
||||
let socket_path = "/tmp/example.sock".to_string();
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_unix_sockets: vec![socket_path.clone()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
let state = network_proxy_state_for_policy({
|
||||
let mut network = network_settings_with_unix_sockets(
|
||||
&["example.com"],
|
||||
&[],
|
||||
std::slice::from_ref(&socket_path),
|
||||
);
|
||||
network.dangerously_allow_all_unix_sockets = true;
|
||||
network
|
||||
});
|
||||
|
||||
assert!(!state.is_unix_socket_allowed(&socket_path).await.unwrap());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::config::NetworkDomainPermissions;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::config::NetworkUnixSocketPermissions;
|
||||
use crate::mitm::MitmState;
|
||||
use crate::policy::DomainPattern;
|
||||
use crate::policy::compile_allowlist_globset;
|
||||
@@ -46,12 +48,9 @@ pub struct PartialNetworkConfig {
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub domains: Option<NetworkDomainPermissions>,
|
||||
#[serde(default)]
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -60,10 +59,12 @@ pub fn build_config_state(
|
||||
constraints: NetworkProxyConstraints,
|
||||
) -> anyhow::Result<ConfigState> {
|
||||
crate::config::validate_unix_socket_allowlist_paths(&config)?;
|
||||
validate_denylist_domain_patterns("network.denied_domains", &config.network.denied_domains)
|
||||
let allowed_domains = config.network.allowed_domains().unwrap_or_default();
|
||||
let denied_domains = config.network.denied_domains().unwrap_or_default();
|
||||
validate_non_global_wildcard_domain_patterns("network.denied_domains", &denied_domains)
|
||||
.map_err(NetworkProxyConstraintError::into_anyhow)?;
|
||||
let deny_set = compile_denylist_globset(&config.network.denied_domains)?;
|
||||
let allow_set = compile_allowlist_globset(&config.network.allowed_domains)?;
|
||||
let deny_set = compile_denylist_globset(&denied_domains)?;
|
||||
let allow_set = compile_allowlist_globset(&allowed_domains)?;
|
||||
let mitm = if config.network.mitm {
|
||||
Some(Arc::new(MitmState::new(
|
||||
config.network.allow_upstream_proxy,
|
||||
@@ -106,7 +107,14 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
let enabled = config.network.enabled;
|
||||
validate_denylist_domain_patterns("network.denied_domains", &config.network.denied_domains)?;
|
||||
let config_allowed_domains = config.network.allowed_domains().unwrap_or_default();
|
||||
let config_denied_domains = config.network.denied_domains().unwrap_or_default();
|
||||
let denied_domain_overrides: HashSet<String> = config_denied_domains
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
let config_allow_unix_sockets = config.network.allow_unix_sockets();
|
||||
validate_non_global_wildcard_domain_patterns("network.denied_domains", &config_denied_domains)?;
|
||||
if let Some(max_enabled) = constraints.enabled {
|
||||
validate(enabled, move |candidate| {
|
||||
if *candidate && !max_enabled {
|
||||
@@ -206,20 +214,24 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
if let Some(allowed_domains) = &constraints.allowed_domains {
|
||||
validate_non_global_wildcard_domain_patterns("network.allowed_domains", allowed_domains)?;
|
||||
match constraints.allowlist_expansion_enabled {
|
||||
Some(true) => {
|
||||
let required_set: HashSet<String> = allowed_domains
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
validate(config.network.allowed_domains.clone(), move |candidate| {
|
||||
validate(config_allowed_domains, |candidate| {
|
||||
let candidate_set: HashSet<String> = candidate
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
let missing: Vec<String> = required_set
|
||||
.iter()
|
||||
.filter(|entry| !candidate_set.contains(*entry))
|
||||
.filter(|entry| {
|
||||
!candidate_set.contains(*entry)
|
||||
&& !denied_domain_overrides.contains(*entry)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if missing.is_empty() {
|
||||
@@ -238,12 +250,16 @@ pub fn validate_policy_against_constraints(
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
validate(config.network.allowed_domains.clone(), move |candidate| {
|
||||
validate(config_allowed_domains, |candidate| {
|
||||
let candidate_set: HashSet<String> = candidate
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
if candidate_set == required_set {
|
||||
let expected_set: HashSet<String> = required_set
|
||||
.difference(&denied_domain_overrides)
|
||||
.cloned()
|
||||
.collect();
|
||||
if candidate_set == expected_set {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid_value(
|
||||
@@ -259,7 +275,7 @@ pub fn validate_policy_against_constraints(
|
||||
.iter()
|
||||
.map(|entry| DomainPattern::parse_for_constraints(entry))
|
||||
.collect();
|
||||
validate(config.network.allowed_domains.clone(), move |candidate| {
|
||||
validate(config_allowed_domains, move |candidate| {
|
||||
let mut invalid = Vec::new();
|
||||
for entry in candidate {
|
||||
let candidate_pattern = DomainPattern::parse_for_constraints(entry);
|
||||
@@ -285,14 +301,14 @@ pub fn validate_policy_against_constraints(
|
||||
}
|
||||
|
||||
if let Some(denied_domains) = &constraints.denied_domains {
|
||||
validate_denylist_domain_patterns("network.denied_domains", denied_domains)?;
|
||||
validate_non_global_wildcard_domain_patterns("network.denied_domains", denied_domains)?;
|
||||
let required_set: HashSet<String> = denied_domains
|
||||
.iter()
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
.collect();
|
||||
match constraints.denylist_expansion_enabled {
|
||||
Some(false) => {
|
||||
validate(config.network.denied_domains.clone(), move |candidate| {
|
||||
validate(config_denied_domains, move |candidate| {
|
||||
let candidate_set: HashSet<String> = candidate
|
||||
.iter()
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
@@ -309,7 +325,7 @@ pub fn validate_policy_against_constraints(
|
||||
})?;
|
||||
}
|
||||
Some(true) | None => {
|
||||
validate(config.network.denied_domains.clone(), move |candidate| {
|
||||
validate(config_denied_domains, move |candidate| {
|
||||
let candidate_set: HashSet<String> =
|
||||
candidate.iter().map(|s| s.to_ascii_lowercase()).collect();
|
||||
let missing: Vec<String> = required_set
|
||||
@@ -336,32 +352,29 @@ pub fn validate_policy_against_constraints(
|
||||
.iter()
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
.collect();
|
||||
validate(
|
||||
config.network.allow_unix_sockets.clone(),
|
||||
move |candidate| {
|
||||
let mut invalid = Vec::new();
|
||||
for entry in candidate {
|
||||
if !allowed_set.contains(&entry.to_ascii_lowercase()) {
|
||||
invalid.push(entry.clone());
|
||||
}
|
||||
validate(config_allow_unix_sockets, move |candidate| {
|
||||
let mut invalid = Vec::new();
|
||||
for entry in candidate {
|
||||
if !allowed_set.contains(&entry.to_ascii_lowercase()) {
|
||||
invalid.push(entry.clone());
|
||||
}
|
||||
if invalid.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid_value(
|
||||
"network.allow_unix_sockets",
|
||||
format!("{invalid:?}"),
|
||||
"subset of managed allow_unix_sockets",
|
||||
))
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
if invalid.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid_value(
|
||||
"network.allow_unix_sockets",
|
||||
format!("{invalid:?}"),
|
||||
"subset of managed allow_unix_sockets",
|
||||
))
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_denylist_domain_patterns(
|
||||
fn validate_non_global_wildcard_domain_patterns(
|
||||
field_name: &'static str,
|
||||
patterns: &[String],
|
||||
) -> Result<(), NetworkProxyConstraintError> {
|
||||
@@ -401,3 +414,6 @@ fn network_mode_rank(mode: NetworkMode) -> u8 {
|
||||
NetworkMode::Full => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_core::config_loader::ConfigLayerEntry;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -333,10 +335,9 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
unix_sockets,
|
||||
allow_local_binding,
|
||||
} = network;
|
||||
|
||||
@@ -362,21 +363,24 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
|
||||
));
|
||||
}
|
||||
if let Some(allowed_domains) = allowed_domains {
|
||||
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
|
||||
if let Some(domains) = domains {
|
||||
parts.push(format!(
|
||||
"domains={}",
|
||||
format_network_permission_entries(&domains.entries, format_network_domain_permission)
|
||||
));
|
||||
}
|
||||
if let Some(managed_allowed_domains_only) = managed_allowed_domains_only {
|
||||
parts.push(format!(
|
||||
"managed_allowed_domains_only={managed_allowed_domains_only}"
|
||||
));
|
||||
}
|
||||
if let Some(denied_domains) = denied_domains {
|
||||
parts.push(format!("denied_domains=[{}]", denied_domains.join(", ")));
|
||||
}
|
||||
if let Some(allow_unix_sockets) = allow_unix_sockets {
|
||||
if let Some(unix_sockets) = unix_sockets {
|
||||
parts.push(format!(
|
||||
"allow_unix_sockets=[{}]",
|
||||
allow_unix_sockets.join(", ")
|
||||
"unix_sockets={}",
|
||||
format_network_permission_entries(
|
||||
&unix_sockets.entries,
|
||||
format_network_unix_socket_permission,
|
||||
)
|
||||
));
|
||||
}
|
||||
if let Some(allow_local_binding) = allow_local_binding {
|
||||
@@ -386,6 +390,33 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
fn format_network_permission_entries<T: Copy>(
|
||||
entries: &std::collections::BTreeMap<String, T>,
|
||||
format_value: impl Fn(T) -> &'static str,
|
||||
) -> String {
|
||||
let parts = entries
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={}", format_value(*value)))
|
||||
.collect::<Vec<_>>();
|
||||
format!("{{{}}}", parts.join(", "))
|
||||
}
|
||||
|
||||
fn format_network_domain_permission(permission: NetworkDomainPermissionToml) -> &'static str {
|
||||
match permission {
|
||||
NetworkDomainPermissionToml::Allow => "allow",
|
||||
NetworkDomainPermissionToml::Deny => "deny",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_network_unix_socket_permission(
|
||||
permission: NetworkUnixSocketPermissionToml,
|
||||
) -> &'static str {
|
||||
match permission {
|
||||
NetworkUnixSocketPermissionToml::Allow => "allow",
|
||||
NetworkUnixSocketPermissionToml::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_debug_config_lines;
|
||||
@@ -400,6 +431,8 @@ mod tests {
|
||||
use codex_core::config_loader::McpServerIdentity;
|
||||
use codex_core::config_loader::McpServerRequirement;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -516,7 +549,12 @@ mod tests {
|
||||
network: Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
@@ -580,7 +618,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, allowed_domains=[example.com] (source: cloud requirements)"
|
||||
"experimental_network: enabled=true, domains={example.com=allow} (source: cloud requirements)"
|
||||
));
|
||||
assert!(!rendered.contains(" - rules:"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_core::config_loader::ConfigLayerEntry;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -333,10 +335,9 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
domains,
|
||||
managed_allowed_domains_only,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
unix_sockets,
|
||||
allow_local_binding,
|
||||
} = network;
|
||||
|
||||
@@ -362,21 +363,24 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
"dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
|
||||
));
|
||||
}
|
||||
if let Some(allowed_domains) = allowed_domains {
|
||||
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
|
||||
if let Some(domains) = domains {
|
||||
parts.push(format!(
|
||||
"domains={}",
|
||||
format_network_permission_entries(&domains.entries, format_network_domain_permission)
|
||||
));
|
||||
}
|
||||
if let Some(managed_allowed_domains_only) = managed_allowed_domains_only {
|
||||
parts.push(format!(
|
||||
"managed_allowed_domains_only={managed_allowed_domains_only}"
|
||||
));
|
||||
}
|
||||
if let Some(denied_domains) = denied_domains {
|
||||
parts.push(format!("denied_domains=[{}]", denied_domains.join(", ")));
|
||||
}
|
||||
if let Some(allow_unix_sockets) = allow_unix_sockets {
|
||||
if let Some(unix_sockets) = unix_sockets {
|
||||
parts.push(format!(
|
||||
"allow_unix_sockets=[{}]",
|
||||
allow_unix_sockets.join(", ")
|
||||
"unix_sockets={}",
|
||||
format_network_permission_entries(
|
||||
&unix_sockets.entries,
|
||||
format_network_unix_socket_permission,
|
||||
)
|
||||
));
|
||||
}
|
||||
if let Some(allow_local_binding) = allow_local_binding {
|
||||
@@ -386,6 +390,33 @@ fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
fn format_network_permission_entries<T: Copy>(
|
||||
entries: &std::collections::BTreeMap<String, T>,
|
||||
format_value: impl Fn(T) -> &'static str,
|
||||
) -> String {
|
||||
let parts = entries
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={}", format_value(*value)))
|
||||
.collect::<Vec<_>>();
|
||||
format!("{{{}}}", parts.join(", "))
|
||||
}
|
||||
|
||||
fn format_network_domain_permission(permission: NetworkDomainPermissionToml) -> &'static str {
|
||||
match permission {
|
||||
NetworkDomainPermissionToml::Allow => "allow",
|
||||
NetworkDomainPermissionToml::Deny => "deny",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_network_unix_socket_permission(
|
||||
permission: NetworkUnixSocketPermissionToml,
|
||||
) -> &'static str {
|
||||
match permission {
|
||||
NetworkUnixSocketPermissionToml::Allow => "allow",
|
||||
NetworkUnixSocketPermissionToml::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_debug_config_lines;
|
||||
@@ -400,6 +431,10 @@ mod tests {
|
||||
use codex_core::config_loader::McpServerIdentity;
|
||||
use codex_core::config_loader::McpServerRequirement;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionsToml;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -516,7 +551,12 @@ mod tests {
|
||||
network: Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
@@ -580,10 +620,45 @@ 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, allowed_domains=[example.com] (source: cloud requirements)"
|
||||
"experimental_network: enabled=true, domains={example.com=allow} (source: cloud requirements)"
|
||||
));
|
||||
assert!(!rendered.contains(" - rules:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_formats_unix_socket_permissions() {
|
||||
let requirements = ConfigRequirements {
|
||||
network: Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
unix_sockets: Some(NetworkUnixSocketPermissionsToml {
|
||||
entries: BTreeMap::from([
|
||||
(
|
||||
"/tmp/codex.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"/tmp/blocked.sock".to_string(),
|
||||
NetworkUnixSocketPermissionToml::None,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
)),
|
||||
..ConfigRequirements::default()
|
||||
};
|
||||
|
||||
let stack =
|
||||
ConfigLayerStack::new(Vec::new(), requirements, ConfigRequirementsToml::default())
|
||||
.expect("config layer stack");
|
||||
|
||||
let rendered = render_to_text(&render_debug_config_lines(&stack));
|
||||
assert!(rendered.contains(
|
||||
"experimental_network: unix_sockets={/tmp/blocked.sock=none, /tmp/codex.sock=allow} (source: cloud requirements)"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_lists_session_flag_key_value_pairs() {
|
||||
let session_flags = toml::from_str::<TomlValue>(
|
||||
|
||||
Reference in New Issue
Block a user