permissions: remove macOS seatbelt extension profiles (#15918)

## Why

`PermissionProfile` should only describe the per-command permissions we
still want to grant dynamically. Keeping
`MacOsSeatbeltProfileExtensions` in that surface forced extra macOS-only
approval, protocol, schema, and TUI branches for a capability we no
longer want to expose.

## What changed

- Removed the macOS-specific permission-profile types from
`codex-protocol`, the app-server v2 API, and the generated
schema/TypeScript artifacts.
- Deleted the core and sandboxing plumbing that threaded
`MacOsSeatbeltProfileExtensions` through execution requests and seatbelt
construction.
- Simplified macOS seatbelt generation so it always includes the fixed
read-only preferences allowlist instead of carrying a configurable
profile extension.
- Removed the macOS additional-permissions UI/docs/test coverage and
deleted the obsolete macOS permission modules.
- Tightened `request_permissions` intersection handling so explicitly
empty requested read lists are preserved only when that field was
actually granted, avoiding zero-grant responses being stored as active
permissions.
This commit is contained in:
Michael Bolin
2026-03-26 17:12:45 -07:00
committed by GitHub
parent 44d28f500f
commit e6e2999209
50 changed files with 148 additions and 2269 deletions

View File

@@ -1,5 +1,4 @@
use crate::mcp::RequestId;
use crate::models::MacOsSeatbeltProfileExtensions;
use crate::models::PermissionProfile;
use crate::parse_command::ParsedCommand;
use crate::permissions::FileSystemSandboxPolicy;
@@ -20,7 +19,6 @@ pub struct Permissions {
pub sandbox_policy: SandboxPolicy,
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
pub network_sandbox_policy: NetworkSandboxPolicy,
pub macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
}
#[allow(clippy::large_enum_variant)]

View File

@@ -88,138 +88,15 @@ impl NetworkPermissions {
}
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Hash,
Serialize,
Deserialize,
JsonSchema,
TS,
)]
#[serde(rename_all = "snake_case")]
pub enum MacOsPreferencesPermission {
None,
// IMPORTANT: ReadOnly needs to be the default because it's the
// security-sensitive default and keeps cf prefs working.
#[default]
ReadOnly,
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 {
#[default]
None,
All,
BundleIds(Vec<String>),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum MacOsAutomationPermissionDe {
Mode(String),
BundleIds(Vec<String>),
BundleIdsObject { bundle_ids: Vec<String> },
}
impl TryFrom<MacOsAutomationPermissionDe> for MacOsAutomationPermission {
type Error = String;
/// Accepts one of:
/// - `"none"` or `"all"`
/// - a plain list of bundle IDs, e.g. `["com.apple.Notes"]`
/// - an object with bundle IDs, e.g. `{"bundle_ids": ["com.apple.Notes"]}`
fn try_from(value: MacOsAutomationPermissionDe) -> Result<Self, Self::Error> {
let permission = match value {
MacOsAutomationPermissionDe::Mode(value) => {
let normalized = value.trim().to_ascii_lowercase();
if normalized == "all" {
MacOsAutomationPermission::All
} else if normalized == "none" {
MacOsAutomationPermission::None
} else {
return Err(format!(
"invalid macOS automation permission: {value}; expected none, all, or bundle ids"
));
}
}
MacOsAutomationPermissionDe::BundleIds(bundle_ids)
| MacOsAutomationPermissionDe::BundleIdsObject { bundle_ids } => {
let bundle_ids = bundle_ids
.into_iter()
.map(|bundle_id| bundle_id.trim().to_string())
.filter(|bundle_id| !bundle_id.is_empty())
.collect::<Vec<String>>();
if bundle_ids.is_empty() {
MacOsAutomationPermission::None
} else {
MacOsAutomationPermission::BundleIds(bundle_ids)
}
}
};
Ok(permission)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, Serialize, Deserialize, JsonSchema, TS)]
#[serde(default)]
pub struct MacOsSeatbeltProfileExtensions {
#[serde(alias = "preferences")]
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)]
pub struct PermissionProfile {
pub network: Option<NetworkPermissions>,
pub file_system: Option<FileSystemPermissions>,
pub macos: Option<MacOsSeatbeltProfileExtensions>,
}
impl PermissionProfile {
pub fn is_empty(&self) -> bool {
self.network.is_none() && self.file_system.is_none() && self.macos.is_none()
self.network.is_none() && self.file_system.is_none()
}
}
@@ -726,7 +603,7 @@ fn granular_prompt_intro_text() -> &'static str {
}
fn request_permissions_tool_prompt_section() -> &'static str {
"# request_permissions Tool\n\nThe built-in `request_permissions` tool is available in this session. Invoke it when you need to request additional `network`, `file_system`, or `macos` permissions before later shell-like commands need them. Request only the specific permissions required for the task."
"# request_permissions Tool\n\nThe built-in `request_permissions` tool is available in this session. Invoke it when you need to request additional `network` or `file_system` permissions before later shell-like commands need them. Request only the specific permissions required for the task."
}
fn granular_instructions(
@@ -1647,170 +1524,10 @@ mod tests {
let permission_profile = PermissionProfile {
network: Some(NetworkPermissions { enabled: None }),
file_system: None,
macos: None,
};
assert_eq!(permission_profile.is_empty(), false);
}
#[test]
fn macos_preferences_permission_deserializes_read_write() {
let permission = serde_json::from_str::<MacOsPreferencesPermission>("\"read_write\"")
.expect("deserialize macos preferences permission");
assert_eq!(permission, MacOsPreferencesPermission::ReadWrite);
}
#[test]
fn macos_preferences_permission_order_matches_permissiveness() {
assert!(MacOsPreferencesPermission::None < MacOsPreferencesPermission::ReadOnly);
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!({
"network": null,
"file_system": null,
"macos": {
"macos_preferences": "read_write",
"macos_automation": ["com.apple.Notes"],
"macos_launch_services": true,
"macos_accessibility": true,
"macos_calendar": true
}
}))
.expect("deserialize permission profile");
assert_eq!(
permission_profile,
PermissionProfile {
network: None,
file_system: None,
macos: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
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,
}),
}
);
}
#[test]
fn macos_seatbelt_profile_extensions_deserializes_missing_fields_to_defaults() {
let permissions =
serde_json::from_value::<MacOsSeatbeltProfileExtensions>(serde_json::json!({
"macos_automation": ["com.apple.Notes"]
}))
.expect("deserialize macos permissions");
assert_eq!(
permissions,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
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,
}
);
}
#[test]
fn macos_seatbelt_profile_extensions_deserializes_tool_schema_aliases() {
let permissions =
serde_json::from_value::<MacOsSeatbeltProfileExtensions>(serde_json::json!({
"preferences": "read_write",
"automations": ["com.apple.Notes"],
"launch_services": true,
"accessibility": true,
"calendar": true,
"reminders": true,
"contacts": "read_only"
}))
.expect("deserialize macos permissions");
assert_eq!(
permissions,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
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,
}
);
}
#[test]
fn macos_automation_permission_deserializes_all_and_none() {
let all = serde_json::from_str::<MacOsAutomationPermission>("\"all\"")
.expect("deserialize all automation permission");
let none = serde_json::from_str::<MacOsAutomationPermission>("\"none\"")
.expect("deserialize none automation permission");
assert_eq!(all, MacOsAutomationPermission::All);
assert_eq!(none, MacOsAutomationPermission::None);
}
#[test]
fn macos_automation_permission_deserializes_bundle_ids_object() {
let permission = serde_json::from_value::<MacOsAutomationPermission>(serde_json::json!({
"bundle_ids": ["com.apple.Notes"]
}))
.expect("deserialize bundle_ids object automation permission");
assert_eq!(
permission,
MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string(),])
);
}
#[test]
fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
let contents = vec![serde_json::json!({

View File

@@ -32,7 +32,6 @@ impl From<RequestPermissionProfile> for PermissionProfile {
Self {
network: value.network,
file_system: value.file_system,
macos: None,
}
}
}