Move macOS sandbox builders into codex-sandboxing (#15593)

## Summary
- move macOS permission merging/intersection logic and tests from
`codex-core` into `codex-sandboxing`
- move seatbelt policy builders, permissions logic, SBPL assets, and
their tests into `codex-sandboxing`
- keep `codex-core` owning only the seatbelt spawn wrapper and switch
call sites to import the moved APIs directly

## Notes
- no re-exports added
- moved the seatbelt tests with the implementation so internal helpers
could stay private
- local verification is still finishing while this PR is open
This commit is contained in:
pakrym-oai
2026-03-23 21:26:35 -07:00
committed by GitHub
Unverified
parent 2227248cd6
commit 431af0807c
19 changed files with 650 additions and 601 deletions
-1
View File
@@ -93,7 +93,6 @@ mod event_mapping;
mod response_debug_context;
pub mod review_format;
pub mod review_prompts;
mod seatbelt_permissions;
mod thread_manager;
pub mod web_search;
pub mod windows_sandbox_read_grants;
@@ -1,199 +0,0 @@
; macOS platform defaults included via `ReadOnlyAccess::Restricted::include_platform_defaults`
; Read access to standard system paths
(allow file-read* file-test-existence
(subpath "/Library/Apple")
(subpath "/Library/Filesystems/NetFSPlugins")
(subpath "/Library/Preferences/Logging")
(subpath "/private/var/db/DarwinDirectory/local/recordStore.data")
(subpath "/private/var/db/timezone")
(subpath "/usr/lib")
(subpath "/usr/share")
(subpath "/Library/Preferences")
(subpath "/var/db")
(subpath "/private/var/db"))
; Map system frameworks + dylibs for loader.
(allow file-map-executable
(subpath "/Library/Apple/System/Library/Frameworks")
(subpath "/Library/Apple/System/Library/PrivateFrameworks")
(subpath "/Library/Apple/usr/lib")
(subpath "/System/Library/Extensions")
(subpath "/System/Library/Frameworks")
(subpath "/System/Library/PrivateFrameworks")
(subpath "/System/Library/SubFrameworks")
(subpath "/System/iOSSupport/System/Library/Frameworks")
(subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
(subpath "/System/iOSSupport/System/Library/SubFrameworks")
(subpath "/usr/lib"))
; System Framework and AppKit resources
(allow file-read* file-test-existence
(subpath "/Library/Apple/System/Library/Frameworks")
(subpath "/Library/Apple/System/Library/PrivateFrameworks")
(subpath "/Library/Apple/usr/lib")
(subpath "/System/Library/Frameworks")
(subpath "/System/Library/PrivateFrameworks")
(subpath "/System/Library/SubFrameworks")
(subpath "/System/iOSSupport/System/Library/Frameworks")
(subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
(subpath "/System/iOSSupport/System/Library/SubFrameworks")
(subpath "/usr/lib"))
; Allow guarded vnodes.
(allow system-mac-syscall (mac-policy-name "vnguard"))
; Determine whether a container is expected.
(allow system-mac-syscall
(require-all
(mac-policy-name "Sandbox")
(mac-syscall-number 67)))
; Allow resolution of standard system symlinks.
(allow file-read-metadata file-test-existence
(literal "/etc")
(literal "/tmp")
(literal "/var")
(literal "/private/etc/localtime"))
; Allow stat'ing of firmlink parent path components.
(allow file-read-metadata file-test-existence
(path-ancestors "/System/Volumes/Data/private"))
; Allow processes to get their current working directory.
(allow file-read* file-test-existence
(literal "/"))
; Allow FSIOC_CAS_BSDFLAGS as alternate chflags.
(allow system-fsctl (fsctl-command FSIOC_CAS_BSDFLAGS))
; Allow access to standard special files.
(allow file-read* file-test-existence
(literal "/dev/autofs_nowait")
(literal "/dev/random")
(literal "/dev/urandom")
(literal "/private/etc/master.passwd")
(literal "/private/etc/passwd")
(literal "/private/etc/protocols")
(literal "/private/etc/services"))
; Allow null/zero read/write.
(allow file-read* file-test-existence file-write-data
(literal "/dev/null")
(literal "/dev/zero"))
; Allow read/write access to the file descriptors.
(allow file-read-data file-test-existence file-write-data
(subpath "/dev/fd"))
; Provide access to debugger helpers.
(allow file-read* file-test-existence file-write-data file-ioctl
(literal "/dev/dtracehelper"))
; Scratch space so tools can create temp files.
(allow file-read* file-test-existence file-write* (subpath "/tmp"))
(allow file-read* file-write* (subpath "/private/tmp"))
(allow file-read* file-write* (subpath "/var/tmp"))
(allow file-read* file-write* (subpath "/private/var/tmp"))
; Allow reading standard config directories.
(allow file-read* (subpath "/etc"))
(allow file-read* (subpath "/private/etc"))
(allow file-read* file-test-existence
(literal "/System/Library/CoreServices")
(literal "/System/Library/CoreServices/.SystemVersionPlatform.plist")
(literal "/System/Library/CoreServices/SystemVersion.plist"))
; Some processes read /var metadata during startup.
(allow file-read-metadata (subpath "/var"))
(allow file-read-metadata (subpath "/private/var"))
; IOKit access for root domain services.
(allow iokit-open
(iokit-registry-entry-class "RootDomainUserClient"))
; macOS Standard library queries opendirectoryd at startup
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
; Allow IPC to analytics, logging, trust, and other system agents.
(allow mach-lookup
(global-name "com.apple.analyticsd")
(global-name "com.apple.analyticsd.messagetracer")
(global-name "com.apple.appsleep")
(global-name "com.apple.bsd.dirhelper")
(global-name "com.apple.cfprefsd.agent")
(global-name "com.apple.cfprefsd.daemon")
(global-name "com.apple.diagnosticd")
(global-name "com.apple.dt.automationmode.reader")
(global-name "com.apple.espd")
(global-name "com.apple.logd")
(global-name "com.apple.logd.events")
(global-name "com.apple.runningboard")
(global-name "com.apple.secinitd")
(global-name "com.apple.system.DirectoryService.libinfo_v1")
(global-name "com.apple.system.logger")
(global-name "com.apple.system.notification_center")
(global-name "com.apple.system.opendirectoryd.membership")
(global-name "com.apple.trustd")
(global-name "com.apple.trustd.agent")
(global-name "com.apple.xpc.activity.unmanaged")
(local-name "com.apple.cfprefsd.agent"))
; Allow IPC to the syslog socket for logging.
(allow network-outbound (literal "/private/var/run/syslog"))
; macOS Notifications
(allow ipc-posix-shm-read*
(ipc-posix-name "apple.shm.notification_center"))
; Regulatory domain support.
(allow file-read*
(literal "/private/var/db/eligibilityd/eligibility.plist"))
; Audio and power management services.
(allow mach-lookup (global-name "com.apple.audio.audiohald"))
(allow mach-lookup (global-name "com.apple.audio.AudioComponentRegistrar"))
(allow mach-lookup (global-name "com.apple.PowerManagement.control"))
; Allow reading the minimum system runtime so exec works.
(allow file-read-data (subpath "/bin"))
(allow file-read-metadata (subpath "/bin"))
(allow file-read-data (subpath "/sbin"))
(allow file-read-metadata (subpath "/sbin"))
(allow file-read-data (subpath "/usr/bin"))
(allow file-read-metadata (subpath "/usr/bin"))
(allow file-read-data (subpath "/usr/sbin"))
(allow file-read-metadata (subpath "/usr/sbin"))
(allow file-read-data (subpath "/usr/libexec"))
(allow file-read-metadata (subpath "/usr/libexec"))
(allow file-read* (subpath "/Library/Preferences"))
(allow file-read* (subpath "/opt/homebrew/lib"))
(allow file-read* (subpath "/usr/local/lib"))
(allow file-read* (subpath "/Applications"))
; Terminal basics and device handles.
(allow file-read* (regex "^/dev/fd/(0|1|2)$"))
(allow file-write* (regex "^/dev/fd/(1|2)$"))
(allow file-read* file-write* (literal "/dev/null"))
(allow file-read* file-write* (literal "/dev/tty"))
(allow file-read-metadata (literal "/dev"))
(allow file-read-metadata (regex "^/dev/.*$"))
(allow file-read-metadata (literal "/dev/stdin"))
(allow file-read-metadata (literal "/dev/stdout"))
(allow file-read-metadata (literal "/dev/stderr"))
(allow file-read-metadata (regex "^/dev/tty[^/]*$"))
(allow file-read-metadata (regex "^/dev/pty[^/]*$"))
(allow file-read* file-write* (regex "^/dev/ttys[0-9]+$"))
(allow file-read* file-write* (literal "/dev/ptmx"))
(allow file-ioctl (regex "^/dev/ttys[0-9]+$"))
; Allow metadata traversal for firmlink parents.
(allow file-read-metadata (literal "/System/Volumes") (vnode-type DIRECTORY))
(allow file-read-metadata (literal "/System/Volumes/Data") (vnode-type DIRECTORY))
(allow file-read-metadata (literal "/System/Volumes/Data/Users") (vnode-type DIRECTORY))
; App sandbox extensions
(allow file-read* (extension "com.apple.app-sandbox.read"))
(allow file-read* file-write* (extension "com.apple.app-sandbox.read-write"))
@@ -1,154 +0,0 @@
use std::collections::BTreeSet;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
/// Merges macOS seatbelt profile extensions by taking the permissive union of
/// each permission field.
pub(crate) fn merge_macos_seatbelt_profile_extensions(
base: Option<&MacOsSeatbeltProfileExtensions>,
permissions: Option<&MacOsSeatbeltProfileExtensions>,
) -> Option<MacOsSeatbeltProfileExtensions> {
let Some(permissions) = permissions else {
return base.cloned();
};
match base {
Some(base) => Some(MacOsSeatbeltProfileExtensions {
macos_preferences: union_macos_preferences_permission(
&base.macos_preferences,
&permissions.macos_preferences,
),
macos_automation: union_macos_automation_permission(
&base.macos_automation,
&permissions.macos_automation,
),
macos_launch_services: base.macos_launch_services || permissions.macos_launch_services,
macos_accessibility: base.macos_accessibility || permissions.macos_accessibility,
macos_calendar: base.macos_calendar || permissions.macos_calendar,
macos_reminders: base.macos_reminders || permissions.macos_reminders,
macos_contacts: union_macos_contacts_permission(
&base.macos_contacts,
&permissions.macos_contacts,
),
}),
None => Some(permissions.clone()),
}
}
pub(crate) fn intersect_macos_seatbelt_profile_extensions(
requested: Option<MacOsSeatbeltProfileExtensions>,
granted: Option<MacOsSeatbeltProfileExtensions>,
) -> Option<MacOsSeatbeltProfileExtensions> {
match (requested, granted) {
(Some(requested), Some(granted)) => {
let macos_automation = intersect_macos_automation_permission(
&requested.macos_automation,
&granted.macos_automation,
);
Some(MacOsSeatbeltProfileExtensions {
macos_preferences: requested.macos_preferences.min(granted.macos_preferences),
macos_automation,
macos_launch_services: requested.macos_launch_services
&& granted.macos_launch_services,
macos_accessibility: requested.macos_accessibility && granted.macos_accessibility,
macos_calendar: requested.macos_calendar && granted.macos_calendar,
macos_reminders: requested.macos_reminders && granted.macos_reminders,
macos_contacts: requested.macos_contacts.min(granted.macos_contacts),
})
}
_ => None,
}
}
/// Unions two preferences permissions by keeping the more permissive one.
///
/// The larger rank wins: `None < ReadOnly < ReadWrite`. When both sides have
/// the same rank, this keeps `base`.
fn union_macos_preferences_permission(
base: &MacOsPreferencesPermission,
requested: &MacOsPreferencesPermission,
) -> MacOsPreferencesPermission {
if base < requested {
requested.clone()
} else {
base.clone()
}
}
fn union_macos_contacts_permission(
base: &MacOsContactsPermission,
requested: &MacOsContactsPermission,
) -> MacOsContactsPermission {
if base < requested {
requested.clone()
} else {
base.clone()
}
}
/// Unions two automation permissions by keeping the more permissive result.
///
/// `All` wins over everything, `None` yields to the other side, and two bundle
/// ID allowlists are unioned together.
fn union_macos_automation_permission(
base: &MacOsAutomationPermission,
requested: &MacOsAutomationPermission,
) -> MacOsAutomationPermission {
match (base, requested) {
(MacOsAutomationPermission::All, _) | (_, MacOsAutomationPermission::All) => {
MacOsAutomationPermission::All
}
(MacOsAutomationPermission::None, _) => requested.clone(),
(_, MacOsAutomationPermission::None) => base.clone(),
(
MacOsAutomationPermission::BundleIds(base_bundle_ids),
MacOsAutomationPermission::BundleIds(requested_bundle_ids),
) => MacOsAutomationPermission::BundleIds(
base_bundle_ids
.iter()
.chain(requested_bundle_ids.iter())
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
),
}
}
fn intersect_macos_automation_permission(
requested: &MacOsAutomationPermission,
granted: &MacOsAutomationPermission,
) -> MacOsAutomationPermission {
match (requested, granted) {
(_, MacOsAutomationPermission::None) | (MacOsAutomationPermission::None, _) => {
MacOsAutomationPermission::None
}
(MacOsAutomationPermission::All, granted) => granted.clone(),
(MacOsAutomationPermission::BundleIds(requested), MacOsAutomationPermission::All) => {
MacOsAutomationPermission::BundleIds(requested.clone())
}
(
MacOsAutomationPermission::BundleIds(requested),
MacOsAutomationPermission::BundleIds(granted),
) => {
let bundle_ids = requested
.iter()
.filter(|bundle_id| granted.contains(bundle_id))
.cloned()
.collect::<Vec<String>>();
if bundle_ids.is_empty() {
MacOsAutomationPermission::None
} else {
MacOsAutomationPermission::BundleIds(bundle_ids)
}
}
}
}
#[cfg(all(test, target_os = "macos"))]
#[path = "macos_permissions_tests.rs"]
mod tests;
@@ -1,121 +0,0 @@
use super::intersect_macos_automation_permission;
use super::intersect_macos_seatbelt_profile_extensions;
use super::merge_macos_seatbelt_profile_extensions;
use super::union_macos_automation_permission;
use super::union_macos_contacts_permission;
use super::union_macos_preferences_permission;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use pretty_assertions::assert_eq;
#[test]
fn merge_extensions_widens_permissions() {
let base = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
]),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::ReadOnly,
};
let requested = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
"com.apple.Calendar".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: true,
macos_contacts: MacOsContactsPermission::ReadWrite,
};
let merged =
merge_macos_seatbelt_profile_extensions(Some(&base), Some(&requested)).expect("merge");
assert_eq!(
merged,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: true,
macos_contacts: MacOsContactsPermission::ReadWrite,
}
);
}
#[test]
fn union_macos_preferences_permission_does_not_downgrade() {
let base = MacOsPreferencesPermission::ReadWrite;
let requested = MacOsPreferencesPermission::ReadOnly;
let merged = union_macos_preferences_permission(&base, &requested);
assert_eq!(merged, MacOsPreferencesPermission::ReadWrite);
}
#[test]
fn union_macos_automation_permission_all_is_dominant() {
let base = MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]);
let requested = MacOsAutomationPermission::All;
let merged = union_macos_automation_permission(&base, &requested);
assert_eq!(merged, MacOsAutomationPermission::All);
}
#[test]
fn intersect_macos_automation_permission_keeps_common_bundle_ids() {
let requested = MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
"com.apple.Calendar".to_string(),
]);
let granted = MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]);
let intersected = intersect_macos_automation_permission(&requested, &granted);
assert_eq!(
intersected,
MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()])
);
}
#[test]
fn intersect_macos_seatbelt_profile_extensions_preserves_default_grant() {
let requested = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]),
macos_launch_services: false,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
};
let granted = MacOsSeatbeltProfileExtensions::default();
let intersected = intersect_macos_seatbelt_profile_extensions(Some(requested), Some(granted));
assert_eq!(intersected, Some(MacOsSeatbeltProfileExtensions::default()));
}
#[test]
fn union_macos_contacts_permission_does_not_downgrade() {
let base = MacOsContactsPermission::ReadWrite;
let requested = MacOsContactsPermission::ReadOnly;
let merged = union_macos_contacts_permission(&base, &requested);
assert_eq!(merged, MacOsContactsPermission::ReadWrite);
}
+6 -8
View File
@@ -6,8 +6,6 @@ sandbox placement and transformation of portable CommandSpec into a
readytospawn environment.
*/
pub(crate) mod macos_permissions;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutput;
@@ -16,10 +14,6 @@ use crate::exec::StdoutStream;
use crate::exec::execute_exec_request;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use crate::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::tools::sandboxing::SandboxablePreference;
@@ -40,10 +34,14 @@ use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_sandboxing::landlock::allow_network_for_proxy;
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
use codex_sandboxing::macos_permissions::intersect_macos_seatbelt_profile_extensions;
use codex_sandboxing::macos_permissions::merge_macos_seatbelt_profile_extensions;
#[cfg(target_os = "macos")]
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
use codex_utils_absolute_path::AbsolutePathBuf;
use dunce::canonicalize;
use macos_permissions::intersect_macos_seatbelt_profile_extensions;
use macos_permissions::merge_macos_seatbelt_profile_extensions;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
+11 -556
View File
@@ -1,40 +1,19 @@
#![cfg(target_os = "macos")]
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::PROXY_URL_ENV_KEYS;
use codex_network_proxy::has_proxy_url_env_vars;
use codex_network_proxy::proxy_url_env_value;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::ffi::CStr;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
use tracing::warn;
use url::Url;
use crate::protocol::SandboxPolicy;
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use crate::seatbelt_permissions::build_seatbelt_extensions;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::SpawnChildRequest;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use codex_network_proxy::NetworkProxy;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
include_str!("restricted_read_only_platform_defaults.sbpl");
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
/// already has root access.
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Child;
pub async fn spawn_command_under_seatbelt(
command: Vec<String>,
@@ -45,12 +24,14 @@ pub async fn spawn_command_under_seatbelt(
network: Option<&NetworkProxy>,
mut env: HashMap<String, String>,
) -> std::io::Result<Child> {
let args = create_seatbelt_command_args(
let args = create_seatbelt_command_args_for_policies_with_extensions(
command,
sandbox_policy,
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
NetworkSandboxPolicy::from(sandbox_policy),
sandbox_policy_cwd,
/*enforce_managed_network*/ false,
network,
/*extensions*/ None,
);
let arg0 = None;
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
@@ -66,529 +47,3 @@ pub async fn spawn_command_under_seatbelt(
})
.await
}
fn is_loopback_host(host: &str) -> bool {
host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1"
}
fn proxy_scheme_default_port(scheme: &str) -> u16 {
match scheme {
"https" => 443,
"socks5" | "socks5h" | "socks4" | "socks4a" => 1080,
_ => 80,
}
}
fn proxy_loopback_ports_from_env(env: &HashMap<String, String>) -> Vec<u16> {
let mut ports = BTreeSet::new();
for key in PROXY_URL_ENV_KEYS {
let Some(proxy_url) = proxy_url_env_value(env, key) else {
continue;
};
let trimmed = proxy_url.trim();
if trimmed.is_empty() {
continue;
}
let candidate = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("http://{trimmed}")
};
let Ok(parsed) = Url::parse(&candidate) else {
continue;
};
let Some(host) = parsed.host_str() else {
continue;
};
if !is_loopback_host(host) {
continue;
}
let scheme = parsed.scheme().to_ascii_lowercase();
let port = parsed
.port()
.unwrap_or_else(|| proxy_scheme_default_port(scheme.as_str()));
ports.insert(port);
}
ports.into_iter().collect()
}
#[derive(Debug, Default)]
struct ProxyPolicyInputs {
ports: Vec<u16>,
has_proxy_config: bool,
allow_local_binding: bool,
unix_domain_socket_policy: UnixDomainSocketPolicy,
}
#[derive(Debug, Clone)]
// Keep allow-all and allowlist modes disjoint so we don't carry ignored state.
enum UnixDomainSocketPolicy {
AllowAll,
Restricted { allowed: Vec<AbsolutePathBuf> },
}
impl Default for UnixDomainSocketPolicy {
fn default() -> Self {
Self::Restricted { allowed: vec![] }
}
}
#[derive(Debug, Clone)]
struct UnixSocketPathParam {
index: usize,
path: AbsolutePathBuf,
}
fn proxy_policy_inputs(network: Option<&NetworkProxy>) -> ProxyPolicyInputs {
if let Some(network) = network {
let mut env = HashMap::new();
network.apply_to_env(&mut env);
let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() {
UnixDomainSocketPolicy::AllowAll
} else {
let allowed = network
.allow_unix_sockets()
.iter()
.filter_map(
|socket_path| match normalize_path_for_sandbox(Path::new(socket_path)) {
Some(path) => Some((path.to_string_lossy().to_string(), path)),
None => {
warn!(
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
);
None
}
},
)
.collect::<BTreeMap<_, _>>()
.into_values()
.collect();
UnixDomainSocketPolicy::Restricted { allowed }
};
return ProxyPolicyInputs {
ports: proxy_loopback_ports_from_env(&env),
has_proxy_config: has_proxy_url_env_vars(&env),
allow_local_binding: network.allow_local_binding(),
unix_domain_socket_policy,
};
}
ProxyPolicyInputs::default()
}
fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
// `AbsolutePathBuf::from_absolute_path()` normalizes relative paths against the current
// working directory, so keep the explicit check to avoid silently accepting relative entries.
if !path.is_absolute() {
return None;
}
let absolute_path = AbsolutePathBuf::from_absolute_path(path).ok()?;
let normalized_path = absolute_path
.as_path()
.canonicalize()
.ok()
.and_then(|canonical_path| AbsolutePathBuf::from_absolute_path(canonical_path).ok());
normalized_path.or(Some(absolute_path))
}
fn unix_socket_path_params(proxy: &ProxyPolicyInputs) -> Vec<UnixSocketPathParam> {
let mut deduped_paths: BTreeMap<String, AbsolutePathBuf> = BTreeMap::new();
let UnixDomainSocketPolicy::Restricted { allowed } = &proxy.unix_domain_socket_policy else {
return vec![];
};
for path in allowed {
deduped_paths
.entry(path.to_string_lossy().to_string())
.or_insert_with(|| path.clone());
}
deduped_paths
.into_values()
.enumerate()
.map(|(index, path)| UnixSocketPathParam { index, path })
.collect()
}
fn unix_socket_path_param_key(index: usize) -> String {
format!("UNIX_SOCKET_PATH_{index}")
}
fn unix_socket_dir_params(proxy: &ProxyPolicyInputs) -> Vec<(String, PathBuf)> {
unix_socket_path_params(proxy)
.into_iter()
.map(|param| {
(
unix_socket_path_param_key(param.index),
param.path.into_path_buf(),
)
})
.collect()
}
/// Returns zero or more complete Seatbelt policy lines for unix socket rules.
/// When non-empty, the returned string is newline-terminated so callers can
/// append it directly to larger policy blocks.
fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
let socket_params = unix_socket_path_params(proxy);
let has_unix_socket_access = matches!(
proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::AllowAll
) || !socket_params.is_empty();
if !has_unix_socket_access {
return String::new();
}
let mut policy = String::new();
policy.push_str("(allow system-socket (socket-domain AF_UNIX))\n");
if matches!(
proxy.unix_domain_socket_policy,
UnixDomainSocketPolicy::AllowAll
) {
// Keep AllowAll genuinely broad here; path qualifiers look narrower
// without a clear macOS behavioral benefit.
policy.push_str("(allow network-bind (local unix-socket))\n");
policy.push_str("(allow network-outbound (remote unix-socket))\n");
return policy;
}
for param in socket_params {
let key = unix_socket_path_param_key(param.index);
// Use subpath so allowlists cover sockets created beneath approved directories.
policy.push_str(&format!(
"(allow network-bind (local unix-socket (subpath (param \"{key}\"))))\n"
));
policy.push_str(&format!(
"(allow network-outbound (remote unix-socket (subpath (param \"{key}\"))))\n"
));
}
policy
}
#[cfg_attr(not(test), allow(dead_code))]
fn dynamic_network_policy(
sandbox_policy: &SandboxPolicy,
enforce_managed_network: bool,
proxy: &ProxyPolicyInputs,
) -> String {
dynamic_network_policy_for_network(
NetworkSandboxPolicy::from(sandbox_policy),
enforce_managed_network,
proxy,
)
}
fn dynamic_network_policy_for_network(
network_policy: NetworkSandboxPolicy,
enforce_managed_network: bool,
proxy: &ProxyPolicyInputs,
) -> String {
let should_use_restricted_network_policy =
!proxy.ports.is_empty() || proxy.has_proxy_config || enforce_managed_network;
if should_use_restricted_network_policy {
let mut policy = String::new();
if proxy.allow_local_binding {
policy.push_str("; allow loopback local binding and loopback traffic\n");
policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
}
for port in &proxy.ports {
policy.push_str(&format!(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
));
}
let unix_socket_policy = unix_socket_policy(proxy);
if !unix_socket_policy.is_empty() {
policy.push_str("; allow unix domain sockets for local IPC\n");
policy.push_str(&unix_socket_policy);
}
return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
}
if proxy.has_proxy_config {
// Proxy configuration is present but we could not infer any valid loopback endpoints.
// Fail closed to avoid silently widening network access in proxy-enforced sessions.
return String::new();
}
if enforce_managed_network {
// Managed network requirements are active but no usable proxy endpoints
// are available. Fail closed for network access.
return String::new();
}
if network_policy.is_enabled() {
// No proxy env is configured: retain the existing full-network behavior.
format!(
"(allow network-outbound)\n(allow network-inbound)\n{MACOS_SEATBELT_NETWORK_POLICY}"
)
} else {
String::new()
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn create_seatbelt_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
) -> Vec<String> {
create_seatbelt_command_args_with_extensions(
command,
sandbox_policy,
sandbox_policy_cwd,
enforce_managed_network,
network,
/*extensions*/ None,
)
}
fn root_absolute_path() -> AbsolutePathBuf {
match AbsolutePathBuf::from_absolute_path(Path::new("/")) {
Ok(path) => path,
Err(err) => panic!("root path must be absolute: {err}"),
}
}
#[derive(Debug, Clone)]
struct SeatbeltAccessRoot {
root: AbsolutePathBuf,
excluded_subpaths: Vec<AbsolutePathBuf>,
}
fn build_seatbelt_access_policy(
action: &str,
param_prefix: &str,
roots: Vec<SeatbeltAccessRoot>,
) -> (String, Vec<(String, PathBuf)>) {
let mut policy_components = Vec::new();
let mut params = Vec::new();
for (index, access_root) in roots.into_iter().enumerate() {
let root =
normalize_path_for_sandbox(access_root.root.as_path()).unwrap_or(access_root.root);
let root_param = format!("{param_prefix}_{index}");
params.push((root_param.clone(), root.into_path_buf()));
if access_root.excluded_subpaths.is_empty() {
policy_components.push(format!("(subpath (param \"{root_param}\"))"));
continue;
}
let mut require_parts = vec![format!("(subpath (param \"{root_param}\"))")];
for (excluded_index, excluded_subpath) in
access_root.excluded_subpaths.into_iter().enumerate()
{
let excluded_subpath =
normalize_path_for_sandbox(excluded_subpath.as_path()).unwrap_or(excluded_subpath);
let excluded_param = format!("{param_prefix}_{index}_RO_{excluded_index}");
params.push((excluded_param.clone(), excluded_subpath.into_path_buf()));
require_parts.push(format!(
"(require-not (subpath (param \"{excluded_param}\")))"
));
}
policy_components.push(format!("(require-all {} )", require_parts.join(" ")));
}
if policy_components.is_empty() {
(String::new(), Vec::new())
} else {
(
format!("(allow {action}\n{}\n)", policy_components.join(" ")),
params,
)
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn create_seatbelt_command_args_with_extensions(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
extensions: Option<&MacOsSeatbeltProfileExtensions>,
) -> Vec<String> {
create_seatbelt_command_args_for_policies_with_extensions(
command,
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
NetworkSandboxPolicy::from(sandbox_policy),
sandbox_policy_cwd,
enforce_managed_network,
network,
extensions,
)
}
pub fn create_seatbelt_command_args_for_policies_with_extensions(
command: Vec<String>,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox_policy_cwd: &Path,
enforce_managed_network: bool,
network: Option<&NetworkProxy>,
extensions: Option<&MacOsSeatbeltProfileExtensions>,
) -> Vec<String> {
let unreadable_roots =
file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd);
let (file_write_policy, file_write_dir_params) =
if file_system_sandbox_policy.has_full_disk_write_access() {
if unreadable_roots.is_empty() {
// Allegedly, this is more permissive than `(allow file-write*)`.
(
r#"(allow file-write* (regex #"^/"))"#.to_string(),
Vec::new(),
)
} else {
build_seatbelt_access_policy(
"file-write*",
"WRITABLE_ROOT",
vec![SeatbeltAccessRoot {
root: root_absolute_path(),
excluded_subpaths: unreadable_roots.clone(),
}],
)
}
} else {
build_seatbelt_access_policy(
"file-write*",
"WRITABLE_ROOT",
file_system_sandbox_policy
.get_writable_roots_with_cwd(sandbox_policy_cwd)
.into_iter()
.map(|root| SeatbeltAccessRoot {
root: root.root,
excluded_subpaths: root.read_only_subpaths,
})
.collect(),
)
};
let (file_read_policy, file_read_dir_params) =
if file_system_sandbox_policy.has_full_disk_read_access() {
if unreadable_roots.is_empty() {
(
"; allow read-only file operations\n(allow file-read*)".to_string(),
Vec::new(),
)
} else {
let (policy, params) = build_seatbelt_access_policy(
"file-read*",
"READABLE_ROOT",
vec![SeatbeltAccessRoot {
root: root_absolute_path(),
excluded_subpaths: unreadable_roots,
}],
);
(
format!("; allow read-only file operations\n{policy}"),
params,
)
}
} else {
let (policy, params) = build_seatbelt_access_policy(
"file-read*",
"READABLE_ROOT",
file_system_sandbox_policy
.get_readable_roots_with_cwd(sandbox_policy_cwd)
.into_iter()
.map(|root| SeatbeltAccessRoot {
excluded_subpaths: unreadable_roots
.iter()
.filter(|path| path.as_path().starts_with(root.as_path()))
.cloned()
.collect(),
root,
})
.collect(),
);
if policy.is_empty() {
(String::new(), params)
} else {
(
format!("; allow read-only file operations\n{policy}"),
params,
)
}
};
let proxy = proxy_policy_inputs(network);
let network_policy =
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
let seatbelt_extensions = extensions.map_or_else(
|| {
// Backward-compatibility default when no extension profile is provided.
build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default())
},
build_seatbelt_extensions,
);
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
let mut policy_sections = vec![
MACOS_SEATBELT_BASE_POLICY.to_string(),
file_read_policy,
file_write_policy,
network_policy,
];
if include_platform_defaults {
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
}
if !seatbelt_extensions.policy.is_empty() {
policy_sections.push(seatbelt_extensions.policy.clone());
}
let full_policy = policy_sections.join("\n");
let dir_params = [
file_read_dir_params,
file_write_dir_params,
macos_dir_params(),
unix_socket_dir_params(&proxy),
seatbelt_extensions.dir_params,
]
.concat();
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
let definition_args = dir_params
.into_iter()
.map(|(key, value)| format!("-D{key}={value}", value = value.to_string_lossy()));
seatbelt_args.extend(definition_args);
seatbelt_args.push("--".to_string());
seatbelt_args.extend(command);
seatbelt_args
}
/// Wraps libc::confstr to return a String.
fn confstr(name: libc::c_int) -> Option<String> {
let mut buf = vec![0_i8; (libc::PATH_MAX as usize) + 1];
let len = unsafe { libc::confstr(name, buf.as_mut_ptr(), buf.len()) };
if len == 0 {
return None;
}
// confstr guarantees NUL-termination when len > 0.
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
cstr.to_str().ok().map(ToString::to_string)
}
/// Wraps confstr to return a canonicalized PathBuf.
fn confstr_path(name: libc::c_int) -> Option<PathBuf> {
let s = confstr(name)?;
let path = PathBuf::from(s);
path.canonicalize().ok().or(Some(path))
}
fn macos_dir_params() -> Vec<(String, PathBuf)> {
if let Some(p) = confstr_path(libc::_CS_DARWIN_USER_CACHE_DIR) {
return vec![("DARWIN_USER_CACHE_DIR".to_string(), p)];
}
vec![]
}
#[cfg(test)]
#[path = "seatbelt_tests.rs"]
mod tests;
-108
View File
@@ -1,108 +0,0 @@
(version 1)
; inspired by Chrome's sandbox policy:
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/renderer.sb;l=64;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
; start with closed-by-default
(deny default)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
; process-info
(allow process-info* (target same-sandbox))
(allow file-write-data
(require-all
(path "/dev/null")
(vnode-type CHARACTER-DEVICE)))
; sysctls permitted.
(allow sysctl-read
(sysctl-name "hw.activecpu")
(sysctl-name "hw.busfrequency_compat")
(sysctl-name "hw.byteorder")
(sysctl-name "hw.cacheconfig")
(sysctl-name "hw.cachelinesize_compat")
(sysctl-name "hw.cpufamily")
(sysctl-name "hw.cpufrequency_compat")
(sysctl-name "hw.cputype")
(sysctl-name "hw.l1dcachesize_compat")
(sysctl-name "hw.l1icachesize_compat")
(sysctl-name "hw.l2cachesize_compat")
(sysctl-name "hw.l3cachesize_compat")
(sysctl-name "hw.logicalcpu_max")
(sysctl-name "hw.machine")
(sysctl-name "hw.model")
(sysctl-name "hw.memsize")
(sysctl-name "hw.ncpu")
(sysctl-name "hw.nperflevels")
; Chrome locks these CPU feature detection down a bit more tightly,
; but mostly for fingerprinting concerns which isn't an issue for codex.
(sysctl-name-prefix "hw.optional.arm.")
(sysctl-name-prefix "hw.optional.armv8_")
(sysctl-name "hw.packages")
(sysctl-name "hw.pagesize_compat")
(sysctl-name "hw.pagesize")
(sysctl-name "hw.physicalcpu")
(sysctl-name "hw.physicalcpu_max")
(sysctl-name "hw.logicalcpu")
(sysctl-name "hw.cpufrequency")
(sysctl-name "hw.tbfrequency_compat")
(sysctl-name "hw.vectorunit")
(sysctl-name "machdep.cpu.brand_string")
(sysctl-name "kern.argmax")
(sysctl-name "kern.hostname")
(sysctl-name "kern.maxfilesperproc")
(sysctl-name "kern.maxproc")
(sysctl-name "kern.osproductversion")
(sysctl-name "kern.osrelease")
(sysctl-name "kern.ostype")
(sysctl-name "kern.osvariant_status")
(sysctl-name "kern.osversion")
(sysctl-name "kern.secure_kernel")
(sysctl-name "kern.usrstack64")
(sysctl-name "kern.version")
(sysctl-name "sysctl.proc_cputype")
(sysctl-name "vm.loadavg")
(sysctl-name-prefix "hw.perflevel")
(sysctl-name-prefix "kern.proc.pgrp.")
(sysctl-name-prefix "kern.proc.pid.")
(sysctl-name-prefix "net.routetable.")
)
; Allow Java to read some CPU info. This is misclassified as a "write" because
; userspace passes a memory buffer to the sysctl, but conceptually it is a read.
(allow sysctl-write
(sysctl-name "kern.grade_cputype"))
; IOKit
(allow iokit-open
(iokit-registry-entry-class "RootDomainUserClient")
)
; needed to look up user info, see https://crbug.com/792228
(allow mach-lookup
(global-name "com.apple.system.opendirectoryd.libinfo")
)
; Needed for python multiprocessing on MacOS for the SemLock
(allow ipc-posix-sem)
(allow mach-lookup
(global-name "com.apple.PowerManagement.control")
)
; allow openpty()
(allow pseudo-tty)
(allow file-read* file-write* file-ioctl (literal "/dev/ptmx"))
(allow file-read* file-write*
(require-all
(regex #"^/dev/ttys[0-9]+")
(extension "com.apple.sandbox.pty")))
; PTYs created before entering seatbelt may lack the extension; allow ioctl
; on those slave ttys so interactive shells detect a TTY and remain functional.
(allow file-ioctl (regex #"^/dev/ttys[0-9]+"))
@@ -1,35 +0,0 @@
; when network access is enabled, these policies are added after those in seatbelt_base_policy.sbpl
; proxy-specific allow rules are injected by codex-core based on environment.
; Ref https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/network.sb;drc=f8f264d5e4e7509c913f4c60c2639d15905a07e4
; allow only safe AF_SYSTEM sockets used for local platform services.
(allow system-socket
(require-all
(socket-domain AF_SYSTEM)
(socket-protocol 2)
)
)
(allow mach-lookup
; Used to look up the _CS_DARWIN_USER_CACHE_DIR in the sandbox.
(global-name "com.apple.bsd.dirhelper")
(global-name "com.apple.system.opendirectoryd.membership")
; Communicate with the security server for TLS certificate information.
(global-name "com.apple.SecurityServer")
(global-name "com.apple.networkd")
(global-name "com.apple.ocspd")
(global-name "com.apple.trustd.agent")
; Read network configuration.
(global-name "com.apple.SystemConfiguration.DNSConfiguration")
(global-name "com.apple.SystemConfiguration.configd")
)
(allow sysctl-read
(sysctl-name-regex #"^net.routetable")
)
(allow file-write*
(subpath (param "DARWIN_USER_CACHE_DIR"))
)
-192
View File
@@ -1,192 +0,0 @@
#![cfg(target_os = "macos")]
use std::collections::BTreeSet;
use std::path::PathBuf;
pub use codex_protocol::models::MacOsAutomationPermission;
pub use codex_protocol::models::MacOsContactsPermission;
pub use codex_protocol::models::MacOsPreferencesPermission;
pub use codex_protocol::models::MacOsSeatbeltProfileExtensions;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct SeatbeltExtensionPolicy {
pub(crate) policy: String,
pub(crate) dir_params: Vec<(String, PathBuf)>,
}
fn normalized_extensions(
extensions: &MacOsSeatbeltProfileExtensions,
) -> MacOsSeatbeltProfileExtensions {
let mut normalized = extensions.clone();
if let MacOsAutomationPermission::BundleIds(bundle_ids) = &extensions.macos_automation {
let bundle_ids = normalize_bundle_ids(bundle_ids);
normalized.macos_automation = if bundle_ids.is_empty() {
MacOsAutomationPermission::None
} else {
MacOsAutomationPermission::BundleIds(bundle_ids)
};
}
normalized
}
pub(crate) fn build_seatbelt_extensions(
extensions: &MacOsSeatbeltProfileExtensions,
) -> SeatbeltExtensionPolicy {
let extensions = normalized_extensions(extensions);
let mut clauses = Vec::new();
match extensions.macos_preferences {
MacOsPreferencesPermission::None => {}
MacOsPreferencesPermission::ReadOnly => {
clauses.push(
"(allow ipc-posix-shm-read* (ipc-posix-name-prefix \"apple.cfprefs.\"))"
.to_string(),
);
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.cfprefsd.daemon\")\n (global-name \"com.apple.cfprefsd.agent\")\n (local-name \"com.apple.cfprefsd.agent\"))"
.to_string(),
);
clauses.push("(allow user-preference-read)".to_string());
}
MacOsPreferencesPermission::ReadWrite => {
clauses.push(
"(allow ipc-posix-shm-read* (ipc-posix-name-prefix \"apple.cfprefs.\"))"
.to_string(),
);
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.cfprefsd.daemon\")\n (global-name \"com.apple.cfprefsd.agent\")\n (local-name \"com.apple.cfprefsd.agent\"))"
.to_string(),
);
clauses.push("(allow user-preference-read)".to_string());
clauses.push("(allow user-preference-write)".to_string());
clauses.push(
"(allow ipc-posix-shm-write-data (ipc-posix-name-prefix \"apple.cfprefs.\"))"
.to_string(),
);
clauses.push(
"(allow ipc-posix-shm-write-create (ipc-posix-name-prefix \"apple.cfprefs.\"))"
.to_string(),
);
}
}
match extensions.macos_automation {
MacOsAutomationPermission::None => {}
MacOsAutomationPermission::All => {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
.to_string(),
);
clauses.push("(allow appleevent-send)".to_string());
}
MacOsAutomationPermission::BundleIds(bundle_ids) => {
if !bundle_ids.is_empty() {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
.to_string(),
);
let destinations = bundle_ids
.iter()
.map(|bundle_id| format!(" (appleevent-destination \"{bundle_id}\")"))
.collect::<Vec<String>>()
.join("\n");
clauses.push(format!("(allow appleevent-send\n{destinations}\n)"));
}
}
}
if extensions.macos_launch_services {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.launchservicesd\")\n (global-name \"com.apple.lsd.mapdb\")\n (global-name \"com.apple.coreservices.quarantine-resolver\")\n (global-name \"com.apple.lsd.modifydb\"))"
.to_string(),
);
clauses.push("(allow lsopen)".to_string());
}
if extensions.macos_accessibility {
clauses.push("(allow mach-lookup (local-name \"com.apple.axserver\"))".to_string());
}
if extensions.macos_calendar {
clauses.push("(allow mach-lookup (global-name \"com.apple.CalendarAgent\"))".to_string());
}
if extensions.macos_reminders {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.CalendarAgent\")\n (global-name \"com.apple.remindd\"))"
.to_string(),
);
}
let mut dir_params = Vec::new();
match extensions.macos_contacts {
MacOsContactsPermission::None => {}
MacOsContactsPermission::ReadOnly => {
clauses.push(
"(allow file-read* file-test-existence\n (subpath \"/System/Library/Address Book Plug-Ins\")\n (subpath (param \"ADDRESSBOOK_DIR\")))"
.to_string(),
);
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.tccd\")\n (global-name \"com.apple.tccd.system\")\n (global-name \"com.apple.contactsd.persistence\")\n (global-name \"com.apple.AddressBook.ContactsAccountsService\")\n (global-name \"com.apple.contacts.account-caching\")\n (global-name \"com.apple.accountsd.accountmanager\"))"
.to_string(),
);
if let Some(addressbook_dir) = addressbook_dir() {
dir_params.push(("ADDRESSBOOK_DIR".to_string(), addressbook_dir));
}
}
MacOsContactsPermission::ReadWrite => {
clauses.push(
"(allow file-read* file-write*\n (subpath \"/System/Library/Address Book Plug-Ins\")\n (subpath (param \"ADDRESSBOOK_DIR\"))\n (subpath \"/var/folders\")\n (subpath \"/private/var/folders\"))"
.to_string(),
);
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.tccd\")\n (global-name \"com.apple.tccd.system\")\n (global-name \"com.apple.contactsd.persistence\")\n (global-name \"com.apple.AddressBook.ContactsAccountsService\")\n (global-name \"com.apple.contacts.account-caching\")\n (global-name \"com.apple.accountsd.accountmanager\")\n (global-name \"com.apple.securityd.xpc\"))"
.to_string(),
);
if let Some(addressbook_dir) = addressbook_dir() {
dir_params.push(("ADDRESSBOOK_DIR".to_string(), addressbook_dir));
}
}
}
if clauses.is_empty() {
SeatbeltExtensionPolicy::default()
} else {
SeatbeltExtensionPolicy {
policy: format!(
"; macOS permission profile extensions\n{}\n",
clauses.join("\n")
),
dir_params,
}
}
}
fn addressbook_dir() -> Option<PathBuf> {
Some(dirs::home_dir()?.join("Library/Application Support/AddressBook"))
}
fn normalize_bundle_ids(bundle_ids: &[String]) -> Vec<String> {
let mut unique = BTreeSet::new();
for bundle_id in bundle_ids {
let candidate = bundle_id.trim();
if is_valid_bundle_id(candidate) {
unique.insert(candidate.to_string());
}
}
unique.into_iter().collect()
}
fn is_valid_bundle_id(bundle_id: &str) -> bool {
if bundle_id.len() < 3 || !bundle_id.contains('.') {
return false;
}
bundle_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
}
#[cfg(test)]
#[path = "seatbelt_permissions_tests.rs"]
mod tests;
@@ -1,154 +0,0 @@
use super::MacOsAutomationPermission;
use super::MacOsContactsPermission;
use super::MacOsPreferencesPermission;
use super::MacOsSeatbeltProfileExtensions;
use super::build_seatbelt_extensions;
#[test]
fn preferences_read_only_emits_read_clauses_only() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
..Default::default()
});
assert!(policy.policy.contains("(allow user-preference-read)"));
assert!(!policy.policy.contains("(allow user-preference-write)"));
}
#[test]
fn preferences_read_write_emits_write_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
});
assert!(policy.policy.contains("(allow user-preference-read)"));
assert!(policy.policy.contains("(allow user-preference-write)"));
assert!(
policy.policy.contains(
"(allow ipc-posix-shm-write-create (ipc-posix-name-prefix \"apple.cfprefs.\"))"
)
);
}
#[test]
fn automation_all_emits_unscoped_appleevents() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_automation: MacOsAutomationPermission::All,
..Default::default()
});
assert!(policy.policy.contains("(allow appleevent-send)"));
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
}
#[test]
fn automation_bundle_ids_are_normalized_and_scoped() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
" com.apple.Notes ".to_string(),
"com.apple.Calendar".to_string(),
"bad bundle".to_string(),
"com.apple.Notes".to_string(),
]),
..Default::default()
});
assert!(
policy
.policy
.contains("(appleevent-destination \"com.apple.Calendar\")")
);
assert!(
policy
.policy
.contains("(appleevent-destination \"com.apple.Notes\")")
);
assert!(!policy.policy.contains("bad bundle"));
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
}
#[test]
fn launch_services_emit_launch_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_launch_services: true,
..Default::default()
});
assert!(
policy
.policy
.contains("com.apple.coreservices.launchservicesd")
);
assert!(policy.policy.contains("com.apple.lsd.mapdb"));
assert!(
policy
.policy
.contains("com.apple.coreservices.quarantine-resolver")
);
assert!(policy.policy.contains("com.apple.lsd.modifydb"));
assert!(policy.policy.contains("(allow lsopen)"));
}
#[test]
fn accessibility_and_calendar_emit_mach_lookups() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_accessibility: true,
macos_calendar: true,
..Default::default()
});
assert!(policy.policy.contains("com.apple.axserver"));
assert!(policy.policy.contains("com.apple.CalendarAgent"));
}
#[test]
fn reminders_emit_calendar_agent_and_remindd_lookups() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_reminders: true,
..Default::default()
});
assert!(policy.policy.contains("com.apple.CalendarAgent"));
assert!(policy.policy.contains("com.apple.remindd"));
}
#[test]
fn contacts_read_only_emit_contacts_read_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_contacts: MacOsContactsPermission::ReadOnly,
..Default::default()
});
assert!(
policy
.policy
.contains("(subpath \"/System/Library/Address Book Plug-Ins\")")
);
assert!(
policy
.policy
.contains("(subpath (param \"ADDRESSBOOK_DIR\"))")
);
assert!(policy.policy.contains("com.apple.contactsd.persistence"));
assert!(policy.policy.contains("com.apple.accountsd.accountmanager"));
assert!(!policy.policy.contains("com.apple.securityd.xpc"));
assert!(
policy
.dir_params
.iter()
.any(|(key, _)| key == "ADDRESSBOOK_DIR")
);
}
#[test]
fn contacts_read_write_emit_write_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_contacts: MacOsContactsPermission::ReadWrite,
..Default::default()
});
assert!(policy.policy.contains("(subpath \"/var/folders\")"));
assert!(policy.policy.contains("(subpath \"/private/var/folders\")"));
assert!(policy.policy.contains("com.apple.securityd.xpc"));
}
#[test]
fn default_extensions_emit_preferences_read_only_policy() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default());
assert!(policy.policy.contains("(allow user-preference-read)"));
assert!(!policy.policy.contains("(allow user-preference-write)"));
}
File diff suppressed because it is too large Load Diff
@@ -20,8 +20,6 @@ use crate::protocol::GranularApprovalConfig;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
use crate::sandboxing::SandboxPermissions;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::skills::SkillMetadata;
use codex_execpolicy::Decision;
use codex_execpolicy::Evaluation;
@@ -40,6 +38,8 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SkillScope;
#[cfg(target_os = "macos")]
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::ExecResult;