Compare commits

...

3 Commits

Author SHA1 Message Date
Bede Carroll
f8f549d779 test(core): fix mach service ordering expectations
Co-authored-by: Codex <noreply@openai.com>
2026-03-13 23:21:07 -07:00
Bede Carroll
9c415f4a71 test: replace vendor-specific mach service examples
Co-authored-by: Codex <noreply@openai.com>
2026-03-13 21:21:09 -07:00
Bede Carroll
6720485f6f feat: add macOS Mach service sandbox permissions
Support turn-scoped macOS mach-lookup grants through the protocol, app-server, seatbelt policy generation, and approval UI.

Co-authored-by: Codex <noreply@openai.com>
2026-03-13 17:22:33 -07:00
24 changed files with 345 additions and 8 deletions

View File

@@ -45,6 +45,12 @@
"launchServices": {
"type": "boolean"
},
"machServices": {
"items": {
"type": "string"
},
"type": "array"
},
"preferences": {
"$ref": "#/definitions/MacOsPreferencesPermission"
},
@@ -58,6 +64,7 @@
"calendar",
"contacts",
"launchServices",
"machServices",
"preferences",
"reminders"
],

View File

@@ -45,6 +45,12 @@
"launchServices": {
"type": "boolean"
},
"machServices": {
"items": {
"type": "string"
},
"type": "array"
},
"preferences": {
"$ref": "#/definitions/MacOsPreferencesPermission"
},
@@ -58,6 +64,7 @@
"calendar",
"contacts",
"launchServices",
"machServices",
"preferences",
"reminders"
],

View File

@@ -79,6 +79,15 @@
"null"
]
},
"machServices": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"preferences": {
"anyOf": [
{

View File

@@ -45,6 +45,12 @@
"launchServices": {
"type": "boolean"
},
"machServices": {
"items": {
"type": "string"
},
"type": "array"
},
"preferences": {
"$ref": "#/definitions/MacOsPreferencesPermission"
},
@@ -58,6 +64,7 @@
"calendar",
"contacts",
"launchServices",
"machServices",
"preferences",
"reminders"
],

View File

@@ -41,6 +41,12 @@
"launchServices": {
"type": "boolean"
},
"machServices": {
"items": {
"type": "string"
},
"type": "array"
},
"preferences": {
"$ref": "#/definitions/MacOsPreferencesPermission"
},
@@ -54,6 +60,7 @@
"calendar",
"contacts",
"launchServices",
"machServices",
"preferences",
"reminders"
],
@@ -2238,6 +2245,15 @@
"null"
]
},
"machServices": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"preferences": {
"anyOf": [
{

View File

@@ -5,4 +5,4 @@ import type { MacOsAutomationPermission } from "../MacOsAutomationPermission";
import type { MacOsContactsPermission } from "../MacOsContactsPermission";
import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission";
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, launchServices: boolean, accessibility: boolean, calendar: boolean, reminders: boolean, contacts: MacOsContactsPermission, };
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, machServices: Array<string>, launchServices: boolean, accessibility: boolean, calendar: boolean, reminders: boolean, contacts: MacOsContactsPermission, };

View File

@@ -5,4 +5,4 @@ import type { MacOsAutomationPermission } from "../MacOsAutomationPermission";
import type { MacOsContactsPermission } from "../MacOsContactsPermission";
import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission";
export type GrantedMacOsPermissions = { preferences?: MacOsPreferencesPermission, automations?: MacOsAutomationPermission, launchServices?: boolean, accessibility?: boolean, calendar?: boolean, reminders?: boolean, contacts?: MacOsContactsPermission, };
export type GrantedMacOsPermissions = { preferences?: MacOsPreferencesPermission, automations?: MacOsAutomationPermission, machServices?: Array<string>, launchServices?: boolean, accessibility?: boolean, calendar?: boolean, reminders?: boolean, contacts?: MacOsContactsPermission, };

View File

@@ -1021,6 +1021,7 @@ impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
pub struct AdditionalMacOsPermissions {
pub preferences: CoreMacOsPreferencesPermission,
pub automations: CoreMacOsAutomationPermission,
pub mach_services: Vec<String>,
pub launch_services: bool,
pub accessibility: bool,
pub calendar: bool,
@@ -1033,6 +1034,7 @@ impl From<CoreMacOsSeatbeltProfileExtensions> for AdditionalMacOsPermissions {
Self {
preferences: value.macos_preferences,
automations: value.macos_automation,
mach_services: value.macos_mach_services,
launch_services: value.macos_launch_services,
accessibility: value.macos_accessibility,
calendar: value.macos_calendar,
@@ -1047,6 +1049,7 @@ impl From<AdditionalMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
Self {
macos_preferences: value.preferences,
macos_automation: value.automations,
macos_mach_services: value.mach_services,
macos_launch_services: value.launch_services,
macos_accessibility: value.accessibility,
macos_calendar: value.calendar,
@@ -1120,6 +1123,9 @@ pub struct GrantedMacOsPermissions {
pub automations: Option<CoreMacOsAutomationPermission>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub mach_services: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub launch_services: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -1144,6 +1150,7 @@ impl From<GrantedMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
macos_automation: value
.automations
.unwrap_or(CoreMacOsAutomationPermission::None),
macos_mach_services: value.mach_services.unwrap_or_default(),
macos_launch_services: value.launch_services.unwrap_or(false),
macos_accessibility: value.accessibility.unwrap_or(false),
macos_calendar: value.calendar.unwrap_or(false),
@@ -1173,6 +1180,7 @@ impl From<GrantedPermissionProfile> for CorePermissionProfile {
let macos = value.macos.and_then(|macos| {
if macos.preferences.is_none()
&& macos.automations.is_none()
&& macos.mach_services.as_ref().is_none_or(Vec::is_empty)
&& macos.launch_services.is_none()
&& macos.accessibility.is_none()
&& macos.calendar.is_none()
@@ -5900,6 +5908,7 @@ mod tests {
"automations": {
"bundle_ids": ["com.apple.Notes"]
},
"machServices": ["com.vendor.helper"],
"launchServices": false,
"accessibility": false,
"calendar": false,
@@ -5918,9 +5927,17 @@ mod tests {
params
.additional_permissions
.and_then(|permissions| permissions.macos)
.map(|macos| (macos.automations, macos.launch_services, macos.contacts)),
.map(|macos| {
(
macos.automations,
macos.mach_services,
macos.launch_services,
macos.contacts,
)
}),
Some((
CoreMacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string(),]),
vec!["com.vendor.helper".to_string()],
false,
CoreMacOsContactsPermission::ReadOnly,
))
@@ -5944,6 +5961,7 @@ mod tests {
"macos": {
"preferences": "read_only",
"automations": "none",
"machServices": [],
"launchServices": false,
"accessibility": false,
"calendar": false,
@@ -6011,6 +6029,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::ReadOnly,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -6035,6 +6054,26 @@ mod tests {
macos_automation: CoreMacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
macos_reminders: false,
macos_contacts: CoreMacOsContactsPermission::None,
}),
),
(
json!({
"machServices": ["com.vendor.helper"],
}),
Some(GrantedMacOsPermissions {
mach_services: Some(vec!["com.vendor.helper".to_string()]),
..Default::default()
}),
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: vec!["com.vendor.helper".to_string()],
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -6053,6 +6092,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: true,
macos_accessibility: false,
macos_calendar: false,
@@ -6071,6 +6111,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: true,
macos_calendar: false,
@@ -6089,6 +6130,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: true,
@@ -6107,6 +6149,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -6125,6 +6168,7 @@ mod tests {
Some(CoreMacOsSeatbeltProfileExtensions {
macos_preferences: CoreMacOsPreferencesPermission::None,
macos_automation: CoreMacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,

View File

@@ -983,6 +983,8 @@ The client responds with `result.permissions`, which should be the granted subse
Only the granted subset matters on the wire. Any permissions omitted from `result.permissions` are treated as denied, including omitted nested keys inside `result.permissions.macos`, so a sparse response like `{ "permissions": { "macos": { "accessibility": true } } }` grants only accessibility. Any permissions not present in the original request are ignored by the server.
The macOS permission object supports `preferences`, `automations`, `machServices` (exact global Mach service names for `mach-lookup`), `launchServices`, `accessibility`, `calendar`, `reminders`, and `contacts`.
Within the same turn, granted permissions are sticky: later shell-like tool calls can automatically reuse the granted subset without reissuing a separate permission request.
If the session approval policy uses `Granular` with `request_permissions: false`, standalone `request_permissions` tool calls are auto-denied and no `item/permissions/requestApproval` prompt is sent. Inline `with_additional_permissions` command requests remain controlled by `sandbox_approval`, and any previously granted permissions remain sticky for later shell-like calls in the same turn.

View File

@@ -35,6 +35,8 @@ Seatbelt also supports macOS permission-profile extensions layered on top of
enables Apple Events send only to listed bundle IDs.
- `macos_launch_services = true`:
enables LaunchServices lookups and open/launch operations.
- `macos_mach_services = ["com.vendor.helper", ...]`:
enables `mach-lookup` for listed global Mach service names.
- `macos_accessibility = true`:
enables `com.apple.axserver` mach lookup.
- `macos_calendar = true`:

View File

@@ -25,6 +25,10 @@ pub(crate) fn merge_macos_seatbelt_profile_extensions(
&base.macos_automation,
&permissions.macos_automation,
),
macos_mach_services: union_string_permissions(
&base.macos_mach_services,
&permissions.macos_mach_services,
),
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,
@@ -52,6 +56,10 @@ pub(crate) fn intersect_macos_seatbelt_profile_extensions(
Some(MacOsSeatbeltProfileExtensions {
macos_preferences: requested.macos_preferences.min(granted.macos_preferences),
macos_automation,
macos_mach_services: intersect_string_permissions(
&requested.macos_mach_services,
&granted.macos_mach_services,
),
macos_launch_services: requested.macos_launch_services
&& granted.macos_launch_services,
macos_accessibility: requested.macos_accessibility && granted.macos_accessibility,
@@ -90,6 +98,15 @@ fn union_macos_contacts_permission(
}
}
fn union_string_permissions(base: &[String], requested: &[String]) -> Vec<String> {
base.iter()
.chain(requested.iter())
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
/// Unions two automation permissions by keeping the more permissive result.
///
/// `All` wins over everything, `None` yields to the other side, and two bundle
@@ -149,6 +166,15 @@ fn intersect_macos_automation_permission(
}
}
fn intersect_string_permissions(requested: &[String], granted: &[String]) -> Vec<String> {
let granted = granted.iter().collect::<BTreeSet<_>>();
requested
.iter()
.filter(|value| granted.contains(value))
.cloned()
.collect()
}
#[cfg(all(test, target_os = "macos"))]
#[path = "macos_permissions_tests.rs"]
mod tests;

View File

@@ -17,6 +17,7 @@ fn merge_extensions_widens_permissions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -29,6 +30,10 @@ fn merge_extensions_widens_permissions() {
"com.apple.Notes".to_string(),
"com.apple.Calendar".to_string(),
]),
macos_mach_services: vec![
"com.vendor.helper".to_string(),
"com.apple.logd".to_string(),
],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -47,6 +52,10 @@ fn merge_extensions_widens_permissions() {
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec![
"com.apple.logd".to_string(),
"com.vendor.helper".to_string(),
],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -97,6 +106,7 @@ 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_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: false,
macos_accessibility: true,
macos_calendar: true,

View File

@@ -265,6 +265,7 @@ fn intersect_permission_profiles_preserves_default_macos_grants() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: false,
macos_accessibility: true,
macos_calendar: true,
@@ -300,6 +301,7 @@ fn normalize_additional_permissions_preserves_macos_permissions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -317,6 +319,7 @@ fn normalize_additional_permissions_preserves_macos_permissions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -385,6 +388,7 @@ fn effective_permissions_merge_macos_extensions_with_additional_permissions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -401,6 +405,7 @@ fn effective_permissions_merge_macos_extensions_with_additional_permissions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.vendor.helper".to_string()],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -419,6 +424,10 @@ fn effective_permissions_merge_macos_extensions_with_additional_permissions() {
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec![
"com.apple.logd".to_string(),
"com.vendor.helper".to_string(),
],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,

View File

@@ -26,6 +26,7 @@ fn normalized_extensions(
MacOsAutomationPermission::BundleIds(bundle_ids)
};
}
normalized.macos_mach_services = normalize_mach_services(&extensions.macos_mach_services);
normalized
}
@@ -96,6 +97,16 @@ pub(crate) fn build_seatbelt_extensions(
}
}
if !extensions.macos_mach_services.is_empty() {
let services = extensions
.macos_mach_services
.iter()
.map(|service| format!(" (global-name \"{service}\")"))
.collect::<Vec<String>>()
.join("\n");
clauses.push(format!("(allow mach-lookup\n{services}\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\"))"
@@ -178,6 +189,17 @@ fn normalize_bundle_ids(bundle_ids: &[String]) -> Vec<String> {
unique.into_iter().collect()
}
fn normalize_mach_services(mach_services: &[String]) -> Vec<String> {
let mut unique = BTreeSet::new();
for mach_service in mach_services {
let candidate = mach_service.trim();
if is_valid_mach_service(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;
@@ -187,6 +209,15 @@ fn is_valid_bundle_id(bundle_id: &str) -> bool {
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
}
fn is_valid_mach_service(mach_service: &str) -> bool {
if mach_service.len() < 3 || !mach_service.contains('.') {
return false;
}
mach_service
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
}
#[cfg(test)]
#[path = "seatbelt_permissions_tests.rs"]
mod tests;

View File

@@ -64,6 +64,22 @@ fn automation_bundle_ids_are_normalized_and_scoped() {
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
}
#[test]
fn mach_services_emit_mach_lookup_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
macos_mach_services: vec![
" com.vendor.helper ".to_string(),
"com.apple.logd".to_string(),
"bad service".to_string(),
"com.apple.logd".to_string(),
],
..Default::default()
});
assert!(policy.policy.contains("com.vendor.helper"));
assert!(policy.policy.contains("com.apple.logd"));
assert!(!policy.policy.contains("bad service"));
}
#[test]
fn launch_services_emit_launch_clauses() {
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {

View File

@@ -202,6 +202,7 @@ fn seatbelt_args_include_macos_permission_extensions() {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.apple.logd".to_string()],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -213,6 +214,7 @@ fn seatbelt_args_include_macos_permission_extensions() {
assert!(policy.contains("(allow user-preference-write)"));
assert!(policy.contains("(appleevent-destination \"com.apple.Notes\")"));
assert!(policy.contains("com.apple.logd"));
assert!(policy.contains("com.apple.axserver"));
assert!(policy.contains("com.apple.CalendarAgent"));
}

View File

@@ -689,6 +689,7 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: Vec::new(),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -718,6 +719,7 @@ permissions:
macos: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -795,6 +797,7 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string()
],),
macos_mach_services: Vec::new(),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -844,6 +847,7 @@ permissions:
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string()
],),
macos_mach_services: Vec::new(),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,

View File

@@ -347,6 +347,7 @@ fn shell_request_escalation_execution_is_explicit() {
let network_sandbox_policy = NetworkSandboxPolicy::Restricted;
let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_mach_services: vec!["com.apple.logd".to_string()],
..Default::default()
};

View File

@@ -499,6 +499,80 @@ fn create_file_system_permissions_schema() -> JsonSchema {
}
}
fn create_macos_permissions_schema() -> JsonSchema {
JsonSchema::Object {
properties: BTreeMap::from([
(
"preferences".to_string(),
JsonSchema::String {
description: Some(
"macOS preferences access. Supported values: `none`, `read_only`, or `read_write`."
.to_string(),
),
},
),
(
"automations".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"macOS automation access as app bundle identifiers.".to_string(),
),
},
),
(
"mach_services".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"Exact macOS global Mach service names to allow for mach-lookup (for example, `com.vendor.helper`)."
.to_string(),
),
},
),
(
"launch_services".to_string(),
JsonSchema::Boolean {
description: Some(
"Whether to request macOS Launch Services access.".to_string(),
),
},
),
(
"accessibility".to_string(),
JsonSchema::Boolean {
description: Some(
"Whether to request macOS accessibility access.".to_string(),
),
},
),
(
"calendar".to_string(),
JsonSchema::Boolean {
description: Some("Whether to request macOS calendar access.".to_string()),
},
),
(
"reminders".to_string(),
JsonSchema::Boolean {
description: Some("Whether to request macOS reminders access.".to_string()),
},
),
(
"contacts".to_string(),
JsonSchema::String {
description: Some(
"macOS contacts access. Supported values: `none`, `read_only`, or `read_write`."
.to_string(),
),
},
),
]),
required: None,
additional_properties: Some(false.into()),
}
}
fn create_additional_permissions_schema() -> JsonSchema {
JsonSchema::Object {
properties: BTreeMap::from([
@@ -507,6 +581,7 @@ fn create_additional_permissions_schema() -> JsonSchema {
"file_system".to_string(),
create_file_system_permissions_schema(),
),
("macos".to_string(), create_macos_permissions_schema()),
]),
required: None,
additional_properties: Some(false.into()),
@@ -536,7 +611,7 @@ fn create_approval_parameters(
JsonSchema::String {
description: Some(
if exec_permission_approvals_enabled {
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem or network permissions (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem, network, or macOS permissions (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
} else {
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
}

View File

@@ -2269,7 +2269,7 @@ fn shell_tool_with_request_permission_includes_additional_permissions() {
panic!("expected sandbox_permissions description");
};
assert!(description.contains("with_additional_permissions"));
assert!(description.contains("filesystem or network permissions"));
assert!(description.contains("macOS permissions"));
let Some(JsonSchema::Object {
properties: additional_properties,
@@ -2280,7 +2280,25 @@ fn shell_tool_with_request_permission_includes_additional_permissions() {
};
assert!(additional_properties.contains_key("network"));
assert!(additional_properties.contains_key("file_system"));
assert!(!additional_properties.contains_key("macos"));
assert!(additional_properties.contains_key("macos"));
let Some(JsonSchema::Object {
properties: macos_properties,
additional_properties,
..
}) = additional_properties.get("macos")
else {
panic!("expected macos object");
};
assert_eq!(additional_properties, &Some(false.into()));
assert!(macos_properties.contains_key("preferences"));
assert!(macos_properties.contains_key("automations"));
assert!(macos_properties.contains_key("mach_services"));
assert!(macos_properties.contains_key("launch_services"));
assert!(macos_properties.contains_key("accessibility"));
assert!(macos_properties.contains_key("calendar"));
assert!(macos_properties.contains_key("reminders"));
assert!(macos_properties.contains_key("contacts"));
}
#[test]

View File

@@ -197,6 +197,8 @@ pub struct MacOsSeatbeltProfileExtensions {
pub macos_preferences: MacOsPreferencesPermission,
#[serde(alias = "automations")]
pub macos_automation: MacOsAutomationPermission,
#[serde(alias = "mach_services")]
pub macos_mach_services: Vec<String>,
#[serde(alias = "launch_services")]
pub macos_launch_services: bool,
#[serde(alias = "accessibility")]
@@ -1657,6 +1659,7 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: Vec::new(),
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -1684,6 +1687,7 @@ mod tests {
macos: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::None,
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -1709,6 +1713,7 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: Vec::new(),
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
@@ -1724,6 +1729,7 @@ mod tests {
serde_json::from_value::<MacOsSeatbeltProfileExtensions>(serde_json::json!({
"preferences": "read_write",
"automations": ["com.apple.Notes"],
"mach_services": ["com.vendor.helper"],
"launch_services": true,
"accessibility": true,
"calendar": true,
@@ -1739,6 +1745,7 @@ mod tests {
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.vendor.helper".to_string()],
macos_launch_services: true,
macos_accessibility: true,
macos_calendar: true,
@@ -1748,6 +1755,35 @@ mod tests {
);
}
#[test]
fn macos_seatbelt_profile_extensions_deserializes_mach_services() {
let permissions =
serde_json::from_value::<MacOsSeatbeltProfileExtensions>(serde_json::json!({
"mach_services": [
"com.vendor.helper",
"com.apple.logd",
]
}))
.expect("deserialize macos mach services");
assert_eq!(
permissions,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::None,
macos_mach_services: vec![
"com.vendor.helper".to_string(),
"com.apple.logd".to_string(),
],
macos_launch_services: false,
macos_accessibility: false,
macos_calendar: false,
macos_reminders: false,
macos_contacts: MacOsContactsPermission::None,
}
);
}
#[test]
fn macos_automation_permission_deserializes_all_and_none() {
let all = serde_json::from_str::<MacOsAutomationPermission>("\"all\"")

View File

@@ -11,6 +11,14 @@ When you need extra sandboxed permissions for one command, use:
- `network.enabled`: set to `true` to enable network access
- `file_system.read`: list of paths that need read access
- `file_system.write`: list of paths that need write access
- `macos.preferences`: `read_only` or `read_write`
- `macos.automations`: list of bundle IDs that need Apple Events access
- `macos.mach_services`: list of exact global Mach service names that need `mach-lookup` (for example, `com.vendor.helper`)
- `macos.launch_services`: set to `true` to allow Launch Services access
- `macos.accessibility`: set to `true` to allow accessibility APIs
- `macos.calendar`: set to `true` to allow Calendar access
- `macos.reminders`: set to `true` to allow Reminders access
- `macos.contacts`: `read_only` or `read_write`
When using the `request_permissions` tool directly, only request `network` and `file_system` permissions.

View File

@@ -800,6 +800,12 @@ pub(crate) fn format_additional_permissions_rule(
}
MacOsAutomationPermission::None => {}
}
if !macos.macos_mach_services.is_empty() {
parts.push(format!(
"macOS mach services {}",
macos.macos_mach_services.join(", ")
));
}
if macos.macos_accessibility {
parts.push("macOS accessibility".to_string());
}
@@ -1423,6 +1429,7 @@ mod tests {
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_mach_services: vec!["com.vendor.helper".to_string()],
macos_launch_services: false,
macos_accessibility: true,
macos_calendar: true,

View File

@@ -7,8 +7,8 @@ expression: "render_overlay_lines(&view, 120)"
Reason: need macOS automation
Permission rule: macOS preferences readwrite; macOS automation com.apple.Calendar, com.apple.Notes; macOS
accessibility; macOS calendar; macOS reminders
Permission rule: macOS preferences readwrite; macOS automation com.apple.Calendar, com.apple.Notes; macOS mach
services com.vendor.helper; macOS accessibility; macOS calendar; macOS reminders
$ osascript -e 'tell application'