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
parent 8ac27b2a16
commit 889b4796fc
25 changed files with 779 additions and 25 deletions

View file

@ -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",

View file

@ -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"

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"

View file

@ -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"

View file

@ -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";

View file

@ -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, };

View file

@ -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";

View file

@ -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, };

View file

@ -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, };

View file

@ -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<AdditionalFileSystemPermissions> 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<CoreMacOsSeatbeltProfileExtensions> for AdditionalMacOsPermissions {
@ -982,8 +986,11 @@ impl From<CoreMacOsSeatbeltProfileExtensions> 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<AdditionalMacOsPermissions> 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<CoreMacOsAutomationPermission>,
#[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)]
pub accessibility: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub calendar: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub reminders: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub contacts: Option<CoreMacOsContactsPermission>,
}
impl From<GrantedMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
@ -1078,8 +1097,11 @@ impl From<GrantedMacOsPermissions> 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<GrantedPermissionProfile> 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::<CommandExecutionRequestApprovalParams>(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,
}),
),
];

View file

@ -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()
},

View file

@ -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

View file

@ -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"))

View file

@ -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);
}
}

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,
})
);
}

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];

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());

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()
})

View file

@ -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()

View file

@ -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::<PermissionProfile>(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::<PermissionProfile>(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::<MacOsSeatbeltProfileExtensions>(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,
}
);
}

View file

@ -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()
}),

View file

@ -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'