From 889b4796fca2f292cd57438da90057234f1e6ea7 Mon Sep 17 00:00:00 2001 From: Leo Shimonaka Date: Tue, 10 Mar 2026 16:34:47 -0700 Subject: [PATCH] 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 --- ...CommandExecutionRequestApprovalParams.json | 22 ++- .../schema/json/EventMsg.json | 24 +++ .../PermissionsRequestApprovalParams.json | 22 ++- .../PermissionsRequestApprovalResponse.json | 30 ++++ .../schema/json/ServerRequest.json | 22 ++- .../codex_app_server_protocol.schemas.json | 60 ++++++- .../codex_app_server_protocol.v2.schemas.json | 24 +++ .../typescript/MacOsContactsPermission.ts | 5 + .../MacOsSeatbeltProfileExtensions.ts | 3 +- .../schema/typescript/index.ts | 1 + .../v2/AdditionalMacOsPermissions.ts | 3 +- .../typescript/v2/GrantedMacOsPermissions.ts | 3 +- .../app-server-protocol/src/protocol/v2.rs | 146 +++++++++++++++++- .../app-server/src/bespoke_event_handling.rs | 67 ++++++++ codex-rs/core/README.md | 6 + ...stricted_read_only_platform_defaults.sbpl} | 20 ++- .../core/src/sandboxing/macos_permissions.rs | 46 ++++++ codex-rs/core/src/sandboxing/mod.rs | 20 +++ codex-rs/core/src/seatbelt.rs | 9 +- codex-rs/core/src/seatbelt_permissions.rs | 136 +++++++++++++++- codex-rs/core/src/skills/loader.rs | 41 +++++ .../runtimes/shell/unix_escalation_tests.rs | 1 + codex-rs/protocol/src/models.rs | 76 ++++++++- .../tui/src/bottom_pane/approval_overlay.rs | 15 ++ ...y_additional_permissions_macos_prompt.snap | 2 +- 25 files changed, 779 insertions(+), 25 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/MacOsContactsPermission.ts rename codex-rs/core/src/{seatbelt_platform_defaults.sbpl => restricted_read_only_platform_defaults.sbpl} (89%) diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index befa086b3..2c146b952 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -39,15 +39,27 @@ "calendar": { "type": "boolean" }, + "contacts": { + "$ref": "#/definitions/MacOsContactsPermission" + }, + "launchServices": { + "type": "boolean" + }, "preferences": { "$ref": "#/definitions/MacOsPreferencesPermission" + }, + "reminders": { + "type": "boolean" } }, "required": [ "accessibility", "automations", "calendar", - "preferences" + "contacts", + "launchServices", + "preferences", + "reminders" ], "type": "object" }, @@ -324,6 +336,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 845c5eb48..6db4cb10c 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -4044,6 +4044,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", @@ -4070,6 +4078,18 @@ "default": false, "type": "boolean" }, + "macos_contacts": { + "allOf": [ + { + "$ref": "#/definitions/MacOsContactsPermission" + } + ], + "default": "none" + }, + "macos_launch_services": { + "default": false, + "type": "boolean" + }, "macos_preferences": { "allOf": [ { @@ -4077,6 +4097,10 @@ } ], "default": "read_only" + }, + "macos_reminders": { + "default": false, + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index f642c81cf..0d5c09193 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -39,15 +39,27 @@ "calendar": { "type": "boolean" }, + "contacts": { + "$ref": "#/definitions/MacOsContactsPermission" + }, + "launchServices": { + "type": "boolean" + }, "preferences": { "$ref": "#/definitions/MacOsPreferencesPermission" + }, + "reminders": { + "type": "boolean" } }, "required": [ "accessibility", "automations", "calendar", - "preferences" + "contacts", + "launchServices", + "preferences", + "reminders" ], "type": "object" }, @@ -124,6 +136,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json index 2637ed5dd..df9e519dc 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json @@ -63,6 +63,22 @@ "null" ] }, + "contacts": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsContactsPermission" + }, + { + "type": "null" + } + ] + }, + "launchServices": { + "type": [ + "boolean", + "null" + ] + }, "preferences": { "anyOf": [ { @@ -72,6 +88,12 @@ "type": "null" } ] + }, + "reminders": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -138,6 +160,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 310b50171..a00871971 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -39,15 +39,27 @@ "calendar": { "type": "boolean" }, + "contacts": { + "$ref": "#/definitions/MacOsContactsPermission" + }, + "launchServices": { + "type": "boolean" + }, "preferences": { "$ref": "#/definitions/MacOsPreferencesPermission" + }, + "reminders": { + "type": "boolean" } }, "required": [ "accessibility", "automations", "calendar", - "preferences" + "contacts", + "launchServices", + "preferences", + "reminders" ], "type": "object" }, @@ -653,6 +665,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index c902a8dfc..8aff1b2b1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -35,15 +35,27 @@ "calendar": { "type": "boolean" }, + "contacts": { + "$ref": "#/definitions/MacOsContactsPermission" + }, + "launchServices": { + "type": "boolean" + }, "preferences": { "$ref": "#/definitions/MacOsPreferencesPermission" + }, + "reminders": { + "type": "boolean" } }, "required": [ "accessibility", "automations", "calendar", - "preferences" + "contacts", + "launchServices", + "preferences", + "reminders" ], "type": "object" }, @@ -5303,6 +5315,22 @@ "null" ] }, + "contacts": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsContactsPermission" + }, + { + "type": "null" + } + ] + }, + "launchServices": { + "type": [ + "boolean", + "null" + ] + }, "preferences": { "anyOf": [ { @@ -5312,6 +5340,12 @@ "type": "null" } ] + }, + "reminders": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -5573,6 +5607,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", @@ -5599,6 +5641,18 @@ "default": false, "type": "boolean" }, + "macos_contacts": { + "allOf": [ + { + "$ref": "#/definitions/MacOsContactsPermission" + } + ], + "default": "none" + }, + "macos_launch_services": { + "default": false, + "type": "boolean" + }, "macos_preferences": { "allOf": [ { @@ -5606,6 +5660,10 @@ } ], "default": "read_only" + }, + "macos_reminders": { + "default": false, + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index fe43f29d9..ba738b426 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -8070,6 +8070,14 @@ } ] }, + "MacOsContactsPermission": { + "enum": [ + "none", + "read_only", + "read_write" + ], + "type": "string" + }, "MacOsPreferencesPermission": { "enum": [ "none", @@ -8096,6 +8104,18 @@ "default": false, "type": "boolean" }, + "macos_contacts": { + "allOf": [ + { + "$ref": "#/definitions/MacOsContactsPermission" + } + ], + "default": "none" + }, + "macos_launch_services": { + "default": false, + "type": "boolean" + }, "macos_preferences": { "allOf": [ { @@ -8103,6 +8123,10 @@ } ], "default": "read_only" + }, + "macos_reminders": { + "default": false, + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/typescript/MacOsContactsPermission.ts b/codex-rs/app-server-protocol/schema/typescript/MacOsContactsPermission.ts new file mode 100644 index 000000000..dd6d7b59e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/MacOsContactsPermission.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MacOsContactsPermission = "none" | "read_only" | "read_write"; diff --git a/codex-rs/app-server-protocol/schema/typescript/MacOsSeatbeltProfileExtensions.ts b/codex-rs/app-server-protocol/schema/typescript/MacOsSeatbeltProfileExtensions.ts index 91d83df60..4fa47f144 100644 --- a/codex-rs/app-server-protocol/schema/typescript/MacOsSeatbeltProfileExtensions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/MacOsSeatbeltProfileExtensions.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MacOsAutomationPermission } from "./MacOsAutomationPermission"; +import type { MacOsContactsPermission } from "./MacOsContactsPermission"; import type { MacOsPreferencesPermission } from "./MacOsPreferencesPermission"; -export type MacOsSeatbeltProfileExtensions = { macos_preferences: MacOsPreferencesPermission, macos_automation: MacOsAutomationPermission, macos_accessibility: boolean, macos_calendar: boolean, }; +export type MacOsSeatbeltProfileExtensions = { macos_preferences: MacOsPreferencesPermission, macos_automation: MacOsAutomationPermission, macos_launch_services: boolean, macos_accessibility: boolean, macos_calendar: boolean, macos_reminders: boolean, macos_contacts: MacOsContactsPermission, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index a7b38b044..a1209c75b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -112,6 +112,7 @@ export type { LocalShellAction } from "./LocalShellAction"; export type { LocalShellExecAction } from "./LocalShellExecAction"; export type { LocalShellStatus } from "./LocalShellStatus"; export type { MacOsAutomationPermission } from "./MacOsAutomationPermission"; +export type { MacOsContactsPermission } from "./MacOsContactsPermission"; export type { MacOsPreferencesPermission } from "./MacOsPreferencesPermission"; export type { MacOsSeatbeltProfileExtensions } from "./MacOsSeatbeltProfileExtensions"; export type { McpAuthStatus } from "./McpAuthStatus"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts index 4030294f3..177661bb0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MacOsAutomationPermission } from "../MacOsAutomationPermission"; +import type { MacOsContactsPermission } from "../MacOsContactsPermission"; import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission"; -export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, accessibility: boolean, calendar: boolean, }; +export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, launchServices: boolean, accessibility: boolean, calendar: boolean, reminders: boolean, contacts: MacOsContactsPermission, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts index b95a2940f..edf779488 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts @@ -2,6 +2,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MacOsAutomationPermission } from "../MacOsAutomationPermission"; +import type { MacOsContactsPermission } from "../MacOsContactsPermission"; import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission"; -export type GrantedMacOsPermissions = { preferences?: MacOsPreferencesPermission, automations?: MacOsAutomationPermission, accessibility?: boolean, calendar?: boolean, }; +export type GrantedMacOsPermissions = { preferences?: MacOsPreferencesPermission, automations?: MacOsAutomationPermission, launchServices?: boolean, accessibility?: boolean, calendar?: boolean, reminders?: boolean, contacts?: MacOsContactsPermission, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 5c77b71d5..8155fe1c0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -31,6 +31,7 @@ use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission; +use codex_protocol::models::MacOsContactsPermission as CoreMacOsContactsPermission; use codex_protocol::models::MacOsPreferencesPermission as CoreMacOsPreferencesPermission; use codex_protocol::models::MacOsSeatbeltProfileExtensions as CoreMacOsSeatbeltProfileExtensions; use codex_protocol::models::MessagePhase; @@ -973,8 +974,11 @@ impl From for CoreFileSystemPermissions { pub struct AdditionalMacOsPermissions { pub preferences: CoreMacOsPreferencesPermission, pub automations: CoreMacOsAutomationPermission, + pub launch_services: bool, pub accessibility: bool, pub calendar: bool, + pub reminders: bool, + pub contacts: CoreMacOsContactsPermission, } impl From for AdditionalMacOsPermissions { @@ -982,8 +986,11 @@ impl From for AdditionalMacOsPermissions { Self { preferences: value.macos_preferences, automations: value.macos_automation, + launch_services: value.macos_launch_services, accessibility: value.macos_accessibility, calendar: value.macos_calendar, + reminders: value.macos_reminders, + contacts: value.macos_contacts, } } } @@ -993,8 +1000,11 @@ impl From for CoreMacOsSeatbeltProfileExtensions { Self { macos_preferences: value.preferences, macos_automation: value.automations, + macos_launch_services: value.launch_services, macos_accessibility: value.accessibility, macos_calendar: value.calendar, + macos_reminders: value.reminders, + macos_contacts: value.contacts, } } } @@ -1063,10 +1073,19 @@ pub struct GrantedMacOsPermissions { pub automations: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub launch_services: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub accessibility: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub calendar: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub reminders: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub contacts: Option, } impl From for CoreMacOsSeatbeltProfileExtensions { @@ -1078,8 +1097,11 @@ impl From for CoreMacOsSeatbeltProfileExtensions { macos_automation: value .automations .unwrap_or(CoreMacOsAutomationPermission::None), + macos_launch_services: value.launch_services.unwrap_or(false), macos_accessibility: value.accessibility.unwrap_or(false), macos_calendar: value.calendar.unwrap_or(false), + macos_reminders: value.reminders.unwrap_or(false), + macos_contacts: value.contacts.unwrap_or(CoreMacOsContactsPermission::None), } } } @@ -1104,8 +1126,11 @@ impl From for CorePermissionProfile { let macos = value.macos.and_then(|macos| { if macos.preferences.is_none() && macos.automations.is_none() + && macos.launch_services.is_none() && macos.accessibility.is_none() && macos.calendar.is_none() + && macos.reminders.is_none() + && macos.contacts.is_none() { None } else { @@ -5494,8 +5519,11 @@ mod tests { "automations": { "bundle_ids": ["com.apple.Notes"] }, + "launchServices": false, "accessibility": false, - "calendar": false + "calendar": false, + "reminders": false, + "contacts": "read_only" } }, "skillMetadata": null, @@ -5509,10 +5537,52 @@ mod tests { params .additional_permissions .and_then(|permissions| permissions.macos) - .map(|macos| macos.automations), - Some(CoreMacOsAutomationPermission::BundleIds(vec![ - "com.apple.Notes".to_string(), - ])) + .map(|macos| (macos.automations, macos.launch_services, macos.contacts)), + Some(( + CoreMacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string(),]), + false, + CoreMacOsContactsPermission::ReadOnly, + )) + ); + } + + #[test] + fn command_execution_request_approval_accepts_macos_reminders_permission() { + let params = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "command": "cat file", + "cwd": "/tmp", + "commandActions": null, + "reason": null, + "networkApprovalContext": null, + "additionalPermissions": { + "network": null, + "fileSystem": null, + "macos": { + "preferences": "read_only", + "automations": "none", + "launchServices": false, + "accessibility": false, + "calendar": false, + "reminders": true, + "contacts": "none" + } + }, + "skillMetadata": null, + "proposedExecpolicyAmendment": null, + "proposedNetworkPolicyAmendments": null, + "availableDecisions": null + })) + .expect("reminders permission should deserialize"); + + assert_eq!( + params + .additional_permissions + .and_then(|permissions| permissions.macos) + .map(|macos| macos.reminders), + Some(true) ); } @@ -5560,8 +5630,11 @@ mod tests { Some(CoreMacOsSeatbeltProfileExtensions { macos_preferences: CoreMacOsPreferencesPermission::ReadOnly, macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: false, macos_calendar: false, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::None, }), ), ( @@ -5581,8 +5654,29 @@ mod tests { macos_automation: CoreMacOsAutomationPermission::BundleIds(vec![ "com.apple.Notes".to_string(), ]), + macos_launch_services: false, macos_accessibility: false, macos_calendar: false, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::None, + }), + ), + ( + json!({ + "launchServices": true, + }), + Some(GrantedMacOsPermissions { + launch_services: Some(true), + ..Default::default() + }), + Some(CoreMacOsSeatbeltProfileExtensions { + macos_preferences: CoreMacOsPreferencesPermission::None, + macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: true, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::None, }), ), ( @@ -5596,8 +5690,11 @@ mod tests { Some(CoreMacOsSeatbeltProfileExtensions { macos_preferences: CoreMacOsPreferencesPermission::None, macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: true, macos_calendar: false, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::None, }), ), ( @@ -5611,8 +5708,47 @@ mod tests { Some(CoreMacOsSeatbeltProfileExtensions { macos_preferences: CoreMacOsPreferencesPermission::None, macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: false, macos_calendar: true, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::None, + }), + ), + ( + json!({ + "reminders": true, + }), + Some(GrantedMacOsPermissions { + reminders: Some(true), + ..Default::default() + }), + Some(CoreMacOsSeatbeltProfileExtensions { + macos_preferences: CoreMacOsPreferencesPermission::None, + macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: false, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: true, + macos_contacts: CoreMacOsContactsPermission::None, + }), + ), + ( + json!({ + "contacts": "read_only", + }), + Some(GrantedMacOsPermissions { + contacts: Some(CoreMacOsContactsPermission::ReadOnly), + ..Default::default() + }), + Some(CoreMacOsSeatbeltProfileExtensions { + macos_preferences: CoreMacOsPreferencesPermission::None, + macos_automation: CoreMacOsAutomationPermission::None, + macos_launch_services: false, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: false, + macos_contacts: CoreMacOsContactsPermission::ReadOnly, }), ), ]; diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 85ca56c91..620f397af 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2627,6 +2627,7 @@ mod tests { use codex_app_server_protocol::TurnPlanStepStatus; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::MacOsAutomationPermission; + use codex_protocol::models::MacOsContactsPermission; use codex_protocol::models::MacOsPreferencesPermission; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::plan_tool::PlanItemArg; @@ -2716,8 +2717,11 @@ mod tests { "com.apple.Notes".to_string(), "com.apple.Reminders".to_string(), ]), + macos_launch_services: true, macos_accessibility: true, macos_calendar: true, + macos_reminders: true, + macos_contacts: MacOsContactsPermission::ReadWrite, }), ..Default::default() }; @@ -2731,8 +2735,11 @@ mod tests { macos: Some(MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::ReadOnly, macos_automation: MacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: false, macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, }), ..Default::default() }, @@ -2749,8 +2756,28 @@ mod tests { macos_automation: MacOsAutomationPermission::BundleIds(vec![ "com.apple.Notes".to_string(), ]), + macos_launch_services: false, macos_accessibility: false, macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, + }), + ..Default::default() + }, + ), + ( + serde_json::json!({ + "launchServices": true, + }), + CorePermissionProfile { + macos: Some(MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::None, + macos_automation: MacOsAutomationPermission::None, + macos_launch_services: true, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, }), ..Default::default() }, @@ -2763,8 +2790,11 @@ mod tests { macos: Some(MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::None, macos_automation: MacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: true, macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, }), ..Default::default() }, @@ -2777,8 +2807,45 @@ mod tests { macos: Some(MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::None, macos_automation: MacOsAutomationPermission::None, + macos_launch_services: false, macos_accessibility: false, macos_calendar: true, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, + }), + ..Default::default() + }, + ), + ( + serde_json::json!({ + "reminders": true, + }), + CorePermissionProfile { + macos: Some(MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::None, + macos_automation: MacOsAutomationPermission::None, + macos_launch_services: false, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: true, + macos_contacts: MacOsContactsPermission::None, + }), + ..Default::default() + }, + ), + ( + serde_json::json!({ + "contacts": "read_only", + }), + CorePermissionProfile { + macos: Some(MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::None, + macos_automation: MacOsAutomationPermission::None, + macos_launch_services: false, + macos_accessibility: false, + macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::ReadOnly, }), ..Default::default() }, diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 8a66b47b4..09aadcfe9 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -33,10 +33,16 @@ Seatbelt also supports macOS permission-profile extensions layered on top of enables broad Apple Events send permissions. - `macos_automation = ["com.apple.Notes", ...]`: enables Apple Events send only to listed bundle IDs. +- `macos_launch_services = true`: + enables LaunchServices lookups and open/launch operations. - `macos_accessibility = true`: enables `com.apple.axserver` mach lookup. - `macos_calendar = true`: enables `com.apple.CalendarAgent` mach lookup. +- `macos_contacts = "read_only"`: + enables Address Book read access and Contacts read services. +- `macos_contacts = "read_write"`: + includes the readonly Contacts clauses plus Address Book writes and keychain/temp helpers required for writes. ### Linux diff --git a/codex-rs/core/src/seatbelt_platform_defaults.sbpl b/codex-rs/core/src/restricted_read_only_platform_defaults.sbpl similarity index 89% rename from codex-rs/core/src/seatbelt_platform_defaults.sbpl rename to codex-rs/core/src/restricted_read_only_platform_defaults.sbpl index ec2c59aca..0e3a7bb2f 100644 --- a/codex-rs/core/src/seatbelt_platform_defaults.sbpl +++ b/codex-rs/core/src/restricted_read_only_platform_defaults.sbpl @@ -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")) \ No newline at end of file +(allow file-read* file-write* (extension "com.apple.app-sandbox.read-write")) diff --git a/codex-rs/core/src/sandboxing/macos_permissions.rs b/codex-rs/core/src/sandboxing/macos_permissions.rs index c3b3840d4..5717a558c 100644 --- a/codex-rs/core/src/sandboxing/macos_permissions.rs +++ b/codex-rs/core/src/sandboxing/macos_permissions.rs @@ -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); + } } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 2fb0b45f8..377ecb3db 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -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, }) ); } diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index dede3d055..fa0538e38 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -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]; diff --git a/codex-rs/core/src/seatbelt_permissions.rs b/codex-rs/core/src/seatbelt_permissions.rs index 93bc0965a..219ca332e 100644 --- a/codex-rs/core/src/seatbelt_permissions.rs +++ b/codex-rs/core/src/seatbelt_permissions.rs @@ -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 { + Some(dirs::home_dir()?.join("Library/Application Support/AddressBook")) +} + fn normalize_bundle_ids(bundle_ids: &[String]) -> Vec { 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()); diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index 96b42e3a2..84c73f9e2 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -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::( + 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() }) diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index af71bd5e4..861aa1c02 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -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() diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 28f6f5d60..57b3d9e88 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -110,6 +110,28 @@ pub enum MacOsPreferencesPermission { ReadWrite, } +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Hash, + Serialize, + Deserialize, + JsonSchema, + TS, +)] +#[serde(rename_all = "snake_case")] +pub enum MacOsContactsPermission { + #[default] + None, + ReadOnly, + ReadWrite, +} + #[derive(Debug, Clone, PartialEq, Eq, Default, Hash, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "snake_case", try_from = "MacOsAutomationPermissionDe")] pub enum MacOsAutomationPermission { @@ -174,10 +196,16 @@ pub struct MacOsSeatbeltProfileExtensions { pub macos_preferences: MacOsPreferencesPermission, #[serde(alias = "automations")] pub macos_automation: MacOsAutomationPermission, + #[serde(alias = "launch_services")] + pub macos_launch_services: bool, #[serde(alias = "accessibility")] pub macos_accessibility: bool, #[serde(alias = "calendar")] pub macos_calendar: bool, + #[serde(alias = "reminders")] + pub macos_reminders: bool, + #[serde(alias = "contacts")] + pub macos_contacts: MacOsContactsPermission, } #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] @@ -1456,6 +1484,12 @@ mod tests { assert!(MacOsPreferencesPermission::ReadOnly < MacOsPreferencesPermission::ReadWrite); } + #[test] + fn macos_contacts_permission_order_matches_permissiveness() { + assert!(MacOsContactsPermission::None < MacOsContactsPermission::ReadOnly); + assert!(MacOsContactsPermission::ReadOnly < MacOsContactsPermission::ReadWrite); + } + #[test] fn permission_profile_deserializes_macos_seatbelt_profile_extensions() { let permission_profile = serde_json::from_value::(serde_json::json!({ @@ -1464,6 +1498,7 @@ mod tests { "macos": { "macos_preferences": "read_write", "macos_automation": ["com.apple.Notes"], + "macos_launch_services": true, "macos_accessibility": true, "macos_calendar": true } @@ -1480,8 +1515,38 @@ 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, + }), + } + ); + } + + #[test] + fn permission_profile_deserializes_macos_reminders_permission() { + let permission_profile = serde_json::from_value::(serde_json::json!({ + "macos": { + "macos_reminders": true + } + })) + .expect("deserialize reminders permission profile"); + + assert_eq!( + permission_profile, + PermissionProfile { + network: None, + file_system: None, + 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, }), } ); @@ -1502,8 +1567,11 @@ mod tests { macos_automation: MacOsAutomationPermission::BundleIds(vec![ "com.apple.Notes".to_string(), ]), + macos_launch_services: false, macos_accessibility: false, macos_calendar: false, + macos_reminders: false, + macos_contacts: MacOsContactsPermission::None, } ); } @@ -1514,8 +1582,11 @@ mod tests { serde_json::from_value::(serde_json::json!({ "preferences": "read_write", "automations": ["com.apple.Notes"], + "launch_services": true, "accessibility": true, - "calendar": true + "calendar": true, + "reminders": true, + "contacts": "read_only" })) .expect("deserialize macos permissions"); @@ -1526,8 +1597,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: true, + macos_contacts: MacOsContactsPermission::ReadOnly, } ); } diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index a13252939..2420fb323 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -20,6 +20,7 @@ use codex_core::features::Features; use codex_protocol::ThreadId; use codex_protocol::mcp::RequestId; use codex_protocol::models::MacOsAutomationPermission; +use codex_protocol::models::MacOsContactsPermission; use codex_protocol::models::MacOsPreferencesPermission; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ElicitationAction; @@ -800,6 +801,17 @@ pub(crate) fn format_additional_permissions_rule( if macos.macos_calendar { parts.push("macOS calendar".to_string()); } + if macos.macos_reminders { + parts.push("macOS reminders".to_string()); + } + if !matches!(macos.macos_contacts, MacOsContactsPermission::None) { + let value = match macos.macos_contacts { + MacOsContactsPermission::None => "none", + MacOsContactsPermission::ReadOnly => "readonly", + MacOsContactsPermission::ReadWrite => "readwrite", + }; + parts.push(format!("macOS contacts {value}")); + } } if parts.is_empty() { @@ -1401,8 +1413,11 @@ mod tests { "com.apple.Calendar".to_string(), "com.apple.Notes".to_string(), ]), + macos_launch_services: false, macos_accessibility: true, macos_calendar: true, + macos_reminders: true, + macos_contacts: MacOsContactsPermission::None, }), ..Default::default() }), diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__approval_overlay_additional_permissions_macos_prompt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__approval_overlay_additional_permissions_macos_prompt.snap index 32c0f2a30..d9d8717fe 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__approval_overlay_additional_permissions_macos_prompt.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__approval_overlay_additional_permissions_macos_prompt.snap @@ -8,7 +8,7 @@ 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 + accessibility; macOS calendar; macOS reminders $ osascript -e 'tell application'