Files
codex/codex-rs/core/src/sandboxing/macos_permissions.rs
T
Celia Chen aaefee04cd core/protocol: add structured macOS additional permissions and merge them into sandbox execution (#13499)
## Summary
- Introduce strongly-typed macOS additional permissions across
protocol/core/app-server boundaries.
- Merge additional permissions into effective sandbox execution,
including macOS seatbelt profile extensions.
- Expand docs, schema/tool definitions, UI rendering, and tests for
`network`, `file_system`, and `macos` additional permissions.
2026-03-05 16:21:45 -08:00

145 lines
5.2 KiB
Rust

use std::collections::BTreeSet;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
/// Merges macOS seatbelt profile extensions by taking the permissive union of
/// each permission field.
pub(crate) fn merge_macos_seatbelt_profile_extensions(
base: Option<&MacOsSeatbeltProfileExtensions>,
permissions: Option<&MacOsSeatbeltProfileExtensions>,
) -> Option<MacOsSeatbeltProfileExtensions> {
let Some(permissions) = permissions else {
return base.cloned();
};
match base {
Some(base) => Some(MacOsSeatbeltProfileExtensions {
macos_preferences: union_macos_preferences_permission(
&base.macos_preferences,
&permissions.macos_preferences,
),
macos_automation: union_macos_automation_permission(
&base.macos_automation,
&permissions.macos_automation,
),
macos_accessibility: base.macos_accessibility || permissions.macos_accessibility,
macos_calendar: base.macos_calendar || permissions.macos_calendar,
}),
None => Some(permissions.clone()),
}
}
/// Unions two preferences permissions by keeping the more permissive one.
///
/// The larger rank wins: `None < ReadOnly < ReadWrite`. When both sides have
/// the same rank, this keeps `base`.
fn union_macos_preferences_permission(
base: &MacOsPreferencesPermission,
requested: &MacOsPreferencesPermission,
) -> MacOsPreferencesPermission {
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
/// ID allowlists are unioned together.
fn union_macos_automation_permission(
base: &MacOsAutomationPermission,
requested: &MacOsAutomationPermission,
) -> MacOsAutomationPermission {
match (base, requested) {
(MacOsAutomationPermission::All, _) | (_, MacOsAutomationPermission::All) => {
MacOsAutomationPermission::All
}
(MacOsAutomationPermission::None, _) => requested.clone(),
(_, MacOsAutomationPermission::None) => base.clone(),
(
MacOsAutomationPermission::BundleIds(base_bundle_ids),
MacOsAutomationPermission::BundleIds(requested_bundle_ids),
) => MacOsAutomationPermission::BundleIds(
base_bundle_ids
.iter()
.chain(requested_bundle_ids.iter())
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
),
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::merge_macos_seatbelt_profile_extensions;
use super::union_macos_automation_permission;
use super::union_macos_preferences_permission;
use codex_protocol::models::MacOsAutomationPermission;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use pretty_assertions::assert_eq;
#[test]
fn merge_extensions_widens_permissions() {
let base = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadOnly,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
]),
macos_accessibility: false,
macos_calendar: false,
};
let requested = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
"com.apple.Calendar".to_string(),
]),
macos_accessibility: true,
macos_calendar: true,
};
let merged =
merge_macos_seatbelt_profile_extensions(Some(&base), Some(&requested)).expect("merge");
assert_eq!(
merged,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Calendar".to_string(),
"com.apple.Notes".to_string(),
]),
macos_accessibility: true,
macos_calendar: true,
}
);
}
#[test]
fn union_macos_preferences_permission_does_not_downgrade() {
let base = MacOsPreferencesPermission::ReadWrite;
let requested = MacOsPreferencesPermission::ReadOnly;
let merged = union_macos_preferences_permission(&base, &requested);
assert_eq!(merged, MacOsPreferencesPermission::ReadWrite);
}
#[test]
fn union_macos_automation_permission_all_is_dominant() {
let base = MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]);
let requested = MacOsAutomationPermission::All;
let merged = union_macos_automation_permission(&base, &requested);
assert_eq!(merged, MacOsAutomationPermission::All);
}
}