mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
chore: refactor network permissions to use explicit domain and unix socket rule maps (#15120)
## Summary This PR replaces the legacy network allow/deny list model with explicit rule maps for domains and unix sockets across managed requirements, permissions profiles, the network proxy config, and the app server protocol. Concretely, it: - introduces typed domain (`allow` / `deny`) and unix socket permission (`allow` / `none`) entries instead of separate `allowed_domains`, `denied_domains`, and `allow_unix_sockets` lists - updates config loading, managed requirements merging, and exec-policy overlays to read and upsert rule entries consistently - exposes the new shape through protocol/schema outputs, debug surfaces, and app-server config APIs - rejects the legacy list-based keys and updates docs/tests to reflect the new config format ## Why The previous representation split related network policy across multiple parallel lists, which made merging and overriding rules harder to reason about. Moving to explicit keyed permission maps gives us a single source of truth per host/socket entry, makes allow/deny precedence clearer, and gives protocol consumers access to the full rule state instead of derived projections only. ## Backward Compatibility ### Backward compatible - Managed requirements still accept the legacy `experimental_network.allowed_domains`, `experimental_network.denied_domains`, and `experimental_network.allow_unix_sockets` fields. They are normalized into the new canonical `domains` and `unix_sockets` maps internally. - App-server v2 still deserializes legacy `allowedDomains`, `deniedDomains`, and `allowUnixSockets` payloads, so older clients can continue reading managed network requirements. - App-server v2 responses still populate `allowedDomains`, `deniedDomains`, and `allowUnixSockets` as legacy compatibility views derived from the canonical maps. - `managed_allowed_domains_only` keeps the same behavior after normalization. Legacy managed allowlists still participate in the same enforcement path as canonical `domains` entries. ### Not backward compatible - Permissions profiles under `[permissions.<profile>.network]` no longer accept the legacy list-based keys. Those configs must use the canonical `[domains]` and `[unix_sockets]` tables instead of `allowed_domains`, `denied_domains`, or `allow_unix_sockets`. - Managed `experimental_network` config cannot mix canonical and legacy forms in the same block. For example, `domains` cannot be combined with `allowed_domains` or `denied_domains`, and `unix_sockets` cannot be combined with `allow_unix_sockets`. - The canonical format can express explicit `"none"` entries for unix sockets, but those entries do not round-trip through the legacy compatibility fields because the legacy fields only represent allow/deny lists. ## Testing `/target/debug/codex sandbox macos --log-denials /bin/zsh -c 'curl https://www.example.com' ` gives 200 with config ``` [permissions.workspace.network.domains] "www.example.com" = "allow" ``` and fails when set to deny: `curl: (56) CONNECT tunnel failed, response 403`. Also tested backward compatibility path by verifying that adding the following to `/etc/codex/requirements.toml` works: ``` [experimental_network] allowed_domains = ["www.example.com"] ```
This commit is contained in:
1 parent
21a03f1671
commit
dd30c8eedd
37 files changed
+2413
-492
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user