mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
4e77ea0ec7
commit
aaefee04cd
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ sandbox placement and transformation of portable CommandSpec into a
|
||||
ready‑to‑spawn environment.
|
||||
*/
|
||||
|
||||
pub(crate) mod macos_permissions;
|
||||
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::exec::SandboxType;
|
||||
@@ -25,13 +27,13 @@ use crate::tools::sandboxing::SandboxablePreference;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
pub use codex_protocol::models::SandboxPermissions;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize;
|
||||
use macos_permissions::merge_macos_seatbelt_profile_extensions;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
@@ -98,22 +100,54 @@ pub(crate) enum SandboxTransformError {
|
||||
SeatbeltUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct EffectiveSandboxPermissions {
|
||||
pub(crate) sandbox_policy: SandboxPolicy,
|
||||
pub(crate) macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
impl EffectiveSandboxPermissions {
|
||||
pub(crate) fn new(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
macos_seatbelt_profile_extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||
additional_permissions: Option<&PermissionProfile>,
|
||||
) -> Self {
|
||||
let Some(additional_permissions) = additional_permissions else {
|
||||
return Self {
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
macos_seatbelt_profile_extensions: macos_seatbelt_profile_extensions.cloned(),
|
||||
};
|
||||
};
|
||||
|
||||
Self {
|
||||
sandbox_policy: sandbox_policy_with_additional_permissions(
|
||||
sandbox_policy,
|
||||
additional_permissions,
|
||||
),
|
||||
macos_seatbelt_profile_extensions: merge_macos_seatbelt_profile_extensions(
|
||||
macos_seatbelt_profile_extensions,
|
||||
additional_permissions.macos.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_additional_permissions(
|
||||
additional_permissions: PermissionProfile,
|
||||
) -> Result<PermissionProfile, String> {
|
||||
let Some(file_system) = additional_permissions.file_system else {
|
||||
return Ok(PermissionProfile::default());
|
||||
};
|
||||
let read = file_system
|
||||
.read
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.read"));
|
||||
let write = file_system
|
||||
.write
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.write"));
|
||||
Ok(PermissionProfile {
|
||||
network: additional_permissions.network,
|
||||
file_system: Some(FileSystemPermissions { read, write }),
|
||||
..Default::default()
|
||||
file_system: additional_permissions
|
||||
.file_system
|
||||
.map(|file_system| FileSystemPermissions {
|
||||
read: file_system
|
||||
.read
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.read")),
|
||||
write: file_system
|
||||
.write
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.write")),
|
||||
}),
|
||||
macos: additional_permissions.macos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,14 +238,14 @@ fn merge_network_access(
|
||||
fn sandbox_policy_with_additional_permissions(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
additional_permissions: &PermissionProfile,
|
||||
) -> Result<SandboxPolicy, SandboxTransformError> {
|
||||
) -> SandboxPolicy {
|
||||
if additional_permissions.is_empty() {
|
||||
return Ok(sandbox_policy.clone());
|
||||
return sandbox_policy.clone();
|
||||
}
|
||||
|
||||
let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions);
|
||||
|
||||
let policy = match sandbox_policy {
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
sandbox_policy.clone()
|
||||
}
|
||||
@@ -260,9 +294,7 @@ fn sandbox_policy_with_additional_permissions(
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -326,14 +358,18 @@ impl SandboxManager {
|
||||
use_linux_sandbox_bwrap,
|
||||
windows_sandbox_level,
|
||||
} = request;
|
||||
let effective_policy =
|
||||
if let Some(additional_permissions) = spec.additional_permissions.take() {
|
||||
sandbox_policy_with_additional_permissions(policy, &additional_permissions)?
|
||||
} else {
|
||||
policy.clone()
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let macos_seatbelt_profile_extensions = None;
|
||||
let effective_permissions = EffectiveSandboxPermissions::new(
|
||||
policy,
|
||||
macos_seatbelt_profile_extensions,
|
||||
spec.additional_permissions.as_ref(),
|
||||
);
|
||||
let mut env = spec.env;
|
||||
if !effective_policy.has_full_network_access() {
|
||||
if !effective_permissions
|
||||
.sandbox_policy
|
||||
.has_full_network_access()
|
||||
{
|
||||
env.insert(
|
||||
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
|
||||
"1".to_string(),
|
||||
@@ -352,11 +388,13 @@ impl SandboxManager {
|
||||
seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
let mut args = create_seatbelt_command_args_with_extensions(
|
||||
command.clone(),
|
||||
&effective_policy,
|
||||
&effective_permissions.sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
macos_seatbelt_profile_extensions,
|
||||
effective_permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
|
||||
@@ -371,7 +409,7 @@ impl SandboxManager {
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
let mut args = create_linux_sandbox_command_args(
|
||||
command.clone(),
|
||||
&effective_policy,
|
||||
&effective_permissions.sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_linux_sandbox_bwrap,
|
||||
allow_proxy_network,
|
||||
@@ -406,7 +444,7 @@ impl SandboxManager {
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions: spec.sandbox_permissions,
|
||||
sandbox_policy: effective_policy,
|
||||
sandbox_policy: effective_permissions.sandbox_policy,
|
||||
justification: spec.justification,
|
||||
arg0: arg0_override,
|
||||
})
|
||||
@@ -436,6 +474,8 @@ pub async fn execute_exec_request_with_after_spawn(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::EffectiveSandboxPermissions;
|
||||
use super::SandboxManager;
|
||||
use super::normalize_additional_permissions;
|
||||
use super::sandbox_policy_with_additional_permissions;
|
||||
@@ -445,6 +485,12 @@ mod tests {
|
||||
use crate::tools::sandboxing::SandboxablePreference;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -511,6 +557,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn normalize_additional_permissions_preserves_macos_permissions() {
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(
|
||||
permissions.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 read_only_additional_permissions_can_enable_network_without_writes() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
@@ -536,8 +611,7 @@ mod tests {
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("policy");
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
@@ -550,4 +624,59 @@ mod tests {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn effective_permissions_merge_macos_extensions_with_additional_permissions() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let effective_permissions = EffectiveSandboxPermissions::new(
|
||||
&SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![path.clone()],
|
||||
},
|
||||
network_access: false,
|
||||
},
|
||||
Some(&MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadOnly,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Calendar".to_string(),
|
||||
]),
|
||||
macos_accessibility: false,
|
||||
macos_calendar: false,
|
||||
}),
|
||||
Some(&PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
effective_permissions.macos_seatbelt_profile_extensions,
|
||||
Some(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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user