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 authored and GitHub committed 2026-03-23 21:26:35 -07:00
1 parent 2227248cd6
commit 431af0807c
19 files changed
+650 -601

No files matched your search

@@ -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
ready‑to‑spawn 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;