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
@@ -89,7 +89,7 @@ fn resolve_workdir_base_path(
|
||||
}
|
||||
|
||||
/// Validates feature/policy constraints for `with_additional_permissions` and
|
||||
/// returns normalized absolute paths. Errors if paths are invalid.
|
||||
/// normalizes any path-based permissions. Errors if the request is invalid.
|
||||
pub(super) fn normalize_and_validate_additional_permissions(
|
||||
request_permission_enabled: bool,
|
||||
approval_policy: AskForApproval,
|
||||
@@ -119,14 +119,18 @@ pub(super) fn normalize_and_validate_additional_permissions(
|
||||
}
|
||||
let Some(additional_permissions) = additional_permissions else {
|
||||
return Err(
|
||||
"missing `additional_permissions`; provide `file_system.read` and/or `file_system.write` when using `with_additional_permissions`"
|
||||
"missing `additional_permissions`; provide at least one of `network`, `file_system`, or `macos` when using `with_additional_permissions`"
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if additional_permissions.macos.is_some() {
|
||||
return Err("`additional_permissions.macos` is only supported on macOS".to_string());
|
||||
}
|
||||
let normalized = normalize_additional_permissions(additional_permissions)?;
|
||||
if normalized.is_empty() {
|
||||
return Err(
|
||||
"`additional_permissions` must include at least one path in `file_system.read` or `file_system.write`"
|
||||
"`additional_permissions` must include at least one requested permission in `network`, `file_system`, or `macos`"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -839,6 +839,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
permission_profile,
|
||||
)) => {
|
||||
// Merge additive permissions into the existing turn/request sandbox policy.
|
||||
// On macOS, additional profile extensions are unioned with the turn defaults.
|
||||
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
|
||||
command,
|
||||
workdir,
|
||||
@@ -846,7 +847,9 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
sandbox_policy: &self.sandbox_policy,
|
||||
additional_permissions: Some(permission_profile),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
macos_seatbelt_profile_extensions: self
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
})?
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
|
||||
|
||||
@@ -579,3 +579,67 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
||||
prepared.command
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_macos_extensions() {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
|
||||
let executor = CoreShellCommandExecutor {
|
||||
command: vec!["echo".to_string(), "ok".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
justification: None,
|
||||
arg0: None,
|
||||
sandbox_policy_cwd: cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadOnly,
|
||||
..Default::default()
|
||||
}),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_linux_sandbox_bwrap: false,
|
||||
};
|
||||
|
||||
let prepared = executor
|
||||
.prepare_escalated_exec(
|
||||
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
|
||||
&["echo".to_string(), "ok".to_string()],
|
||||
&cwd,
|
||||
HashMap::new(),
|
||||
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(
|
||||
PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_calendar: true,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let policy = prepared
|
||||
.command
|
||||
.get(2)
|
||||
.expect("seatbelt policy should be present");
|
||||
assert_eq!(
|
||||
prepared.command.first().map(String::as_str),
|
||||
Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
);
|
||||
assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p"));
|
||||
assert!(
|
||||
policy.contains("(allow user-preference-read)"),
|
||||
"expected turn macOS seatbelt extensions to be preserved: {:?}",
|
||||
prepared.command
|
||||
);
|
||||
assert!(
|
||||
policy.contains("(allow mach-lookup (global-name \"com.apple.CalendarAgent\"))"),
|
||||
"expected requested macOS seatbelt extensions to be included: {:?}",
|
||||
prepared.command
|
||||
);
|
||||
}
|
||||
|
||||
+103
-27
@@ -253,7 +253,7 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap<Stri
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
if request_permission_enabled {
|
||||
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem access (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
"Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem, network, or macOS permissions (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
} else {
|
||||
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
||||
}
|
||||
@@ -291,36 +291,100 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap<Stri
|
||||
properties.insert(
|
||||
"additional_permissions".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"file_system".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"read".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"network".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"enabled".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant read access for this command."
|
||||
"Set to true to enable network access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"write".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant write access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
)]),
|
||||
required: Some(vec!["file_system".to_string()]),
|
||||
)]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"file_system".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"read".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant read access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"write".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Additional filesystem paths to grant write access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"macos".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"preferences".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Additional macOS preferences access for this command. Supported values: \"readonly\" or \"readwrite\"."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"automations".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Additional macOS automation targets for this command as bundle IDs, or use true in clients that support boolean union payloads."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"accessibility".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Set to true to allow macOS accessibility APIs for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"calendar".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Set to true to allow macOS Calendar access for this command."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: None,
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
);
|
||||
@@ -3366,6 +3430,18 @@ Examples of valid command strings:
|
||||
panic!("expected sandbox_permissions description");
|
||||
};
|
||||
assert!(description.contains("with_additional_permissions"));
|
||||
assert!(description.contains("macOS permissions"));
|
||||
|
||||
let Some(JsonSchema::Object {
|
||||
properties: additional_properties,
|
||||
..
|
||||
}) = properties.get("additional_permissions")
|
||||
else {
|
||||
panic!("expected additional_permissions schema");
|
||||
};
|
||||
assert!(additional_properties.contains_key("network"));
|
||||
assert!(additional_properties.contains_key("file_system"));
|
||||
assert!(additional_properties.contains_key("macos"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user