feat: Add additional macOS Sandbox Permissions for Launch Services, Contacts, Reminders (#14155)

Add additional macOS Sandbox Permissions levers for the following:

- Launch Services
- Contacts
- Reminders
This commit is contained in:
Leo Shimonaka
2026-03-10 16:34:47 -07:00
committed by Michael Bolin
Unverified
parent 8ac27b2a16
commit 889b4796fc
25 changed files with 779 additions and 25 deletions
@@ -27,6 +27,19 @@
(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"))
@@ -87,6 +100,11 @@
(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"))
@@ -178,4 +196,4 @@
; App sandbox extensions
(allow file-read* (extension "com.apple.app-sandbox.read"))
(allow file-read* file-write* (extension "com.apple.app-sandbox.read-write"))
(allow file-read* file-write* (extension "com.apple.app-sandbox.read-write"))
@@ -1,6 +1,7 @@
use std::collections::BTreeSet;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
@@ -24,8 +25,14 @@ pub(crate) fn merge_macos_seatbelt_profile_extensions(
&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()),
}
@@ -45,8 +52,12 @@ pub(crate) fn intersect_macos_seatbelt_profile_extensions(
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,
@@ -68,6 +79,17 @@ fn union_macos_preferences_permission(
}
}
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
@@ -133,8 +155,10 @@ mod tests {
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;
@@ -146,8 +170,11 @@ mod tests {
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,
@@ -155,8 +182,11 @@ mod tests {
"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 =
@@ -170,8 +200,11 @@ mod tests {
"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,
}
);
}
@@ -219,8 +252,11 @@ mod tests {
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();
@@ -229,4 +265,14 @@ mod tests {
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);
}
}
+20
View File
@@ -737,6 +737,8 @@ mod tests {
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsAutomationPermission;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsContactsPermission;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsPreferencesPermission;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
@@ -981,8 +983,11 @@ mod tests {
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,
}),
..Default::default()
};
@@ -1013,8 +1018,11 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
})
@@ -1027,8 +1035,11 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
})
);
}
@@ -1092,8 +1103,11 @@ mod tests {
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::None,
}),
Some(&PermissionProfile {
file_system: Some(FileSystemPermissions {
@@ -1105,8 +1119,11 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
}),
@@ -1120,8 +1137,11 @@ mod tests {
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
})
);
}
+7 -2
View File
@@ -27,7 +27,8 @@ 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_SEATBELT_PLATFORM_DEFAULTS: &str = include_str!("seatbelt_platform_defaults.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
@@ -529,7 +530,7 @@ pub(crate) fn create_seatbelt_command_args_for_policies_with_extensions(
network_policy,
];
if include_platform_defaults {
policy_sections.push(MACOS_SEATBELT_PLATFORM_DEFAULTS.to_string());
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
}
if !seatbelt_extensions.policy.is_empty() {
policy_sections.push(seatbelt_extensions.policy.clone());
@@ -599,6 +600,7 @@ mod tests {
use crate::protocol::SandboxPolicy;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt_permissions::MacOsAutomationPermission;
use crate::seatbelt_permissions::MacOsContactsPermission;
use crate::seatbelt_permissions::MacOsPreferencesPermission;
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use codex_protocol::permissions::FileSystemAccessMode;
@@ -787,8 +789,11 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
);
let policy = &args[1];
+128 -8
View File
@@ -4,6 +4,7 @@ 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;
@@ -74,7 +75,7 @@ pub(crate) fn build_seatbelt_extensions(
MacOsAutomationPermission::None => {}
MacOsAutomationPermission::All => {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.launchservicesd\")\n (global-name \"com.apple.coreservices.appleevents\"))"
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
.to_string(),
);
clauses.push("(allow appleevent-send)".to_string());
@@ -82,7 +83,7 @@ pub(crate) fn build_seatbelt_extensions(
MacOsAutomationPermission::BundleIds(bundle_ids) => {
if !bundle_ids.is_empty() {
clauses.push(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.launchservicesd\")\n (global-name \"com.apple.coreservices.appleevents\"))"
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
.to_string(),
);
let destinations = bundle_ids
@@ -95,6 +96,14 @@ pub(crate) fn build_seatbelt_extensions(
}
}
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());
}
@@ -103,6 +112,44 @@ pub(crate) fn build_seatbelt_extensions(
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 {
@@ -111,11 +158,15 @@ pub(crate) fn build_seatbelt_extensions(
"; macOS permission profile extensions\n{}\n",
clauses.join("\n")
),
dir_params: Vec::new(),
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 {
@@ -139,6 +190,7 @@ fn is_valid_bundle_id(bundle_id: &str) -> bool {
#[cfg(test)]
mod tests {
use super::MacOsAutomationPermission;
use super::MacOsContactsPermission;
use super::MacOsPreferencesPermission;
use super::MacOsSeatbeltProfileExtensions;
use super::build_seatbelt_extensions;
@@ -173,11 +225,7 @@ mod tests {
..Default::default()
});
assert!(policy.policy.contains("(allow appleevent-send)"));
assert!(
policy
.policy
.contains("com.apple.coreservices.launchservicesd")
);
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
}
#[test]
@@ -202,6 +250,28 @@ mod tests {
.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]
@@ -215,6 +285,56 @@ mod tests {
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());
+41
View File
@@ -867,6 +867,7 @@ mod tests {
use codex_protocol::config_types::TrustLevel;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use codex_protocol::models::PermissionProfile;
@@ -1466,6 +1467,7 @@ permissions:
macos_preferences: "read_write"
macos_automation:
- "com.apple.Notes"
macos_launch_services: true
macos_accessibility: true
macos_calendar: true
"#,
@@ -1480,8 +1482,39 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
})
);
}
#[test]
fn skill_metadata_parses_macos_reminders_permission_yaml() {
let parsed = serde_yaml::from_str::<SkillMetadataFile>(
r#"
permissions:
macos:
macos_reminders: true
"#,
)
.expect("parse reminders skill metadata");
assert_eq!(
parsed.permissions,
Some(PermissionProfile {
macos: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::None,
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
macos_reminders: true,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
})
@@ -1503,6 +1536,7 @@ permissions:
macos_preferences: "read_write"
macos_automation:
- "com.apple.Notes"
macos_launch_services: true
macos_accessibility: true
macos_calendar: true
"#,
@@ -1525,8 +1559,11 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string()
],),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
})
@@ -1548,6 +1585,7 @@ permissions:
macos_preferences: "read_write"
macos_automation:
- "com.apple.Notes"
macos_launch_services: true
macos_accessibility: true
macos_calendar: true
"#,
@@ -1570,8 +1608,11 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string()
],),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}),
..Default::default()
})
@@ -657,6 +657,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac
PermissionProfile {
macos: Some(MacOsSeatbeltProfileExtensions {
macos_calendar: true,
macos_reminders: false,
..Default::default()
}),
..Default::default()