mirror of
https://github.com/openai/codex.git
synced 2026-03-19 04:16:30 +03:00
Compare commits
2 Commits
latest-alp
...
dev/cc/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e3f8c9da0 | ||
|
|
8bbe08d23d |
@@ -898,6 +898,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkDomainPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkModeSchema": {
|
||||
"enum": [
|
||||
"limited",
|
||||
@@ -911,32 +919,20 @@
|
||||
"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"
|
||||
"domains": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkDomainPermissionToml"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "object"
|
||||
},
|
||||
"enable_socks5": {
|
||||
"type": "boolean"
|
||||
@@ -955,10 +951,23 @@
|
||||
},
|
||||
"socks_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"unix_sockets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkUnixSocketPermissionToml"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkUnixSocketPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -280,7 +280,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");
|
||||
@@ -317,9 +319,11 @@ 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(BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
}),
|
||||
},
|
||||
@@ -395,7 +399,10 @@ 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(BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -123,7 +123,9 @@ 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::NetworkToml;
|
||||
pub use permissions::NetworkUnixSocketPermissionToml;
|
||||
pub use permissions::PermissionProfileToml;
|
||||
pub use permissions::PermissionsToml;
|
||||
pub(crate) use permissions::resolve_permission_profile;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -69,12 +70,32 @@ 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<BTreeMap<String, NetworkDomainPermissionToml>>,
|
||||
pub unix_sockets: Option<BTreeMap<String, NetworkUnixSocketPermissionToml>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkDomainPermissionToml {
|
||||
Allow,
|
||||
Deny,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkUnixSocketPermissionToml {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ResolvedNetworkDomainEntry {
|
||||
pub domain: String,
|
||||
pub permission: NetworkDomainPermissionToml,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum NetworkModeSchema {
|
||||
@@ -114,14 +135,20 @@ 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 self.domains.is_some() {
|
||||
let resolved_domain_entries = self.resolved_domain_entries();
|
||||
let mut allowed_domains = Vec::new();
|
||||
let mut denied_domains = Vec::new();
|
||||
apply_resolved_domain_entries(
|
||||
&mut allowed_domains,
|
||||
&mut denied_domains,
|
||||
&resolved_domain_entries,
|
||||
);
|
||||
config.network.allowed_domains = allowed_domains;
|
||||
config.network.denied_domains = denied_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(allow_unix_sockets) = self.compile_unix_socket_permissions() {
|
||||
config.network.allow_unix_sockets = allow_unix_sockets;
|
||||
}
|
||||
if let Some(allow_local_binding) = self.allow_local_binding {
|
||||
config.network.allow_local_binding = allow_local_binding;
|
||||
@@ -133,6 +160,98 @@ impl NetworkToml {
|
||||
self.apply_to_network_proxy_config(&mut config);
|
||||
config
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_domain_entries(&self) -> Vec<ResolvedNetworkDomainEntry> {
|
||||
self.domains
|
||||
.as_ref()
|
||||
.map(|domains| {
|
||||
domains
|
||||
.iter()
|
||||
.map(|(domain, permission)| ResolvedNetworkDomainEntry {
|
||||
domain: domain.clone(),
|
||||
permission: *permission,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn compile_unix_socket_permissions(&self) -> Option<Vec<String>> {
|
||||
self.unix_sockets.as_ref().map(|unix_sockets| {
|
||||
unix_sockets
|
||||
.iter()
|
||||
.filter_map(|(path, permission)| {
|
||||
(*permission == NetworkUnixSocketPermissionToml::Allow).then(|| path.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_domain_permissions_to(
|
||||
&self,
|
||||
allowed_domains: &mut Vec<String>,
|
||||
denied_domains: &mut Vec<String>,
|
||||
) {
|
||||
if self.domains.is_some() {
|
||||
let resolved_domain_entries = self.resolved_domain_entries();
|
||||
apply_resolved_domain_entries(
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
&resolved_domain_entries,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_unix_socket_permissions_to(&self, allow_unix_sockets: &mut Vec<String>) {
|
||||
if let Some(unix_sockets) = self.unix_sockets.as_ref() {
|
||||
apply_unix_socket_permissions(allow_unix_sockets, unix_sockets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_resolved_domain_entries(
|
||||
allowed_domains: &mut Vec<String>,
|
||||
denied_domains: &mut Vec<String>,
|
||||
resolved_domain_entries: &[ResolvedNetworkDomainEntry],
|
||||
) {
|
||||
for ResolvedNetworkDomainEntry { domain, permission } in resolved_domain_entries {
|
||||
match permission {
|
||||
NetworkDomainPermissionToml::Allow => {
|
||||
remove_network_domain(denied_domains, domain);
|
||||
remove_network_domain(allowed_domains, domain);
|
||||
allowed_domains.push(domain.clone());
|
||||
}
|
||||
NetworkDomainPermissionToml::Deny => {
|
||||
remove_network_domain(allowed_domains, domain);
|
||||
remove_network_domain(denied_domains, domain);
|
||||
denied_domains.push(domain.clone());
|
||||
}
|
||||
NetworkDomainPermissionToml::None => {
|
||||
remove_network_domain(allowed_domains, domain);
|
||||
remove_network_domain(denied_domains, domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
allowed_domains.sort_unstable();
|
||||
denied_domains.sort_unstable();
|
||||
}
|
||||
|
||||
fn apply_unix_socket_permissions(
|
||||
allow_unix_sockets: &mut Vec<String>,
|
||||
unix_sockets: &BTreeMap<String, NetworkUnixSocketPermissionToml>,
|
||||
) {
|
||||
for (socket_path, permission) in unix_sockets {
|
||||
allow_unix_sockets.retain(|entry| entry != socket_path);
|
||||
if *permission == NetworkUnixSocketPermissionToml::Allow {
|
||||
allow_unix_sockets.push(socket_path.clone());
|
||||
}
|
||||
}
|
||||
allow_unix_sockets.sort_unstable();
|
||||
}
|
||||
|
||||
fn remove_network_domain(domains: &mut Vec<String>, domain: &str) {
|
||||
let normalized_domain = normalize_host(domain);
|
||||
domains.retain(|entry| normalize_host(entry) != normalized_domain);
|
||||
}
|
||||
|
||||
pub(crate) fn network_proxy_config_from_profile_network(
|
||||
|
||||
@@ -7,3 +7,76 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() {
|
||||
normalize_absolute_path_for_platform(r"\\?\D:\c\x\worktrees\2508\swift-base", true);
|
||||
assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_domain_entries_preserve_declared_rules() {
|
||||
let network = NetworkToml {
|
||||
domains: Some(BTreeMap::from([
|
||||
(
|
||||
"example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
),
|
||||
(
|
||||
"blocked.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::Deny,
|
||||
),
|
||||
(
|
||||
"clear.example.com".to_string(),
|
||||
NetworkDomainPermissionToml::None,
|
||||
),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
network.resolved_domain_entries(),
|
||||
vec![
|
||||
ResolvedNetworkDomainEntry {
|
||||
domain: "blocked.example.com".to_string(),
|
||||
permission: NetworkDomainPermissionToml::Deny,
|
||||
},
|
||||
ResolvedNetworkDomainEntry {
|
||||
domain: "clear.example.com".to_string(),
|
||||
permission: NetworkDomainPermissionToml::None,
|
||||
},
|
||||
ResolvedNetworkDomainEntry {
|
||||
domain: "example.com".to_string(),
|
||||
permission: NetworkDomainPermissionToml::Allow,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_domain_permissions_to_respects_none_entries_case_insensitively() {
|
||||
let network = NetworkToml {
|
||||
domains: Some(BTreeMap::from([(
|
||||
"EXAMPLE.com".to_string(),
|
||||
NetworkDomainPermissionToml::None,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let mut allowed_domains = vec!["example.com".to_string()];
|
||||
let mut denied_domains = vec!["Example.com".to_string()];
|
||||
|
||||
network.apply_domain_permissions_to(&mut allowed_domains, &mut denied_domains);
|
||||
|
||||
assert_eq!(allowed_domains, Vec::<String>::new());
|
||||
assert_eq!(denied_domains, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_to_network_proxy_config_clears_domains_for_empty_table() {
|
||||
let network = NetworkToml {
|
||||
domains: Some(BTreeMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.allowed_domains = vec!["example.com".to_string()];
|
||||
config.network.denied_domains = vec!["blocked.example.com".to_string()];
|
||||
|
||||
network.apply_to_network_proxy_config(&mut config);
|
||||
|
||||
assert_eq!(config.network.allowed_domains, Vec::<String>::new());
|
||||
assert_eq!(config.network.denied_domains, Vec::<String>::new());
|
||||
}
|
||||
|
||||
@@ -150,14 +150,14 @@ 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 network.domains.is_some() {
|
||||
let allowed_domains = constraints.allowed_domains.get_or_insert_default();
|
||||
let denied_domains = constraints.denied_domains.get_or_insert_default();
|
||||
network.apply_domain_permissions_to(allowed_domains, 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 {
|
||||
constraints.allow_unix_sockets = Some(allow_unix_sockets);
|
||||
if network.unix_sockets.is_some() {
|
||||
let allow_unix_sockets = constraints.allow_unix_sockets.get_or_insert_default();
|
||||
network.apply_unix_socket_permissions_to(allow_unix_sockets);
|
||||
}
|
||||
if let Some(allow_local_binding) = network.allow_local_binding {
|
||||
constraints.allow_local_binding = Some(allow_local_binding);
|
||||
@@ -191,7 +191,18 @@ fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<Netw
|
||||
}
|
||||
|
||||
fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
if let Some(mut network) = selected_network_from_tables(parsed)? {
|
||||
if network.domains.is_some() {
|
||||
network.apply_domain_permissions_to(
|
||||
&mut config.network.allowed_domains,
|
||||
&mut config.network.denied_domains,
|
||||
);
|
||||
}
|
||||
if network.unix_sockets.is_some() {
|
||||
network.apply_unix_socket_permissions_to(&mut config.network.allow_unix_sockets);
|
||||
}
|
||||
network.domains = None;
|
||||
network.unix_sockets = None;
|
||||
network.apply_to_network_proxy_config(config);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -12,7 +12,9 @@ fn higher_precedence_profile_network_beats_lower_profile_network() {
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
allowed_domains = ["lower.example.com"]
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
@@ -21,7 +23,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,7 +42,60 @@ 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,
|
||||
vec![
|
||||
"higher.example.com".to_string(),
|
||||
"lower.example.com".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_network_domain_none_removes_inherited_entry() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
"blocked.example.com" = "deny"
|
||||
"keep.example.com" = "allow"
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "none"
|
||||
"blocked.example.com" = "none"
|
||||
"extra.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,
|
||||
vec![
|
||||
"extra.example.com".to_string(),
|
||||
"keep.example.com".to_string()
|
||||
]
|
||||
);
|
||||
assert!(config.network.denied_domains.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -40,9 +40,13 @@ mitm = 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 `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 no domain entries are set to `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"
|
||||
|
||||
# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
|
||||
# (or `localhost`) is required to permit them.
|
||||
@@ -50,7 +54,8 @@ denied_domains = ["evil.example"]
|
||||
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"]
|
||||
[permissions.workspace.network.unix_sockets]
|
||||
"/tmp/example.sock" = "allow"
|
||||
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
|
||||
# absolute socket path from `x-unix-socket`.
|
||||
dangerously_allow_all_unix_sockets = false
|
||||
|
||||
Reference in New Issue
Block a user