mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
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.
This commit is contained in:
@@ -109,17 +109,32 @@ pub enum MacOsAutomationValue {
|
||||
BundleIds(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[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,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case", try_from = "MacOsAutomationPermissionDe")]
|
||||
pub enum MacOsAutomationPermission {
|
||||
#[default]
|
||||
None,
|
||||
@@ -127,7 +142,52 @@ pub enum MacOsAutomationPermission {
|
||||
BundleIds(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum MacOsAutomationPermissionDe {
|
||||
Mode(String),
|
||||
BundleIds(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"]`
|
||||
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) => {
|
||||
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)]
|
||||
pub struct MacOsSeatbeltProfileExtensions {
|
||||
pub macos_preferences: MacOsPreferencesPermission,
|
||||
pub macos_automation: MacOsAutomationPermission,
|
||||
@@ -139,25 +199,12 @@ pub struct MacOsSeatbeltProfileExtensions {
|
||||
pub struct PermissionProfile {
|
||||
pub network: Option<NetworkPermissions>,
|
||||
pub file_system: Option<FileSystemPermissions>,
|
||||
pub macos: Option<MacOsPermissions>,
|
||||
pub macos: Option<MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
impl PermissionProfile {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.network
|
||||
.as_ref()
|
||||
.map(NetworkPermissions::is_empty)
|
||||
.unwrap_or(true)
|
||||
&& self
|
||||
.file_system
|
||||
.as_ref()
|
||||
.map(FileSystemPermissions::is_empty)
|
||||
.unwrap_or(true)
|
||||
&& self
|
||||
.macos
|
||||
.as_ref()
|
||||
.map(MacOsPermissions::is_empty)
|
||||
.unwrap_or(true)
|
||||
self.network.is_none() && self.file_system.is_none() && self.macos.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,6 +1393,76 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_is_empty_when_all_fields_are_none() {
|
||||
assert_eq!(PermissionProfile::default().is_empty(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_is_not_empty_when_field_is_present_but_nested_empty() {
|
||||
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 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_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_accessibility: true,
|
||||
macos_calendar: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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 convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
|
||||
let contents = vec![serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user