mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
44d28f500f
commit
e6e2999209
Generated
-1
@@ -2505,7 +2505,6 @@ dependencies = [
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
|
||||
@@ -28,41 +28,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalMacOsPermissions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
},
|
||||
"calendar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contacts": {
|
||||
"$ref": "#/definitions/MacOsContactsPermission"
|
||||
},
|
||||
"launchServices": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
},
|
||||
"reminders": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessibility",
|
||||
"automations",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"launchServices",
|
||||
"preferences",
|
||||
"reminders"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -86,16 +51,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"macos": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalMacOsPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -298,49 +253,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsContactsPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
|
||||
@@ -28,41 +28,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalMacOsPermissions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
},
|
||||
"calendar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contacts": {
|
||||
"$ref": "#/definitions/MacOsContactsPermission"
|
||||
},
|
||||
"launchServices": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
},
|
||||
"reminders": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessibility",
|
||||
"automations",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"launchServices",
|
||||
"preferences",
|
||||
"reminders"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -86,16 +51,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"macos": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalMacOsPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -627,49 +582,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsContactsPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
|
||||
@@ -28,41 +28,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalMacOsPermissions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
},
|
||||
"calendar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contacts": {
|
||||
"$ref": "#/definitions/MacOsContactsPermission"
|
||||
},
|
||||
"launchServices": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
},
|
||||
"reminders": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessibility",
|
||||
"automations",
|
||||
"calendar",
|
||||
"contacts",
|
||||
"launchServices",
|
||||
"preferences",
|
||||
"reminders"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -86,16 +51,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"macos": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalMacOsPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2468,49 +2423,6 @@
|
||||
"title": "JSONRPCResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsContactsPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MacOsAutomationPermission = "none" | "all" | { "bundle_ids": Array<string> };
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MacOsContactsPermission = "none" | "read_only" | "read_write";
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MacOsPreferencesPermission = "none" | "read_only" | "read_write";
|
||||
@@ -41,9 +41,6 @@ export type { InputModality } from "./InputModality";
|
||||
export type { LocalShellAction } from "./LocalShellAction";
|
||||
export type { LocalShellExecAction } from "./LocalShellExecAction";
|
||||
export type { LocalShellStatus } from "./LocalShellStatus";
|
||||
export type { MacOsAutomationPermission } from "./MacOsAutomationPermission";
|
||||
export type { MacOsContactsPermission } from "./MacOsContactsPermission";
|
||||
export type { MacOsPreferencesPermission } from "./MacOsPreferencesPermission";
|
||||
export type { MessagePhase } from "./MessagePhase";
|
||||
export type { ModeKind } from "./ModeKind";
|
||||
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { MacOsAutomationPermission } from "../MacOsAutomationPermission";
|
||||
import type { MacOsContactsPermission } from "../MacOsContactsPermission";
|
||||
import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission";
|
||||
|
||||
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, launchServices: boolean, accessibility: boolean, calendar: boolean, reminders: boolean, contacts: MacOsContactsPermission, };
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
import type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
|
||||
import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
|
||||
export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, macos: AdditionalMacOsPermissions | null, };
|
||||
export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, };
|
||||
|
||||
@@ -5,7 +5,6 @@ export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedN
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
export type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
|
||||
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
|
||||
|
||||
@@ -1703,7 +1703,6 @@ mod tests {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
}),
|
||||
macos: None,
|
||||
}),
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
|
||||
@@ -32,10 +32,6 @@ use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission as CoreMacOsContactsPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission as CoreMacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions as CoreMacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
|
||||
@@ -1085,47 +1081,6 @@ impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AdditionalMacOsPermissions {
|
||||
pub preferences: CoreMacOsPreferencesPermission,
|
||||
pub automations: CoreMacOsAutomationPermission,
|
||||
pub launch_services: bool,
|
||||
pub accessibility: bool,
|
||||
pub calendar: bool,
|
||||
pub reminders: bool,
|
||||
pub contacts: CoreMacOsContactsPermission,
|
||||
}
|
||||
|
||||
impl From<CoreMacOsSeatbeltProfileExtensions> for AdditionalMacOsPermissions {
|
||||
fn from(value: CoreMacOsSeatbeltProfileExtensions) -> Self {
|
||||
Self {
|
||||
preferences: value.macos_preferences,
|
||||
automations: value.macos_automation,
|
||||
launch_services: value.macos_launch_services,
|
||||
accessibility: value.macos_accessibility,
|
||||
calendar: value.macos_calendar,
|
||||
reminders: value.macos_reminders,
|
||||
contacts: value.macos_contacts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
|
||||
fn from(value: AdditionalMacOsPermissions) -> Self {
|
||||
Self {
|
||||
macos_preferences: value.preferences,
|
||||
macos_automation: value.automations,
|
||||
macos_launch_services: value.launch_services,
|
||||
macos_accessibility: value.accessibility,
|
||||
macos_calendar: value.calendar,
|
||||
macos_reminders: value.reminders,
|
||||
macos_contacts: value.contacts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -1182,7 +1137,6 @@ impl From<RequestPermissionProfile> for CoreRequestPermissionProfile {
|
||||
pub struct AdditionalPermissionProfile {
|
||||
pub network: Option<AdditionalNetworkPermissions>,
|
||||
pub file_system: Option<AdditionalFileSystemPermissions>,
|
||||
pub macos: Option<AdditionalMacOsPermissions>,
|
||||
}
|
||||
|
||||
impl From<CorePermissionProfile> for AdditionalPermissionProfile {
|
||||
@@ -1190,7 +1144,6 @@ impl From<CorePermissionProfile> for AdditionalPermissionProfile {
|
||||
Self {
|
||||
network: value.network.map(AdditionalNetworkPermissions::from),
|
||||
file_system: value.file_system.map(AdditionalFileSystemPermissions::from),
|
||||
macos: value.macos.map(AdditionalMacOsPermissions::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1200,7 +1153,6 @@ impl From<AdditionalPermissionProfile> for CorePermissionProfile {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
macos: value.macos.map(CoreMacOsSeatbeltProfileExtensions::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1222,7 +1174,6 @@ impl From<GrantedPermissionProfile> for CorePermissionProfile {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
macos: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6075,8 +6026,7 @@ mod tests {
|
||||
"fileSystem": {
|
||||
"read": ["relative/path"],
|
||||
"write": null
|
||||
},
|
||||
"macos": null
|
||||
}
|
||||
},
|
||||
"proposedExecpolicyAmendment": null,
|
||||
"proposedNetworkPolicyAmendments": null,
|
||||
@@ -6090,90 +6040,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_request_approval_accepts_macos_automation_bundle_ids_object() {
|
||||
let params = serde_json::from_value::<CommandExecutionRequestApprovalParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_123",
|
||||
"itemId": "call_123",
|
||||
"command": "cat file",
|
||||
"cwd": "/tmp",
|
||||
"commandActions": null,
|
||||
"reason": null,
|
||||
"networkApprovalContext": null,
|
||||
"additionalPermissions": {
|
||||
"network": null,
|
||||
"fileSystem": null,
|
||||
"macos": {
|
||||
"preferences": "read_only",
|
||||
"automations": {
|
||||
"bundle_ids": ["com.apple.Notes"]
|
||||
},
|
||||
"launchServices": false,
|
||||
"accessibility": false,
|
||||
"calendar": false,
|
||||
"reminders": false,
|
||||
"contacts": "read_only"
|
||||
}
|
||||
},
|
||||
"proposedExecpolicyAmendment": null,
|
||||
"proposedNetworkPolicyAmendments": null,
|
||||
"availableDecisions": null
|
||||
}))
|
||||
.expect("bundle_ids object should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params
|
||||
.additional_permissions
|
||||
.and_then(|permissions| permissions.macos)
|
||||
.map(|macos| (macos.automations, macos.launch_services, macos.contacts)),
|
||||
Some((
|
||||
CoreMacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string(),]),
|
||||
false,
|
||||
CoreMacOsContactsPermission::ReadOnly,
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_request_approval_accepts_macos_reminders_permission() {
|
||||
let params = serde_json::from_value::<CommandExecutionRequestApprovalParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_123",
|
||||
"itemId": "call_123",
|
||||
"command": "cat file",
|
||||
"cwd": "/tmp",
|
||||
"commandActions": null,
|
||||
"reason": null,
|
||||
"networkApprovalContext": null,
|
||||
"additionalPermissions": {
|
||||
"network": null,
|
||||
"fileSystem": null,
|
||||
"macos": {
|
||||
"preferences": "read_only",
|
||||
"automations": "none",
|
||||
"launchServices": false,
|
||||
"accessibility": false,
|
||||
"calendar": false,
|
||||
"reminders": true,
|
||||
"contacts": "none"
|
||||
}
|
||||
},
|
||||
"proposedExecpolicyAmendment": null,
|
||||
"proposedNetworkPolicyAmendments": null,
|
||||
"availableDecisions": null
|
||||
}))
|
||||
.expect("reminders permission should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params
|
||||
.additional_permissions
|
||||
.and_then(|permissions| permissions.macos)
|
||||
.map(|macos| macos.reminders),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_request_approval_uses_request_permission_profile() {
|
||||
let read_only_path = if cfg!(windows) {
|
||||
@@ -6331,7 +6197,6 @@ mod tests {
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
}),
|
||||
macos: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1019,7 +1019,7 @@ the client can offer session-scoped and/or persistent approval choices.
|
||||
|
||||
### Permission requests
|
||||
|
||||
The built-in `request_permissions` tool sends an `item/permissions/requestApproval` JSON-RPC request to the client with the requested permission profile. This v2 payload mirrors the standalone tool's narrower permission shape, so it can request network access and additional filesystem access but does not include the broader `macos` branch used by command-execution `additionalPermissions`.
|
||||
The built-in `request_permissions` tool sends an `item/permissions/requestApproval` JSON-RPC request to the client with the requested permission profile. This v2 payload mirrors the command-execution `additionalPermissions` shape: it can request network access and additional filesystem access.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1055,7 +1055,7 @@ The client responds with `result.permissions`, which should be the granted subse
|
||||
}
|
||||
```
|
||||
|
||||
Only the granted subset matters on the wire. Any permissions omitted from `result.permissions` are treated as denied, including omitted nested keys inside `result.permissions.macos`, so a sparse response like `{ "permissions": { "macos": { "accessibility": true } } }` grants only accessibility. Any permissions not present in the original request are ignored by the server.
|
||||
Only the granted subset matters on the wire. Any permissions omitted from `result.permissions` are treated as denied. Any permissions not present in the original request are ignored by the server.
|
||||
|
||||
Within the same turn, granted permissions are sticky: later shell-like tool calls can automatically reuse the granted subset without reissuing a separate permission request.
|
||||
|
||||
|
||||
@@ -801,7 +801,6 @@ mod tests {
|
||||
write: None,
|
||||
},
|
||||
),
|
||||
macos: None,
|
||||
},
|
||||
),
|
||||
proposed_execpolicy_amendment: None,
|
||||
@@ -864,7 +863,6 @@ mod tests {
|
||||
write: None,
|
||||
},
|
||||
),
|
||||
macos: None,
|
||||
},
|
||||
),
|
||||
proposed_execpolicy_amendment: None,
|
||||
@@ -891,7 +889,6 @@ mod tests {
|
||||
"read": [allowed_path],
|
||||
"write": null,
|
||||
},
|
||||
"macos": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use tokio::process::Child;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
@@ -246,14 +246,13 @@ async fn run_command_under_sandbox(
|
||||
let mut child = match sandbox_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::Seatbelt => {
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
command,
|
||||
&config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
/*enforce_managed_network*/ false,
|
||||
network.as_ref(),
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let network_policy = config.permissions.network_sandbox_policy;
|
||||
spawn_debug_sandbox_child(
|
||||
|
||||
+2
-26
@@ -17,32 +17,8 @@ pointer file), the resolved `gitdir:` target, and `.codex` read-only.
|
||||
Network access and filesystem read/write roots are controlled by
|
||||
`SandboxPolicy`. Seatbelt consumes the resolved policy and enforces it.
|
||||
|
||||
Seatbelt also supports macOS permission-profile extensions layered on top of
|
||||
`SandboxPolicy`:
|
||||
|
||||
- no extension profile provided:
|
||||
keeps legacy default preferences read access (`user-preference-read`).
|
||||
- extension profile provided with no `macos_preferences` grant:
|
||||
does not add preferences access clauses.
|
||||
- `macos_preferences = "readonly"`:
|
||||
enables cfprefs read clauses and `user-preference-read`.
|
||||
- `macos_preferences = "readwrite"`:
|
||||
includes readonly clauses plus `user-preference-write` and cfprefs shm write
|
||||
clauses.
|
||||
- `macos_automation = true`:
|
||||
enables broad Apple Events send permissions.
|
||||
- `macos_automation = ["com.apple.Notes", ...]`:
|
||||
enables Apple Events send only to listed bundle IDs.
|
||||
- `macos_launch_services = true`:
|
||||
enables LaunchServices lookups and open/launch operations.
|
||||
- `macos_accessibility = true`:
|
||||
enables `com.apple.axserver` mach lookup.
|
||||
- `macos_calendar = true`:
|
||||
enables `com.apple.CalendarAgent` mach lookup.
|
||||
- `macos_contacts = "read_only"`:
|
||||
enables Address Book read access and Contacts read services.
|
||||
- `macos_contacts = "read_write"`:
|
||||
includes the readonly Contacts clauses plus Address Book writes and keychain/temp helpers required for writes.
|
||||
Seatbelt also keeps the legacy default preferences read access
|
||||
(`user-preference-read`) needed for cfprefs-backed macOS behavior.
|
||||
|
||||
### Linux
|
||||
|
||||
|
||||
@@ -160,7 +160,6 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
},
|
||||
"justification": params.justification.clone(),
|
||||
})
|
||||
@@ -234,7 +233,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
|
||||
|
||||
assert_eq!(
|
||||
output,
|
||||
"missing `additional_permissions`; provide at least one of `network`, `file_system`, or `macos` when using `with_additional_permissions`"
|
||||
"missing `additional_permissions`; provide at least one of `network` or `file_system` when using `with_additional_permissions`"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4379,7 +4379,6 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
@@ -4522,7 +4521,6 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
@@ -4663,7 +4661,6 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
@@ -4790,7 +4787,6 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
|
||||
@@ -78,7 +78,6 @@ use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
@@ -206,9 +205,6 @@ pub struct Permissions {
|
||||
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
|
||||
/// Whether the final Windows sandboxed child should run on a private desktop.
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
/// Optional macOS seatbelt extension profile used to extend default
|
||||
/// seatbelt permissions when running under seatbelt.
|
||||
pub macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
/// Application configuration loaded from disk and merged with overrides.
|
||||
@@ -2556,7 +2552,6 @@ impl Config {
|
||||
shell_environment_policy,
|
||||
windows_sandbox_mode,
|
||||
windows_sandbox_private_desktop,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
approvals_reviewer,
|
||||
enforce_residency: enforce_residency.value,
|
||||
|
||||
@@ -292,8 +292,6 @@ pub fn build_exec_request(
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
sandbox_policy_cwd: sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(),
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
|
||||
@@ -828,6 +828,33 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_requirement_prompts_for_inline_additional_permissions_under_on_request() {
|
||||
assert_exec_approval_requirement_for_command(
|
||||
ExecApprovalRequirementScenario {
|
||||
policy_src: None,
|
||||
command: vec![
|
||||
"zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
"touch requested-dir/requested-but-unused.txt".to_string(),
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
prefix_rule: None,
|
||||
},
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
|
||||
"touch".to_string(),
|
||||
"requested-dir/requested-but-unused.txt".to_string(),
|
||||
])),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_granular_sandbox_is_disabled()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||
use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -24,14 +24,13 @@ pub async fn spawn_command_under_seatbelt(
|
||||
network: Option<&NetworkProxy>,
|
||||
mut env: HashMap<String, String>,
|
||||
) -> std::io::Result<Child> {
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
network,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let arg0 = None;
|
||||
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
|
||||
@@ -127,18 +127,14 @@ pub(crate) fn normalize_and_validate_additional_permissions(
|
||||
}
|
||||
let Some(additional_permissions) = additional_permissions else {
|
||||
return Err(
|
||||
"missing `additional_permissions`; provide at least one of `network`, `file_system`, or `macos` when using `with_additional_permissions`"
|
||||
"missing `additional_permissions`; provide at least one of `network` or `file_system` 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 requested permission in `network`, `file_system`, or `macos`"
|
||||
"`additional_permissions` must include at least one requested permission in `network` or `file_system`"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1068,8 +1068,6 @@ impl JsReplManager {
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &turn.cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
|
||||
use_legacy_landlock: turn.features.use_legacy_landlock(),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
|
||||
@@ -23,7 +23,6 @@ use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -154,12 +153,6 @@ pub(super) async fn try_run_zsh_fork(
|
||||
windows_sandbox_level,
|
||||
arg0,
|
||||
sandbox_policy_cwd: ctx.turn.cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: ctx
|
||||
.turn
|
||||
.config
|
||||
.permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
|
||||
};
|
||||
@@ -258,12 +251,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
windows_sandbox_level: exec_request.windows_sandbox_level,
|
||||
arg0: exec_request.arg0.clone(),
|
||||
sandbox_policy_cwd: ctx.turn.cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: ctx
|
||||
.turn
|
||||
.config
|
||||
.permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
|
||||
};
|
||||
@@ -359,7 +346,6 @@ impl CoreShellActionProvider {
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
additional_permissions: Option<&PermissionProfile>,
|
||||
macos_seatbelt_profile_extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||
) -> EscalationExecution {
|
||||
match sandbox_permissions {
|
||||
SandboxPermissions::UseDefault => EscalationExecution::TurnDefault,
|
||||
@@ -373,8 +359,6 @@ impl CoreShellActionProvider {
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy,
|
||||
macos_seatbelt_profile_extensions: macos_seatbelt_profile_extensions
|
||||
.cloned(),
|
||||
},
|
||||
))
|
||||
})
|
||||
@@ -560,11 +544,6 @@ impl EscalationPolicy for CoreShellActionProvider {
|
||||
&self.file_system_sandbox_policy,
|
||||
self.network_sandbox_policy,
|
||||
self.prompt_permissions.as_ref(),
|
||||
self.turn
|
||||
.config
|
||||
.permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
),
|
||||
};
|
||||
self.process_decision(
|
||||
@@ -687,8 +666,6 @@ struct CoreShellCommandExecutor {
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
arg0: Option<String>,
|
||||
sandbox_policy_cwd: PathBuf,
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
}
|
||||
@@ -701,8 +678,6 @@ struct PrepareSandboxedExecParams<'a> {
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -787,17 +762,12 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
additional_permissions: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: self
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
})?
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(
|
||||
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,
|
||||
@@ -806,10 +776,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
additional_permissions: Some(permission_profile),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: self
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
})?
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
|
||||
@@ -822,10 +788,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
file_system_sandbox_policy: &permissions.file_system_sandbox_policy,
|
||||
network_sandbox_policy: permissions.network_sandbox_policy,
|
||||
additional_permissions: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.as_ref(),
|
||||
})?
|
||||
}
|
||||
};
|
||||
@@ -848,8 +810,6 @@ impl CoreShellCommandExecutor {
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
additional_permissions,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
} = params;
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
@@ -882,8 +842,6 @@ impl CoreShellCommandExecutor {
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::CoreShellActionProvider;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::CoreShellCommandExecutor;
|
||||
use super::InterceptedExecPolicyContext;
|
||||
use super::ParsedShellCommand;
|
||||
use super::commands_for_intercepted_exec_policy;
|
||||
@@ -8,12 +6,6 @@ use super::evaluate_intercepted_exec_policy;
|
||||
use super::extract_shell_script;
|
||||
use super::join_program_and_argv;
|
||||
use super::map_exec_result;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::config::Constrained;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::config::Permissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::GranularApprovalConfig;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
@@ -23,11 +15,7 @@ use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
@@ -36,18 +24,12 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::SandboxType;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use codex_shell_escalation::EscalationExecution;
|
||||
use codex_shell_escalation::EscalationPermissions;
|
||||
use codex_shell_escalation::ExecResult;
|
||||
use codex_shell_escalation::Permissions as EscalatedPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_shell_escalation::ShellCommandExecutor;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -76,11 +58,6 @@ fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
}])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn unrestricted_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::unrestricted()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execve_prompt_rejection_keeps_prefix_rules_on_rules_flag() {
|
||||
assert_eq!(
|
||||
@@ -294,10 +271,6 @@ fn shell_request_escalation_execution_is_explicit() {
|
||||
},
|
||||
]);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::Restricted;
|
||||
let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
CoreShellActionProvider::shell_request_escalation_execution(
|
||||
@@ -306,7 +279,6 @@ fn shell_request_escalation_execution_is_explicit() {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
None,
|
||||
Some(&macos_seatbelt_profile_extensions),
|
||||
),
|
||||
EscalationExecution::TurnDefault,
|
||||
);
|
||||
@@ -317,7 +289,6 @@ fn shell_request_escalation_execution_is_explicit() {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
None,
|
||||
Some(&macos_seatbelt_profile_extensions),
|
||||
),
|
||||
EscalationExecution::Unsandboxed,
|
||||
);
|
||||
@@ -328,14 +299,12 @@ fn shell_request_escalation_execution_is_explicit() {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
Some(&requested_permissions),
|
||||
Some(&macos_seatbelt_profile_extensions),
|
||||
),
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(
|
||||
EscalatedPermissions {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions),
|
||||
},
|
||||
)),
|
||||
);
|
||||
@@ -558,191 +527,3 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
|
||||
evaluation.decision
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_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(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
arg0: None,
|
||||
sandbox_policy_cwd: cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
..Default::default()
|
||||
}),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
};
|
||||
|
||||
let prepared = executor
|
||||
.prepare_escalated_exec(
|
||||
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
|
||||
&["echo".to_string(), "ok".to_string()],
|
||||
&cwd,
|
||||
HashMap::new(),
|
||||
EscalationExecution::TurnDefault,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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!(
|
||||
prepared
|
||||
.command
|
||||
.get(2)
|
||||
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
|
||||
"expected seatbelt policy to include macOS extension profile: {:?}",
|
||||
prepared.command
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_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::DangerFullAccess,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
arg0: None,
|
||||
sandbox_policy_cwd: cwd.to_path_buf(),
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
};
|
||||
|
||||
let permissions = Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
network_sandbox_policy: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: false,
|
||||
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
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::Permissions(
|
||||
EscalatedPermissions {
|
||||
sandbox_policy: permissions.sandbox_policy.get().clone(),
|
||||
file_system_sandbox_policy: permissions.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: permissions.network_sandbox_policy,
|
||||
macos_seatbelt_profile_extensions: permissions
|
||||
.macos_seatbelt_profile_extensions
|
||||
.clone(),
|
||||
},
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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!(
|
||||
prepared
|
||||
.command
|
||||
.get(2)
|
||||
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
|
||||
"expected seatbelt policy to include macOS extension profile: {:?}",
|
||||
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 sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
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: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
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_legacy_landlock: 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,
|
||||
macos_reminders: false,
|
||||
..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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -348,8 +348,6 @@ impl<'a> SandboxAttempt<'a> {
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
network,
|
||||
sandbox_policy_cwd: self.sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -193,7 +194,7 @@ async fn submit_turn(
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: None,
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox_policy,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
@@ -1579,7 +1580,6 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
|
||||
.clone()
|
||||
.unwrap_or_else(|| panic!("expected merged additional permissions"));
|
||||
assert_eq!(approval_permissions.network, None);
|
||||
assert_eq!(approval_permissions.macos, None);
|
||||
|
||||
let approval_file_system = approval_permissions
|
||||
.file_system
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -32,7 +32,6 @@ impl From<RequestPermissionProfile> for PermissionProfile {
|
||||
Self {
|
||||
network: value.network,
|
||||
file_system: value.file_system,
|
||||
macos: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ workspace = true
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
pub mod landlock;
|
||||
pub mod macos_permissions;
|
||||
mod manager;
|
||||
pub mod policy_transforms;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod seatbelt;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod seatbelt_permissions;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::find_system_bwrap_in_path;
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
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 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_launch_services: base.macos_launch_services || permissions.macos_launch_services,
|
||||
macos_accessibility: base.macos_accessibility || permissions.macos_accessibility,
|
||||
macos_calendar: base.macos_calendar || permissions.macos_calendar,
|
||||
macos_reminders: base.macos_reminders || permissions.macos_reminders,
|
||||
macos_contacts: union_macos_contacts_permission(
|
||||
&base.macos_contacts,
|
||||
&permissions.macos_contacts,
|
||||
),
|
||||
}),
|
||||
None => Some(permissions.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersect_macos_seatbelt_profile_extensions(
|
||||
requested: Option<MacOsSeatbeltProfileExtensions>,
|
||||
granted: Option<MacOsSeatbeltProfileExtensions>,
|
||||
) -> Option<MacOsSeatbeltProfileExtensions> {
|
||||
match (requested, granted) {
|
||||
(Some(requested), Some(granted)) => {
|
||||
let macos_automation = intersect_macos_automation_permission(
|
||||
&requested.macos_automation,
|
||||
&granted.macos_automation,
|
||||
);
|
||||
|
||||
Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: requested.macos_preferences.min(granted.macos_preferences),
|
||||
macos_automation,
|
||||
macos_launch_services: requested.macos_launch_services
|
||||
&& granted.macos_launch_services,
|
||||
macos_accessibility: requested.macos_accessibility && granted.macos_accessibility,
|
||||
macos_calendar: requested.macos_calendar && granted.macos_calendar,
|
||||
macos_reminders: requested.macos_reminders && granted.macos_reminders,
|
||||
macos_contacts: requested.macos_contacts.min(granted.macos_contacts),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
fn union_macos_contacts_permission(
|
||||
base: &MacOsContactsPermission,
|
||||
requested: &MacOsContactsPermission,
|
||||
) -> MacOsContactsPermission {
|
||||
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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_macos_automation_permission(
|
||||
requested: &MacOsAutomationPermission,
|
||||
granted: &MacOsAutomationPermission,
|
||||
) -> MacOsAutomationPermission {
|
||||
match (requested, granted) {
|
||||
(_, MacOsAutomationPermission::None) | (MacOsAutomationPermission::None, _) => {
|
||||
MacOsAutomationPermission::None
|
||||
}
|
||||
(MacOsAutomationPermission::All, granted) => granted.clone(),
|
||||
(MacOsAutomationPermission::BundleIds(requested), MacOsAutomationPermission::All) => {
|
||||
MacOsAutomationPermission::BundleIds(requested.clone())
|
||||
}
|
||||
(
|
||||
MacOsAutomationPermission::BundleIds(requested),
|
||||
MacOsAutomationPermission::BundleIds(granted),
|
||||
) => {
|
||||
let bundle_ids = requested
|
||||
.iter()
|
||||
.filter(|bundle_id| granted.contains(bundle_id))
|
||||
.cloned()
|
||||
.collect::<Vec<String>>();
|
||||
if bundle_ids.is_empty() {
|
||||
MacOsAutomationPermission::None
|
||||
} else {
|
||||
MacOsAutomationPermission::BundleIds(bundle_ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
#[path = "macos_permissions_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,121 +0,0 @@
|
||||
use super::intersect_macos_automation_permission;
|
||||
use super::intersect_macos_seatbelt_profile_extensions;
|
||||
use super::merge_macos_seatbelt_profile_extensions;
|
||||
use super::union_macos_automation_permission;
|
||||
use super::union_macos_contacts_permission;
|
||||
use super::union_macos_preferences_permission;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
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_launch_services: false,
|
||||
macos_accessibility: false,
|
||||
macos_calendar: false,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::ReadOnly,
|
||||
};
|
||||
let requested = MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
"com.apple.Calendar".to_string(),
|
||||
]),
|
||||
macos_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: true,
|
||||
macos_contacts: MacOsContactsPermission::ReadWrite,
|
||||
};
|
||||
|
||||
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_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: true,
|
||||
macos_contacts: MacOsContactsPermission::ReadWrite,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_macos_automation_permission_keeps_common_bundle_ids() {
|
||||
let requested = MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
"com.apple.Calendar".to_string(),
|
||||
]);
|
||||
let granted = MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]);
|
||||
|
||||
let intersected = intersect_macos_automation_permission(&requested, &granted);
|
||||
|
||||
assert_eq!(
|
||||
intersected,
|
||||
MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_macos_seatbelt_profile_extensions_preserves_default_grant() {
|
||||
let requested = MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec!["com.apple.Notes".to_string()]),
|
||||
macos_launch_services: false,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
};
|
||||
let granted = MacOsSeatbeltProfileExtensions::default();
|
||||
|
||||
let intersected = intersect_macos_seatbelt_profile_extensions(Some(requested), Some(granted));
|
||||
|
||||
assert_eq!(intersected, Some(MacOsSeatbeltProfileExtensions::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_macos_contacts_permission_does_not_downgrade() {
|
||||
let base = MacOsContactsPermission::ReadWrite;
|
||||
let requested = MacOsContactsPermission::ReadOnly;
|
||||
|
||||
let merged = union_macos_contacts_permission(&base, &requested);
|
||||
|
||||
assert_eq!(merged, MacOsContactsPermission::ReadWrite);
|
||||
}
|
||||
@@ -8,11 +8,9 @@ use crate::policy_transforms::should_require_platform_sandbox;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
|
||||
use crate::seatbelt::create_seatbelt_command_args_for_policies;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -102,8 +100,6 @@ pub struct SandboxTransformRequest<'a> {
|
||||
// to make shared ownership explicit across runtime/sandbox plumbing.
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
pub codex_linux_sandbox_exe: Option<&'a PathBuf>,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
@@ -181,27 +177,15 @@ impl SandboxManager {
|
||||
enforce_managed_network,
|
||||
network,
|
||||
sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
} = request;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let macos_seatbelt_profile_extensions = None;
|
||||
let additional_permissions = command.additional_permissions.take();
|
||||
let EffectiveSandboxPermissions {
|
||||
sandbox_policy: effective_policy,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: effective_macos_seatbelt_profile_extensions,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
macos_seatbelt_profile_extensions: _,
|
||||
} = EffectiveSandboxPermissions::new(
|
||||
policy,
|
||||
macos_seatbelt_profile_extensions,
|
||||
additional_permissions.as_ref(),
|
||||
);
|
||||
} = EffectiveSandboxPermissions::new(policy, additional_permissions.as_ref());
|
||||
let effective_file_system_policy = effective_file_system_sandbox_policy(
|
||||
file_system_policy,
|
||||
additional_permissions.as_ref(),
|
||||
@@ -216,14 +200,13 @@ impl SandboxManager {
|
||||
SandboxType::None => (os_argv_to_strings(argv), None),
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::MacosSeatbelt => {
|
||||
let mut args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let mut args = create_seatbelt_command_args_for_policies(
|
||||
os_argv_to_strings(argv),
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
effective_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());
|
||||
|
||||
@@ -91,8 +91,6 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
@@ -134,7 +132,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
@@ -146,8 +143,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
@@ -216,8 +211,6 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::macos_permissions::intersect_macos_seatbelt_profile_extensions;
|
||||
use crate::macos_permissions::merge_macos_seatbelt_profile_extensions;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
@@ -20,28 +17,21 @@ use std::collections::HashSet;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EffectiveSandboxPermissions {
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub macos_seatbelt_profile_extensions: Option<MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
impl EffectiveSandboxPermissions {
|
||||
pub 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: effective_sandbox_policy(sandbox_policy, Some(additional_permissions)),
|
||||
macos_seatbelt_profile_extensions: merge_macos_seatbelt_profile_extensions(
|
||||
macos_seatbelt_profile_extensions,
|
||||
additional_permissions.macos.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,12 +54,9 @@ pub fn normalize_additional_permissions(
|
||||
FileSystemPermissions { read, write }
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty());
|
||||
let macos = additional_permissions.macos;
|
||||
|
||||
Ok(PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
macos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,15 +97,10 @@ pub fn merge_permission_profiles(
|
||||
(None, Some(permissions)) => Some(permissions.clone()),
|
||||
(None, None) => None,
|
||||
};
|
||||
let macos = merge_macos_seatbelt_profile_extensions(
|
||||
base.macos.as_ref(),
|
||||
permissions.macos.as_ref(),
|
||||
);
|
||||
|
||||
Some(PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
macos,
|
||||
})
|
||||
.filter(|permissions| !permissions.is_empty())
|
||||
}
|
||||
@@ -134,26 +116,10 @@ pub fn intersect_permission_profiles(
|
||||
.file_system
|
||||
.map(|requested_file_system| {
|
||||
let granted_file_system = granted.file_system.unwrap_or_default();
|
||||
let read = requested_file_system
|
||||
.read
|
||||
.map(|requested_read| {
|
||||
let granted_read = granted_file_system.read.unwrap_or_default();
|
||||
requested_read
|
||||
.into_iter()
|
||||
.filter(|path| granted_read.contains(path))
|
||||
.collect()
|
||||
})
|
||||
.filter(|paths: &Vec<_>| !paths.is_empty());
|
||||
let write = requested_file_system
|
||||
.write
|
||||
.map(|requested_write| {
|
||||
let granted_write = granted_file_system.write.unwrap_or_default();
|
||||
requested_write
|
||||
.into_iter()
|
||||
.filter(|path| granted_write.contains(path))
|
||||
.collect()
|
||||
})
|
||||
.filter(|paths: &Vec<_>| !paths.is_empty());
|
||||
let read =
|
||||
intersect_permission_paths(requested_file_system.read, granted_file_system.read);
|
||||
let write =
|
||||
intersect_permission_paths(requested_file_system.write, granted_file_system.write);
|
||||
FileSystemPermissions { read, write }
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty());
|
||||
@@ -171,15 +137,32 @@ pub fn intersect_permission_profiles(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let macos = intersect_macos_seatbelt_profile_extensions(requested.macos, granted.macos);
|
||||
|
||||
PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
macos,
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_permission_paths(
|
||||
requested: Option<Vec<AbsolutePathBuf>>,
|
||||
granted: Option<Vec<AbsolutePathBuf>>,
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
requested.and_then(|requested_paths| {
|
||||
if requested_paths.is_empty() {
|
||||
return granted.map(|_| Vec::new());
|
||||
}
|
||||
|
||||
let granted_paths = granted.unwrap_or_default();
|
||||
Some(
|
||||
requested_paths
|
||||
.into_iter()
|
||||
.filter(|path| granted_paths.contains(path))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.filter(|paths| !paths.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_permission_paths(
|
||||
paths: Vec<AbsolutePathBuf>,
|
||||
_permission_kind: &str,
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::EffectiveSandboxPermissions;
|
||||
use super::effective_file_system_sandbox_policy;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::intersect_permission_profiles;
|
||||
use super::merge_file_system_policy_with_additional_permissions;
|
||||
use super::normalize_additional_permissions;
|
||||
use super::sandbox_policy_with_additional_permissions;
|
||||
use super::should_require_platform_sandbox;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
#[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_protocol::permissions::FileSystemAccessMode;
|
||||
@@ -110,7 +99,6 @@ fn normalize_additional_permissions_preserves_network() {
|
||||
read: Some(vec![path.clone()]),
|
||||
write: Some(vec![path.clone()]),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
@@ -172,104 +160,73 @@ fn normalize_additional_permissions_drops_empty_nested_profiles() {
|
||||
read: None,
|
||||
write: None,
|
||||
}),
|
||||
macos: None,
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(permissions, PermissionProfile::default());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn normalize_additional_permissions_preserves_default_macos_preferences_permission() {
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions::default()),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(
|
||||
permissions,
|
||||
PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions::default()),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn intersect_permission_profiles_preserves_default_macos_grants() {
|
||||
fn intersect_permission_profiles_preserves_explicit_empty_requested_reads() {
|
||||
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 requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(Vec::from(["/tmp/requested"
|
||||
.try_into()
|
||||
.expect("absolute path")])),
|
||||
write: None,
|
||||
}),
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_launch_services: false,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![path]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let granted = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(Vec::new()),
|
||||
write: None,
|
||||
}),
|
||||
macos: Some(MacOsSeatbeltProfileExtensions::default()),
|
||||
..Default::default()
|
||||
};
|
||||
let granted = requested.clone();
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested, granted),
|
||||
PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions::default()),
|
||||
..Default::default()
|
||||
}
|
||||
intersect_permission_profiles(requested.clone(), granted),
|
||||
requested
|
||||
);
|
||||
}
|
||||
|
||||
#[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_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
fn intersect_permission_profiles_drops_ungranted_nonempty_path_requests() {
|
||||
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 requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: None,
|
||||
}),
|
||||
..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_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
})
|
||||
intersect_permission_profiles(requested, PermissionProfile::default()),
|
||||
PermissionProfile::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() {
|
||||
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 requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![path]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested, PermissionProfile::default()),
|
||||
PermissionProfile::default()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,7 +253,6 @@ fn read_only_additional_permissions_can_enable_network_without_writes() {
|
||||
read: Some(vec![path.clone()]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -312,70 +268,6 @@ fn read_only_additional_permissions_can_enable_network_without_writes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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_launch_services: false,
|
||||
macos_accessibility: false,
|
||||
macos_calendar: false,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
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_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
..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_launch_services: true,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_sandbox_additional_permissions_can_enable_network() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
@@ -395,7 +287,6 @@ fn external_sandbox_additional_permissions_can_enable_network() {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::PROXY_URL_ENV_KEYS;
|
||||
use codex_network_proxy::has_proxy_url_env_vars;
|
||||
use codex_network_proxy::proxy_url_env_value;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -16,12 +15,18 @@ use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
use crate::seatbelt_permissions::build_seatbelt_extensions;
|
||||
|
||||
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
|
||||
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
|
||||
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
|
||||
include_str!("restricted_read_only_platform_defaults.sbpl");
|
||||
const MACOS_DEFAULT_PREFERENCES_POLICY: &str = r#"; allow readonly user preferences
|
||||
(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
|
||||
(allow mach-lookup
|
||||
(global-name "com.apple.cfprefsd.daemon")
|
||||
(global-name "com.apple.cfprefsd.agent")
|
||||
(local-name "com.apple.cfprefsd.agent"))
|
||||
(allow user-preference-read)
|
||||
"#;
|
||||
|
||||
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
|
||||
/// to defend against an attacker trying to inject a malicious version on the
|
||||
@@ -356,33 +361,30 @@ fn build_seatbelt_access_policy(
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
fn create_seatbelt_command_args_with_extensions(
|
||||
fn create_seatbelt_command_args(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies_with_extensions(
|
||||
create_seatbelt_command_args_for_policies(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extensions,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
||||
pub fn create_seatbelt_command_args_for_policies(
|
||||
command: Vec<String>,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
extensions: Option<&MacOsSeatbeltProfileExtensions>,
|
||||
) -> Vec<String> {
|
||||
let unreadable_roots =
|
||||
file_system_sandbox_policy.get_unreadable_roots_with_cwd(sandbox_policy_cwd);
|
||||
@@ -470,17 +472,11 @@ pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let proxy = proxy_policy_inputs(network);
|
||||
let network_policy =
|
||||
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
|
||||
let seatbelt_extensions = extensions.map_or_else(
|
||||
|| {
|
||||
// Backward-compatibility default when no extension profile is provided.
|
||||
build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default())
|
||||
},
|
||||
build_seatbelt_extensions,
|
||||
);
|
||||
|
||||
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
|
||||
let mut policy_sections = vec![
|
||||
MACOS_SEATBELT_BASE_POLICY.to_string(),
|
||||
MACOS_DEFAULT_PREFERENCES_POLICY.to_string(),
|
||||
file_read_policy,
|
||||
file_write_policy,
|
||||
network_policy,
|
||||
@@ -488,9 +484,6 @@ pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
||||
if include_platform_defaults {
|
||||
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
|
||||
}
|
||||
if !seatbelt_extensions.policy.is_empty() {
|
||||
policy_sections.push(seatbelt_extensions.policy.clone());
|
||||
}
|
||||
|
||||
let full_policy = policy_sections.join("\n");
|
||||
|
||||
@@ -499,7 +492,6 @@ pub fn create_seatbelt_command_args_for_policies_with_extensions(
|
||||
file_write_dir_params,
|
||||
macos_dir_params(),
|
||||
unix_socket_dir_params(&proxy),
|
||||
seatbelt_extensions.dir_params,
|
||||
]
|
||||
.concat();
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct SeatbeltExtensionPolicy {
|
||||
pub(crate) policy: String,
|
||||
pub(crate) dir_params: Vec<(String, PathBuf)>,
|
||||
}
|
||||
|
||||
fn normalized_extensions(
|
||||
extensions: &MacOsSeatbeltProfileExtensions,
|
||||
) -> MacOsSeatbeltProfileExtensions {
|
||||
let mut normalized = extensions.clone();
|
||||
if let MacOsAutomationPermission::BundleIds(bundle_ids) = &extensions.macos_automation {
|
||||
let bundle_ids = normalize_bundle_ids(bundle_ids);
|
||||
normalized.macos_automation = if bundle_ids.is_empty() {
|
||||
MacOsAutomationPermission::None
|
||||
} else {
|
||||
MacOsAutomationPermission::BundleIds(bundle_ids)
|
||||
};
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
pub(crate) fn build_seatbelt_extensions(
|
||||
extensions: &MacOsSeatbeltProfileExtensions,
|
||||
) -> SeatbeltExtensionPolicy {
|
||||
let extensions = normalized_extensions(extensions);
|
||||
let mut clauses = Vec::new();
|
||||
|
||||
match extensions.macos_preferences {
|
||||
MacOsPreferencesPermission::None => {}
|
||||
MacOsPreferencesPermission::ReadOnly => {
|
||||
clauses.push(
|
||||
"(allow ipc-posix-shm-read* (ipc-posix-name-prefix \"apple.cfprefs.\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.cfprefsd.daemon\")\n (global-name \"com.apple.cfprefsd.agent\")\n (local-name \"com.apple.cfprefsd.agent\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push("(allow user-preference-read)".to_string());
|
||||
}
|
||||
MacOsPreferencesPermission::ReadWrite => {
|
||||
clauses.push(
|
||||
"(allow ipc-posix-shm-read* (ipc-posix-name-prefix \"apple.cfprefs.\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.cfprefsd.daemon\")\n (global-name \"com.apple.cfprefsd.agent\")\n (local-name \"com.apple.cfprefsd.agent\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push("(allow user-preference-read)".to_string());
|
||||
clauses.push("(allow user-preference-write)".to_string());
|
||||
clauses.push(
|
||||
"(allow ipc-posix-shm-write-data (ipc-posix-name-prefix \"apple.cfprefs.\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push(
|
||||
"(allow ipc-posix-shm-write-create (ipc-posix-name-prefix \"apple.cfprefs.\"))"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match extensions.macos_automation {
|
||||
MacOsAutomationPermission::None => {}
|
||||
MacOsAutomationPermission::All => {
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push("(allow appleevent-send)".to_string());
|
||||
}
|
||||
MacOsAutomationPermission::BundleIds(bundle_ids) => {
|
||||
if !bundle_ids.is_empty() {
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
|
||||
.to_string(),
|
||||
);
|
||||
let destinations = bundle_ids
|
||||
.iter()
|
||||
.map(|bundle_id| format!(" (appleevent-destination \"{bundle_id}\")"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
clauses.push(format!("(allow appleevent-send\n{destinations}\n)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if extensions.macos_launch_services {
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.coreservices.launchservicesd\")\n (global-name \"com.apple.lsd.mapdb\")\n (global-name \"com.apple.coreservices.quarantine-resolver\")\n (global-name \"com.apple.lsd.modifydb\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push("(allow lsopen)".to_string());
|
||||
}
|
||||
|
||||
if extensions.macos_accessibility {
|
||||
clauses.push("(allow mach-lookup (local-name \"com.apple.axserver\"))".to_string());
|
||||
}
|
||||
|
||||
if extensions.macos_calendar {
|
||||
clauses.push("(allow mach-lookup (global-name \"com.apple.CalendarAgent\"))".to_string());
|
||||
}
|
||||
|
||||
if extensions.macos_reminders {
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.CalendarAgent\")\n (global-name \"com.apple.remindd\"))"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut dir_params = Vec::new();
|
||||
match extensions.macos_contacts {
|
||||
MacOsContactsPermission::None => {}
|
||||
MacOsContactsPermission::ReadOnly => {
|
||||
clauses.push(
|
||||
"(allow file-read* file-test-existence\n (subpath \"/System/Library/Address Book Plug-Ins\")\n (subpath (param \"ADDRESSBOOK_DIR\")))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.tccd\")\n (global-name \"com.apple.tccd.system\")\n (global-name \"com.apple.contactsd.persistence\")\n (global-name \"com.apple.AddressBook.ContactsAccountsService\")\n (global-name \"com.apple.contacts.account-caching\")\n (global-name \"com.apple.accountsd.accountmanager\"))"
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(addressbook_dir) = addressbook_dir() {
|
||||
dir_params.push(("ADDRESSBOOK_DIR".to_string(), addressbook_dir));
|
||||
}
|
||||
}
|
||||
MacOsContactsPermission::ReadWrite => {
|
||||
clauses.push(
|
||||
"(allow file-read* file-write*\n (subpath \"/System/Library/Address Book Plug-Ins\")\n (subpath (param \"ADDRESSBOOK_DIR\"))\n (subpath \"/var/folders\")\n (subpath \"/private/var/folders\"))"
|
||||
.to_string(),
|
||||
);
|
||||
clauses.push(
|
||||
"(allow mach-lookup\n (global-name \"com.apple.tccd\")\n (global-name \"com.apple.tccd.system\")\n (global-name \"com.apple.contactsd.persistence\")\n (global-name \"com.apple.AddressBook.ContactsAccountsService\")\n (global-name \"com.apple.contacts.account-caching\")\n (global-name \"com.apple.accountsd.accountmanager\")\n (global-name \"com.apple.securityd.xpc\"))"
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(addressbook_dir) = addressbook_dir() {
|
||||
dir_params.push(("ADDRESSBOOK_DIR".to_string(), addressbook_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clauses.is_empty() {
|
||||
SeatbeltExtensionPolicy::default()
|
||||
} else {
|
||||
SeatbeltExtensionPolicy {
|
||||
policy: format!(
|
||||
"; macOS permission profile extensions\n{}\n",
|
||||
clauses.join("\n")
|
||||
),
|
||||
dir_params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn addressbook_dir() -> Option<PathBuf> {
|
||||
Some(dirs::home_dir()?.join("Library/Application Support/AddressBook"))
|
||||
}
|
||||
|
||||
fn normalize_bundle_ids(bundle_ids: &[String]) -> Vec<String> {
|
||||
let mut unique = BTreeSet::new();
|
||||
for bundle_id in bundle_ids {
|
||||
let candidate = bundle_id.trim();
|
||||
if is_valid_bundle_id(candidate) {
|
||||
unique.insert(candidate.to_string());
|
||||
}
|
||||
}
|
||||
unique.into_iter().collect()
|
||||
}
|
||||
|
||||
fn is_valid_bundle_id(bundle_id: &str) -> bool {
|
||||
if bundle_id.len() < 3 || !bundle_id.contains('.') {
|
||||
return false;
|
||||
}
|
||||
bundle_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "seatbelt_permissions_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,154 +0,0 @@
|
||||
use super::MacOsAutomationPermission;
|
||||
use super::MacOsContactsPermission;
|
||||
use super::MacOsPreferencesPermission;
|
||||
use super::MacOsSeatbeltProfileExtensions;
|
||||
use super::build_seatbelt_extensions;
|
||||
|
||||
#[test]
|
||||
fn preferences_read_only_emits_read_clauses_only() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadOnly,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(policy.policy.contains("(allow user-preference-read)"));
|
||||
assert!(!policy.policy.contains("(allow user-preference-write)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_read_write_emits_write_clauses() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(policy.policy.contains("(allow user-preference-read)"));
|
||||
assert!(policy.policy.contains("(allow user-preference-write)"));
|
||||
assert!(
|
||||
policy.policy.contains(
|
||||
"(allow ipc-posix-shm-write-create (ipc-posix-name-prefix \"apple.cfprefs.\"))"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_all_emits_unscoped_appleevents() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_automation: MacOsAutomationPermission::All,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(policy.policy.contains("(allow appleevent-send)"));
|
||||
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_bundle_ids_are_normalized_and_scoped() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
" com.apple.Notes ".to_string(),
|
||||
"com.apple.Calendar".to_string(),
|
||||
"bad bundle".to_string(),
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("(appleevent-destination \"com.apple.Calendar\")")
|
||||
);
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("(appleevent-destination \"com.apple.Notes\")")
|
||||
);
|
||||
assert!(!policy.policy.contains("bad bundle"));
|
||||
assert!(policy.policy.contains("com.apple.coreservices.appleevents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_services_emit_launch_clauses() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_launch_services: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("com.apple.coreservices.launchservicesd")
|
||||
);
|
||||
assert!(policy.policy.contains("com.apple.lsd.mapdb"));
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("com.apple.coreservices.quarantine-resolver")
|
||||
);
|
||||
assert!(policy.policy.contains("com.apple.lsd.modifydb"));
|
||||
assert!(policy.policy.contains("(allow lsopen)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessibility_and_calendar_emit_mach_lookups() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(policy.policy.contains("com.apple.axserver"));
|
||||
assert!(policy.policy.contains("com.apple.CalendarAgent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reminders_emit_calendar_agent_and_remindd_lookups() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_reminders: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(policy.policy.contains("com.apple.CalendarAgent"));
|
||||
assert!(policy.policy.contains("com.apple.remindd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contacts_read_only_emit_contacts_read_clauses() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_contacts: MacOsContactsPermission::ReadOnly,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("(subpath \"/System/Library/Address Book Plug-Ins\")")
|
||||
);
|
||||
assert!(
|
||||
policy
|
||||
.policy
|
||||
.contains("(subpath (param \"ADDRESSBOOK_DIR\"))")
|
||||
);
|
||||
assert!(policy.policy.contains("com.apple.contactsd.persistence"));
|
||||
assert!(policy.policy.contains("com.apple.accountsd.accountmanager"));
|
||||
assert!(!policy.policy.contains("com.apple.securityd.xpc"));
|
||||
assert!(
|
||||
policy
|
||||
.dir_params
|
||||
.iter()
|
||||
.any(|(key, _)| key == "ADDRESSBOOK_DIR")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contacts_read_write_emit_write_clauses() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions {
|
||||
macos_contacts: MacOsContactsPermission::ReadWrite,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(policy.policy.contains("(subpath \"/var/folders\")"));
|
||||
assert!(policy.policy.contains("(subpath \"/private/var/folders\")"));
|
||||
assert!(policy.policy.contains("com.apple.securityd.xpc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_extensions_emit_preferences_read_only_policy() {
|
||||
let policy = build_seatbelt_extensions(&MacOsSeatbeltProfileExtensions::default());
|
||||
assert!(policy.policy.contains("(allow user-preference-read)"));
|
||||
assert!(!policy.policy.contains("(allow user-preference-write)"));
|
||||
}
|
||||
@@ -2,17 +2,13 @@ use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use super::MACOS_SEATBELT_BASE_POLICY;
|
||||
use super::ProxyPolicyInputs;
|
||||
use super::UnixDomainSocketPolicy;
|
||||
use super::create_seatbelt_command_args_for_policies_with_extensions;
|
||||
use super::create_seatbelt_command_args_with_extensions;
|
||||
use super::create_seatbelt_command_args;
|
||||
use super::create_seatbelt_command_args_for_policies;
|
||||
use super::dynamic_network_policy;
|
||||
use super::macos_dir_params;
|
||||
use super::normalize_path_for_sandbox;
|
||||
use super::unix_socket_dir_params;
|
||||
use super::unix_socket_policy;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -115,14 +111,13 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/"),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
@@ -181,14 +176,13 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
|
||||
},
|
||||
]);
|
||||
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
let args = create_seatbelt_command_args_for_policies(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/"),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
@@ -217,96 +211,15 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_args_include_macos_permission_extensions() {
|
||||
let cwd = std::env::temp_dir();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
vec!["echo".to_string(), "ok".to_string()],
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.as_path(),
|
||||
false,
|
||||
None,
|
||||
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,
|
||||
}),
|
||||
);
|
||||
let policy = &args[1];
|
||||
|
||||
assert!(policy.contains("(allow user-preference-write)"));
|
||||
assert!(policy.contains("(appleevent-destination \"com.apple.Notes\")"));
|
||||
assert!(policy.contains("com.apple.axserver"));
|
||||
assert!(policy.contains("com.apple.CalendarAgent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_id_automation_keeps_lsopen_denied() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let cwd = tmp.path().join("cwd");
|
||||
fs::create_dir_all(&cwd).expect("create cwd");
|
||||
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
vec![
|
||||
"/usr/bin/python3".to_string(),
|
||||
"-c".to_string(),
|
||||
r#"import ctypes
|
||||
import os
|
||||
import sys
|
||||
lib = ctypes.CDLL("/usr/lib/libsandbox.1.dylib")
|
||||
lib.sandbox_check.restype = ctypes.c_int
|
||||
allowed = lib.sandbox_check(os.getpid(), b"lsopen", 0) == 0
|
||||
sys.exit(0 if allowed else 13)
|
||||
"#
|
||||
.to_string(),
|
||||
],
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.as_path(),
|
||||
false,
|
||||
None,
|
||||
Some(&MacOsSeatbeltProfileExtensions {
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&args)
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("execute seatbelt command");
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted") {
|
||||
return;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Some(13),
|
||||
output.status.code(),
|
||||
"lsopen should remain denied even with bundle-scoped automation\nstdout: {}\nstderr: {stderr}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() {
|
||||
let cwd = std::env::temp_dir();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
vec!["echo".to_string(), "ok".to_string()],
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.as_path(),
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let policy = &args[1];
|
||||
assert!(policy.contains("(allow user-preference-read)"));
|
||||
@@ -319,7 +232,7 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
||||
let cwd = tmp.path().join("workspace");
|
||||
fs::create_dir_all(cwd.join("docs")).expect("create docs");
|
||||
let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs");
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
@@ -334,7 +247,6 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
||||
cwd.as_path(),
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -351,25 +263,6 @@ fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_args_default_extension_profile_keeps_preferences_read_access() {
|
||||
let cwd = std::env::temp_dir();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
vec!["echo".to_string(), "ok".to_string()],
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.as_path(),
|
||||
false,
|
||||
None,
|
||||
Some(&MacOsSeatbeltProfileExtensions::default()),
|
||||
);
|
||||
let policy = &args[1];
|
||||
assert!(!policy.contains("appleevent-send"));
|
||||
assert!(!policy.contains("com.apple.axserver"));
|
||||
assert!(!policy.contains("com.apple.CalendarAgent"));
|
||||
assert!(policy.contains("(allow user-preference-read)"));
|
||||
assert!(!policy.contains("user-preference-write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
|
||||
let policy = dynamic_network_policy(
|
||||
@@ -664,13 +557,12 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
shell_command.clone(),
|
||||
&policy,
|
||||
&cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
|
||||
let policy_text = seatbelt_policy_arg(&args);
|
||||
@@ -779,13 +671,12 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let write_hooks_file_args = create_seatbelt_command_args_with_extensions(
|
||||
let write_hooks_file_args = create_seatbelt_command_args(
|
||||
shell_command_git,
|
||||
&policy,
|
||||
&cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&write_hooks_file_args)
|
||||
@@ -816,13 +707,12 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let write_allowed_file_args = create_seatbelt_command_args_with_extensions(
|
||||
let write_allowed_file_args = create_seatbelt_command_args(
|
||||
shell_command_allowed,
|
||||
&policy,
|
||||
&cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&write_allowed_file_args)
|
||||
@@ -882,13 +772,12 @@ fn create_seatbelt_args_block_first_time_dot_codex_creation_with_exact_and_desce
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
shell_command,
|
||||
&policy,
|
||||
repo_root.as_path(),
|
||||
/*enforce_managed_network*/ false,
|
||||
/*network*/ None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
|
||||
let policy_text = seatbelt_policy_arg(&args);
|
||||
@@ -938,13 +827,12 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
shell_command,
|
||||
&policy,
|
||||
&cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
@@ -975,13 +863,12 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let gitdir_args = create_seatbelt_command_args_with_extensions(
|
||||
let gitdir_args = create_seatbelt_command_args(
|
||||
shell_command_gitdir,
|
||||
&policy,
|
||||
&cwd,
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
|
||||
.args(&gitdir_args)
|
||||
@@ -1039,13 +926,12 @@ fn create_seatbelt_args_for_cwd_as_git_repo() {
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let args = create_seatbelt_command_args_with_extensions(
|
||||
let args = create_seatbelt_command_args(
|
||||
shell_command.clone(),
|
||||
&policy,
|
||||
vulnerable_root.as_path(),
|
||||
/*enforce_managed_network*/ false,
|
||||
None,
|
||||
/*extensions*/ None,
|
||||
);
|
||||
|
||||
let tmpdir_env_var = std::env::var("TMPDIR")
|
||||
@@ -1067,19 +953,19 @@ fn create_seatbelt_args_for_cwd_as_git_repo() {
|
||||
// - write access to WRITABLE_ROOT_0 (but not its .git or .codex), WRITABLE_ROOT_1, and cwd as WRITABLE_ROOT_2.
|
||||
let expected_policy = format!(
|
||||
r#"{MACOS_SEATBELT_BASE_POLICY}
|
||||
; allow read-only file operations
|
||||
(allow file-read*)
|
||||
(allow file-write*
|
||||
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (literal (param "WRITABLE_ROOT_0_EXCLUDED_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_EXCLUDED_0"))) (require-not (literal (param "WRITABLE_ROOT_0_EXCLUDED_1"))) (require-not (subpath (param "WRITABLE_ROOT_0_EXCLUDED_1"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry}
|
||||
)
|
||||
|
||||
; macOS permission profile extensions
|
||||
; allow readonly user preferences
|
||||
(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
|
||||
(allow mach-lookup
|
||||
(global-name "com.apple.cfprefsd.daemon")
|
||||
(global-name "com.apple.cfprefsd.agent")
|
||||
(local-name "com.apple.cfprefsd.agent"))
|
||||
(allow user-preference-read)
|
||||
|
||||
; allow read-only file operations
|
||||
(allow file-read*)
|
||||
(allow file-write*
|
||||
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (literal (param "WRITABLE_ROOT_0_EXCLUDED_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_EXCLUDED_0"))) (require-not (literal (param "WRITABLE_ROOT_0_EXCLUDED_1"))) (require-not (subpath (param "WRITABLE_ROOT_0_EXCLUDED_1"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry}
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
|
||||
@@ -19,9 +19,6 @@ use crate::render::renderable::Renderable;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::mcp::RequestId;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ElicitationAction;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -777,48 +774,6 @@ pub(crate) fn format_additional_permissions_rule(
|
||||
parts.push(format!("write {writes}"));
|
||||
}
|
||||
}
|
||||
if let Some(macos) = additional_permissions.macos.as_ref() {
|
||||
if !matches!(
|
||||
macos.macos_preferences,
|
||||
MacOsPreferencesPermission::ReadOnly
|
||||
) {
|
||||
let value = match macos.macos_preferences {
|
||||
MacOsPreferencesPermission::ReadOnly => "readonly",
|
||||
MacOsPreferencesPermission::ReadWrite => "readwrite",
|
||||
MacOsPreferencesPermission::None => "none",
|
||||
};
|
||||
parts.push(format!("macOS preferences {value}"));
|
||||
}
|
||||
match &macos.macos_automation {
|
||||
MacOsAutomationPermission::All => {
|
||||
parts.push("macOS automation all".to_string());
|
||||
}
|
||||
MacOsAutomationPermission::BundleIds(bundle_ids) => {
|
||||
if !bundle_ids.is_empty() {
|
||||
parts.push(format!("macOS automation {}", bundle_ids.join(", ")));
|
||||
}
|
||||
}
|
||||
MacOsAutomationPermission::None => {}
|
||||
}
|
||||
if macos.macos_accessibility {
|
||||
parts.push("macOS accessibility".to_string());
|
||||
}
|
||||
if macos.macos_calendar {
|
||||
parts.push("macOS calendar".to_string());
|
||||
}
|
||||
if macos.macos_reminders {
|
||||
parts.push("macOS reminders".to_string());
|
||||
}
|
||||
if !matches!(macos.macos_contacts, MacOsContactsPermission::None) {
|
||||
let value = match macos.macos_contacts {
|
||||
MacOsContactsPermission::None => "none",
|
||||
MacOsContactsPermission::ReadOnly => "readonly",
|
||||
MacOsContactsPermission::ReadWrite => "readwrite",
|
||||
};
|
||||
parts.push(format!("macOS contacts {value}"));
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -906,9 +861,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::protocol::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::NetworkApprovalProtocol;
|
||||
@@ -1334,7 +1286,6 @@ mod tests {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1382,7 +1333,6 @@ mod tests {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1404,42 +1354,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_permissions_macos_prompt_snapshot() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
command: vec!["osascript".into(), "-e".into(), "tell application".into()],
|
||||
reason: Some("need macOS automation".into()),
|
||||
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
|
||||
network_approval_context: None,
|
||||
additional_permissions: Some(PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Calendar".to_string(),
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_launch_services: false,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: true,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults());
|
||||
assert_snapshot!(
|
||||
"approval_overlay_additional_permissions_macos_prompt",
|
||||
render_overlay_lines(&view, 120)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_exec_prompt_title_includes_host() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/approval_overlay.rs
|
||||
expression: "render_overlay_lines(&view, 120)"
|
||||
---
|
||||
|
||||
Would you like to run the following command?
|
||||
|
||||
Reason: need macOS automation
|
||||
|
||||
Permission rule: macOS preferences readwrite; macOS automation com.apple.Calendar, com.apple.Notes; macOS
|
||||
accessibility; macOS calendar; macOS reminders
|
||||
|
||||
$ osascript -e 'tell application'
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
@@ -7568,7 +7568,6 @@ guardian_approval = true
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
});
|
||||
params.proposed_network_policy_amendments = Some(vec![AppServerNetworkPolicyAmendment {
|
||||
host: "example.com".to_string(),
|
||||
@@ -7601,7 +7600,6 @@ guardian_approval = true
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -19,9 +19,6 @@ use crate::render::renderable::Renderable;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::mcp::RequestId;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsContactsPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ElicitationAction;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -763,48 +760,6 @@ pub(crate) fn format_additional_permissions_rule(
|
||||
parts.push(format!("write {writes}"));
|
||||
}
|
||||
}
|
||||
if let Some(macos) = additional_permissions.macos.as_ref() {
|
||||
if !matches!(
|
||||
macos.macos_preferences,
|
||||
MacOsPreferencesPermission::ReadOnly
|
||||
) {
|
||||
let value = match macos.macos_preferences {
|
||||
MacOsPreferencesPermission::ReadOnly => "readonly",
|
||||
MacOsPreferencesPermission::ReadWrite => "readwrite",
|
||||
MacOsPreferencesPermission::None => "none",
|
||||
};
|
||||
parts.push(format!("macOS preferences {value}"));
|
||||
}
|
||||
match &macos.macos_automation {
|
||||
MacOsAutomationPermission::All => {
|
||||
parts.push("macOS automation all".to_string());
|
||||
}
|
||||
MacOsAutomationPermission::BundleIds(bundle_ids) => {
|
||||
if !bundle_ids.is_empty() {
|
||||
parts.push(format!("macOS automation {}", bundle_ids.join(", ")));
|
||||
}
|
||||
}
|
||||
MacOsAutomationPermission::None => {}
|
||||
}
|
||||
if macos.macos_accessibility {
|
||||
parts.push("macOS accessibility".to_string());
|
||||
}
|
||||
if macos.macos_calendar {
|
||||
parts.push("macOS calendar".to_string());
|
||||
}
|
||||
if macos.macos_reminders {
|
||||
parts.push("macOS reminders".to_string());
|
||||
}
|
||||
if !matches!(macos.macos_contacts, MacOsContactsPermission::None) {
|
||||
let value = match macos.macos_contacts {
|
||||
MacOsContactsPermission::None => "none",
|
||||
MacOsContactsPermission::ReadOnly => "readonly",
|
||||
MacOsContactsPermission::ReadWrite => "readwrite",
|
||||
};
|
||||
parts.push(format!("macOS contacts {value}"));
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -892,9 +847,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::protocol::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::NetworkApprovalProtocol;
|
||||
@@ -1320,7 +1272,6 @@ mod tests {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1368,7 +1319,6 @@ mod tests {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1390,42 +1340,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_permissions_macos_prompt_snapshot() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
command: vec!["osascript".into(), "-e".into(), "tell application".into()],
|
||||
reason: Some("need macOS automation".into()),
|
||||
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
|
||||
network_approval_context: None,
|
||||
additional_permissions: Some(PermissionProfile {
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Calendar".to_string(),
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_launch_services: false,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
macos_reminders: true,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults());
|
||||
assert_snapshot!(
|
||||
"approval_overlay_additional_permissions_macos_prompt",
|
||||
render_overlay_lines(&view, 120)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_exec_prompt_title_includes_host() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/approval_overlay.rs
|
||||
expression: "render_overlay_lines(&view, 120)"
|
||||
---
|
||||
|
||||
Would you like to run the following command?
|
||||
|
||||
Reason: need macOS automation
|
||||
|
||||
Permission rule: macOS preferences readwrite; macOS automation com.apple.Calendar, com.apple.Notes; macOS
|
||||
accessibility; macOS calendar; macOS reminders
|
||||
|
||||
$ osascript -e 'tell application'
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: tui_app_server/src/bottom_pane/approval_overlay.rs
|
||||
expression: "render_overlay_lines(&view, 120)"
|
||||
---
|
||||
|
||||
Would you like to run the following command?
|
||||
|
||||
Reason: need macOS automation
|
||||
|
||||
Permission rule: macOS preferences readwrite; macOS automation com.apple.Calendar, com.apple.Notes; macOS
|
||||
accessibility; macOS calendar; macOS reminders
|
||||
|
||||
$ osascript -e 'tell application'
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
Reference in New Issue
Block a user