From 772f1d9fd392fcb63c9895c00ac3873316e1384e Mon Sep 17 00:00:00 2001 From: celia-oai Date: Thu, 12 Feb 2026 14:06:02 -0800 Subject: [PATCH 1/2] changes --- codex-rs/core/README.md | 20 ++ codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/seatbelt.rs | 85 ++++++- codex-rs/core/src/seatbelt_base_policy.sbpl | 3 - codex-rs/core/src/seatbelt_permissions.rs | 246 ++++++++++++++++++++ 5 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 codex-rs/core/src/seatbelt_permissions.rs diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 9974a591cf8..87ceeccc836 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -14,6 +14,26 @@ When using the workspace-write sandbox policy, the Seatbelt profile allows writes under the configured writable roots while keeping `.git` (directory or pointer file), the resolved `gitdir:` target, and `.codex` read-only. +Network access and filesystem read/write roots are controlled by +`SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it. + +Seatbelt also supports macOS permission-profile extensions layered on top of +`SandboxPolicy`: + +- `macos_preferences = "readonly"`: + enables cfprefs read clauses and `user-preference-read`. +- `macos_preferences = "readwrite"`: + includes readonly clauses plus `user-preference-write` and cfprefs shm write + clauses. +- `macos_automation = true`: + enables broad Apple Events send permissions. +- `macos_automation = ["com.apple.Notes", ...]`: + enables Apple Events send only to listed bundle IDs. +- `macos_accessibility = true`: + enables `com.apple.axserver` mach lookup. +- `macos_calendar = true`: + enables `com.apple.CalendarAgent` mach lookup. + ### Linux Expects the binary containing `codex-core` to run the equivalent of `codex sandbox linux` (legacy alias: `codex debug landlock`) when `arg0` is `codex-linux-sandbox`. See the `codex-arg0` crate for details. diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 764fc4fbba9..e3888ecf867 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -77,6 +77,7 @@ pub use model_provider_info::create_oss_provider_with_base_url; mod event_mapping; pub mod review_format; pub mod review_prompts; +mod seatbelt_permissions; mod thread_manager; pub mod web_search; pub mod windows_sandbox_read_grants; diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index fc425f2184c..270148f2050 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -14,6 +14,8 @@ use tokio::process::Child; 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; @@ -179,6 +181,24 @@ pub(crate) fn create_seatbelt_command_args( sandbox_policy_cwd: &Path, enforce_managed_network: bool, network: Option<&NetworkProxy>, +) -> Vec { + create_seatbelt_command_args_with_extensions( + command, + sandbox_policy, + sandbox_policy_cwd, + enforce_managed_network, + network, + None, + ) +} + +pub(crate) fn create_seatbelt_command_args_with_extensions( + command: Vec, + sandbox_policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + enforce_managed_network: bool, + network: Option<&NetworkProxy>, + extensions: Option<&MacOsSeatbeltProfileExtensions>, ) -> Vec { let (file_write_policy, file_write_dir_params) = { if sandbox_policy.has_full_disk_write_access() { @@ -275,15 +295,26 @@ pub(crate) fn create_seatbelt_command_args( let proxy = proxy_policy_inputs(network); let network_policy = dynamic_network_policy(sandbox_policy, enforce_managed_network, &proxy); + let seatbelt_extensions = extensions + .map(build_seatbelt_extensions) + .unwrap_or_default(); - let full_policy = format!( - "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" - ); + let full_policy = if seatbelt_extensions.policy.is_empty() { + format!( + "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" + ) + } else { + format!( + "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}\n{}", + seatbelt_extensions.policy + ) + }; let dir_params = [ file_read_dir_params, file_write_dir_params, macos_dir_params(), + seatbelt_extensions.dir_params, ] .concat(); @@ -328,10 +359,14 @@ mod tests { use super::MACOS_SEATBELT_BASE_POLICY; use super::ProxyPolicyInputs; use super::create_seatbelt_command_args; + use super::create_seatbelt_command_args_with_extensions; use super::dynamic_network_policy; use super::macos_dir_params; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; + use crate::seatbelt_permissions::MacOsAutomationPermission; + use crate::seatbelt_permissions::MacOsPreferencesPermission; + use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -395,6 +430,50 @@ mod tests { ); } + #[test] + fn seatbelt_args_include_macos_permission_extensions() { + let cwd = std::env::temp_dir(); + let args = create_seatbelt_command_args_with_extensions( + vec!["echo".to_string(), "ok".to_string()], + &SandboxPolicy::new_read_only_policy(), + cwd.as_path(), + false, + None, + Some(&MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::ReadWrite, + macos_automation: MacOsAutomationPermission::BundleIds(vec![ + "com.apple.Notes".to_string(), + ]), + macos_accessibility: true, + macos_calendar: true, + }), + ); + let policy = &args[1]; + + assert!(policy.contains("(allow user-preference-write)")); + assert!(policy.contains("(appleevent-destination \"com.apple.Notes\")")); + assert!(policy.contains("com.apple.axserver")); + assert!(policy.contains("com.apple.CalendarAgent")); + } + + #[test] + fn seatbelt_args_omit_macos_extensions_when_profile_is_empty() { + let cwd = std::env::temp_dir(); + let args = create_seatbelt_command_args_with_extensions( + vec!["echo".to_string(), "ok".to_string()], + &SandboxPolicy::new_read_only_policy(), + cwd.as_path(), + false, + None, + Some(&MacOsSeatbeltProfileExtensions::default()), + ); + let policy = &args[1]; + assert!(!policy.contains("appleevent-send")); + assert!(!policy.contains("com.apple.axserver")); + assert!(!policy.contains("com.apple.CalendarAgent")); + assert!(!policy.contains("user-preference-write")); + } + #[test] fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { let policy = dynamic_network_policy( diff --git a/codex-rs/core/src/seatbelt_base_policy.sbpl b/codex-rs/core/src/seatbelt_base_policy.sbpl index 00676a86f4d..72b13c96cd7 100644 --- a/codex-rs/core/src/seatbelt_base_policy.sbpl +++ b/codex-rs/core/src/seatbelt_base_policy.sbpl @@ -12,9 +12,6 @@ (allow process-fork) (allow signal (target same-sandbox)) -; Allow cf prefs to work. -(allow user-preference-read) - ; process-info (allow process-info* (target same-sandbox)) diff --git a/codex-rs/core/src/seatbelt_permissions.rs b/codex-rs/core/src/seatbelt_permissions.rs new file mode 100644 index 00000000000..29313d7a3c9 --- /dev/null +++ b/codex-rs/core/src/seatbelt_permissions.rs @@ -0,0 +1,246 @@ +#![cfg(target_os = "macos")] + +use std::collections::BTreeSet; +use std::path::PathBuf; + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum MacOsPreferencesPermission { + #[default] + None, + ReadOnly, + ReadWrite, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum MacOsAutomationPermission { + #[default] + None, + All, + BundleIds(Vec), +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MacOsSeatbeltProfileExtensions { + pub macos_preferences: MacOsPreferencesPermission, + pub macos_automation: MacOsAutomationPermission, + pub macos_accessibility: bool, + pub macos_calendar: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct SeatbeltExtensionPolicy { + pub(crate) policy: String, + pub(crate) dir_params: Vec<(String, PathBuf)>, +} + +impl MacOsSeatbeltProfileExtensions { + pub fn normalized(&self) -> Self { + let mut normalized = self.clone(); + if let MacOsAutomationPermission::BundleIds(bundle_ids) = &self.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 = extensions.normalized(); + 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.launchservicesd\")\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 (global-name \"com.apple.coreservices.appleevents\"))" + .to_string(), + ); + let destinations = bundle_ids + .iter() + .map(|bundle_id| format!(" (appleevent-destination \"{bundle_id}\")")) + .collect::>() + .join("\n"); + clauses.push(format!("(allow appleevent-send\n{destinations}\n)")); + } + } + } + + 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 clauses.is_empty() { + SeatbeltExtensionPolicy::default() + } else { + SeatbeltExtensionPolicy { + policy: format!( + "; macOS permission profile extensions\n{}\n", + clauses.join("\n") + ), + dir_params: Vec::new(), + } + } +} + +fn normalize_bundle_ids(bundle_ids: &[String]) -> Vec { + 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)] +mod tests { + use super::MacOsAutomationPermission; + use super::MacOsPreferencesPermission; + use super::MacOsSeatbeltProfileExtensions; + use super::build_seatbelt_extensions; + use pretty_assertions::assert_eq; + + #[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.launchservicesd") + ); + } + + #[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")); + } + + #[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 empty_extensions_emit_empty_policy() { + let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default()); + assert_eq!(policy.policy, ""); + } +} From 60080230e83eb5afa62904e5217a2f7f89998293 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Thu, 12 Feb 2026 14:17:17 -0800 Subject: [PATCH 2/2] changes --- codex-rs/core/README.md | 4 +++ codex-rs/core/src/seatbelt.rs | 46 ++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 87ceeccc836..8a66b47b48c 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -20,6 +20,10 @@ Network access and filesystem read/write roots are controlled by Seatbelt also supports macOS permission-profile extensions layered on top of `SandboxPolicy`: +- no extension profile provided: + keeps legacy default preferences read access (`user-preference-read`). +- extension profile provided with no `macos_preferences` grant: + does not add preferences access clauses. - `macos_preferences = "readonly"`: enables cfprefs read clauses and `user-preference-read`. - `macos_preferences = "readwrite"`: diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 270148f2050..29c05e0183d 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -14,6 +14,7 @@ use tokio::process::Child; use url::Url; use crate::protocol::SandboxPolicy; +use crate::seatbelt_permissions::MacOsPreferencesPermission; use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions; use crate::seatbelt_permissions::build_seatbelt_extensions; use crate::spawn::CODEX_SANDBOX_ENV_VAR; @@ -295,9 +296,16 @@ pub(crate) fn create_seatbelt_command_args_with_extensions( let proxy = proxy_policy_inputs(network); let network_policy = dynamic_network_policy(sandbox_policy, enforce_managed_network, &proxy); - let seatbelt_extensions = extensions - .map(build_seatbelt_extensions) - .unwrap_or_default(); + let seatbelt_extensions = extensions.map_or_else( + || { + // Backward-compatibility default when no extension profile is provided. + build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::ReadOnly, + ..Default::default() + }) + }, + build_seatbelt_extensions, + ); let full_policy = if seatbelt_extensions.policy.is_empty() { format!( @@ -456,6 +464,21 @@ mod tests { assert!(policy.contains("com.apple.CalendarAgent")); } + #[test] + fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() { + let cwd = std::env::temp_dir(); + let args = create_seatbelt_command_args( + vec!["echo".to_string(), "ok".to_string()], + &SandboxPolicy::new_read_only_policy(), + cwd.as_path(), + false, + None, + ); + let policy = &args[1]; + assert!(policy.contains("(allow user-preference-read)")); + assert!(!policy.contains("(allow user-preference-write)")); + } + #[test] fn seatbelt_args_omit_macos_extensions_when_profile_is_empty() { let cwd = std::env::temp_dir(); @@ -471,6 +494,7 @@ mod tests { assert!(!policy.contains("appleevent-send")); assert!(!policy.contains("com.apple.axserver")); assert!(!policy.contains("com.apple.CalendarAgent")); + assert!(!policy.contains("user-preference-read")); assert!(!policy.contains("user-preference-write")); } @@ -643,6 +667,14 @@ mod tests { (allow file-write* (require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")) (subpath (param "WRITABLE_ROOT_2")) ) + +; macOS permission profile extensions +(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs.")) +(allow mach-lookup + (global-name "com.apple.cfprefsd.daemon") + (global-name "com.apple.cfprefsd.agent") + (local-name "com.apple.cfprefsd.agent")) +(allow user-preference-read) "#, ); @@ -930,6 +962,14 @@ mod tests { (allow file-write* (require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry} ) + +; macOS permission profile extensions +(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs.")) +(allow mach-lookup + (global-name "com.apple.cfprefsd.daemon") + (global-name "com.apple.cfprefsd.agent") + (local-name "com.apple.cfprefsd.agent")) +(allow user-preference-read) "#, );