mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
## Summary - adds macOS Seatbelt deny rules for unreadable glob patterns - expands unreadable glob matches on Linux and masks them in bwrap, including canonical symlink targets - keeps Linux glob expansion robust when `rg` is unavailable in minimal or Bazel test environments - adds sandbox integration coverage that runs `shell` and `exec_command` with a `**/*.env = none` policy and verifies the secret contents do not reach the model ## Linux glob expansion ```text Prefer: rg --files --hidden --no-ignore --glob <pattern> -- <search-root> Fallback: internal globset walker when rg is not installed Failure: any other rg failure aborts sandbox construction ``` ``` [permissions.workspace.filesystem] glob_scan_max_depth = 2 [permissions.workspace.filesystem.":project_roots"] "**/*.env" = "none" ``` This keeps the common path fast without making sandbox construction depend on an ambient `rg` binary. If `rg` is present but fails for another reason, the sandbox setup fails closed instead of silently omitting deny-read masks. ## Platform support - macOS: subprocess sandbox enforcement is handled by Seatbelt regex deny rules - Linux: subprocess sandbox enforcement is handled by expanding existing glob matches and masking them in bwrap - Windows: policy/config/direct-tool glob support is already on `main` from #15979; Windows subprocess sandbox paths continue to fail closed when unreadable split filesystem carveouts require runtime enforcement, rather than silently running unsandboxed ## Stack 1. #15979 - merged: cross-platform glob deny-read policy/config/direct-tool support for macOS, Linux, and Windows 2. This PR - macOS/Linux subprocess sandbox enforcement plus Windows fail-closed clarification 3. #17740 - managed deny-read requirements ## Verification - Added integration coverage for `shell` and `exec_command` glob deny-read enforcement - `cargo check -p codex-sandboxing -p codex-linux-sandbox --tests` - `cargo check -p codex-core --test all` - `cargo clippy -p codex-linux-sandbox -p codex-sandboxing --tests` - `just bazel-lock-check` --------- Co-authored-by: Codex <noreply@openai.com>
245 lines
8.4 KiB
Rust
245 lines
8.4 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
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 schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
pub struct PermissionsToml {
|
|
#[serde(flatten)]
|
|
pub entries: BTreeMap<String, PermissionProfileToml>,
|
|
}
|
|
|
|
impl PermissionsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct PermissionProfileToml {
|
|
pub filesystem: Option<FilesystemPermissionsToml>,
|
|
pub network: Option<NetworkToml>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
pub struct FilesystemPermissionsToml {
|
|
/// Optional maximum depth for expanding unreadable glob patterns on
|
|
/// platforms that snapshot glob matches before sandbox startup.
|
|
#[schemars(range(min = 1))]
|
|
pub glob_scan_max_depth: Option<usize>,
|
|
#[serde(flatten)]
|
|
pub entries: BTreeMap<String, FilesystemPermissionToml>,
|
|
}
|
|
|
|
impl FilesystemPermissionsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
|
#[serde(untagged)]
|
|
pub enum FilesystemPermissionToml {
|
|
Access(FileSystemAccessMode),
|
|
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()
|
|
}
|
|
|
|
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, 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 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 {
|
|
pub enabled: Option<bool>,
|
|
pub proxy_url: Option<String>,
|
|
pub enable_socks5: Option<bool>,
|
|
pub socks_url: Option<String>,
|
|
pub enable_socks5_udp: Option<bool>,
|
|
pub allow_upstream_proxy: Option<bool>,
|
|
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
|
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
|
#[schemars(with = "Option<NetworkModeSchema>")]
|
|
pub mode: Option<NetworkMode>,
|
|
pub domains: Option<NetworkDomainPermissionsToml>,
|
|
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
|
pub allow_local_binding: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum NetworkModeSchema {
|
|
Limited,
|
|
Full,
|
|
}
|
|
|
|
impl NetworkToml {
|
|
pub fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
|
|
if let Some(enabled) = self.enabled {
|
|
config.network.enabled = enabled;
|
|
}
|
|
if let Some(proxy_url) = self.proxy_url.as_ref() {
|
|
config.network.proxy_url = proxy_url.clone();
|
|
}
|
|
if let Some(enable_socks5) = self.enable_socks5 {
|
|
config.network.enable_socks5 = enable_socks5;
|
|
}
|
|
if let Some(socks_url) = self.socks_url.as_ref() {
|
|
config.network.socks_url = socks_url.clone();
|
|
}
|
|
if let Some(enable_socks5_udp) = self.enable_socks5_udp {
|
|
config.network.enable_socks5_udp = enable_socks5_udp;
|
|
}
|
|
if let Some(allow_upstream_proxy) = self.allow_upstream_proxy {
|
|
config.network.allow_upstream_proxy = allow_upstream_proxy;
|
|
}
|
|
if let Some(dangerously_allow_non_loopback_proxy) =
|
|
self.dangerously_allow_non_loopback_proxy
|
|
{
|
|
config.network.dangerously_allow_non_loopback_proxy =
|
|
dangerously_allow_non_loopback_proxy;
|
|
}
|
|
if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets {
|
|
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
|
|
}
|
|
if let Some(mode) = self.mode {
|
|
config.network.mode = mode;
|
|
}
|
|
if let Some(domains) = self.domains.as_ref() {
|
|
overlay_network_domain_permissions(config, domains);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
pub fn to_network_proxy_config(&self) -> NetworkProxyConfig {
|
|
let mut config = NetworkProxyConfig::default();
|
|
self.apply_to_network_proxy_config(&mut config);
|
|
config
|
|
}
|
|
}
|
|
|
|
pub 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);
|
|
}
|
|
}
|