From af754b023b8d0a20e8ae7a26df949c3991489ee1 Mon Sep 17 00:00:00 2001 From: Jack Mousseau Date: Fri, 13 Mar 2026 19:49:04 -0700 Subject: [PATCH 1/2] Use request permission profile in app server --- .../PermissionsRequestApprovalParams.json | 93 +---- .../PermissionsRequestApprovalResponse.json | 112 ----- .../schema/json/ServerRequest.json | 28 +- .../codex_app_server_protocol.schemas.json | 97 ++--- .../typescript/v2/GrantedMacOsPermissions.ts | 8 - .../typescript/v2/GrantedPermissionProfile.ts | 3 +- .../v2/PermissionsRequestApprovalParams.ts | 4 +- .../typescript/v2/RequestPermissionProfile.ts | 7 + .../schema/typescript/v2/index.ts | 2 +- .../app-server-protocol/src/protocol/v2.rs | 389 +++++++----------- codex-rs/app-server/README.md | 2 +- .../app-server/src/bespoke_event_handling.rs | 3 +- .../tests/suite/v2/request_permissions.rs | 1 - 13 files changed, 223 insertions(+), 526 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RequestPermissionProfile.ts diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index 0d5c09193a65..ac8d5c4010c6 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -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": { @@ -74,7 +39,8 @@ }, "type": "object" }, - "AdditionalPermissionProfile": { + "RequestPermissionProfile": { + "additionalProperties": false, "properties": { "fileSystem": { "anyOf": [ @@ -86,16 +52,6 @@ } ] }, - "macos": { - "anyOf": [ - { - "$ref": "#/definitions/AdditionalMacOsPermissions" - }, - { - "type": "null" - } - ] - }, "network": { "anyOf": [ { @@ -108,49 +64,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" } }, "properties": { @@ -158,7 +71,7 @@ "type": "string" }, "permissions": { - "$ref": "#/definitions/AdditionalPermissionProfile" + "$ref": "#/definitions/RequestPermissionProfile" }, "reason": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json index df9e519dcf4d..7b0c2b1a3bc4 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json @@ -39,65 +39,6 @@ }, "type": "object" }, - "GrantedMacOsPermissions": { - "properties": { - "accessibility": { - "type": [ - "boolean", - "null" - ] - }, - "automations": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsAutomationPermission" - }, - { - "type": "null" - } - ] - }, - "calendar": { - "type": [ - "boolean", - "null" - ] - }, - "contacts": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsContactsPermission" - }, - { - "type": "null" - } - ] - }, - "launchServices": { - "type": [ - "boolean", - "null" - ] - }, - "preferences": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsPreferencesPermission" - }, - { - "type": "null" - } - ] - }, - "reminders": { - "type": [ - "boolean", - "null" - ] - } - }, - "type": "object" - }, "GrantedPermissionProfile": { "properties": { "fileSystem": { @@ -110,16 +51,6 @@ } ] }, - "macos": { - "anyOf": [ - { - "$ref": "#/definitions/GrantedMacOsPermissions" - }, - { - "type": "null" - } - ] - }, "network": { "anyOf": [ { @@ -133,49 +64,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" - }, "PermissionGrantScope": { "enum": [ "turn", diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index a00871971fdf..1fbbfb1b0a3c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -1449,7 +1449,7 @@ "type": "string" }, "permissions": { - "$ref": "#/definitions/AdditionalPermissionProfile" + "$ref": "#/definitions/RequestPermissionProfile" }, "reason": { "type": [ @@ -1483,6 +1483,32 @@ } ] }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "ThreadId": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 75054b0d6abe..3f30eeeea776 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2198,65 +2198,6 @@ "title": "FuzzyFileSearchSessionUpdatedNotification", "type": "object" }, - "GrantedMacOsPermissions": { - "properties": { - "accessibility": { - "type": [ - "boolean", - "null" - ] - }, - "automations": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsAutomationPermission" - }, - { - "type": "null" - } - ] - }, - "calendar": { - "type": [ - "boolean", - "null" - ] - }, - "contacts": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsContactsPermission" - }, - { - "type": "null" - } - ] - }, - "launchServices": { - "type": [ - "boolean", - "null" - ] - }, - "preferences": { - "anyOf": [ - { - "$ref": "#/definitions/MacOsPreferencesPermission" - }, - { - "type": "null" - } - ] - }, - "reminders": { - "type": [ - "boolean", - "null" - ] - } - }, - "type": "object" - }, "GrantedPermissionProfile": { "properties": { "fileSystem": { @@ -2269,16 +2210,6 @@ } ] }, - "macos": { - "anyOf": [ - { - "$ref": "#/definitions/GrantedMacOsPermissions" - }, - { - "type": "null" - } - ] - }, "network": { "anyOf": [ { @@ -3324,7 +3255,7 @@ "type": "string" }, "permissions": { - "$ref": "#/definitions/AdditionalPermissionProfile" + "$ref": "#/definitions/RequestPermissionProfile" }, "reason": { "type": [ @@ -3382,6 +3313,32 @@ ], "title": "RequestId" }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "ReviewDecision": { "description": "User's decision in response to an ExecApprovalRequest.", "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts deleted file mode 100644 index edf779488742..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedMacOsPermissions.ts +++ /dev/null @@ -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 GrantedMacOsPermissions = { preferences?: MacOsPreferencesPermission, automations?: MacOsAutomationPermission, launchServices?: boolean, accessibility?: boolean, calendar?: boolean, reminders?: boolean, contacts?: MacOsContactsPermission, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedPermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GrantedPermissionProfile.ts index 84a9aa3778da..3ae6c6051129 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GrantedPermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GrantedPermissionProfile.ts @@ -3,6 +3,5 @@ // 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 { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; -import type { GrantedMacOsPermissions } from "./GrantedMacOsPermissions"; -export type GrantedPermissionProfile = { network?: AdditionalNetworkPermissions, fileSystem?: AdditionalFileSystemPermissions, macos?: GrantedMacOsPermissions, }; +export type GrantedPermissionProfile = { network?: AdditionalNetworkPermissions, fileSystem?: AdditionalFileSystemPermissions, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionsRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionsRequestApprovalParams.ts index cde277f1a426..efdefead794a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionsRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionsRequestApprovalParams.ts @@ -1,6 +1,6 @@ // 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 { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; +import type { RequestPermissionProfile } from "./RequestPermissionProfile"; -export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string, reason: string | null, permissions: AdditionalPermissionProfile, }; +export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string, reason: string | null, permissions: RequestPermissionProfile, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RequestPermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RequestPermissionProfile.ts new file mode 100644 index 000000000000..2bf8d8dffefd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RequestPermissionProfile.ts @@ -0,0 +1,7 @@ +// 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 { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; + +export type RequestPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 32272e59bd05..b8419a79ddef 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -115,7 +115,6 @@ export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; export type { GitInfo } from "./GitInfo"; -export type { GrantedMacOsPermissions } from "./GrantedMacOsPermissions"; export type { GrantedPermissionProfile } from "./GrantedPermissionProfile"; export type { GuardianApprovalReview } from "./GuardianApprovalReview"; export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus"; @@ -222,6 +221,7 @@ export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPa export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification"; export type { RemoteSkillSummary } from "./RemoteSkillSummary"; +export type { RequestPermissionProfile } from "./RequestPermissionProfile"; export type { ResidencyRequirement } from "./ResidencyRequirement"; export type { ReviewDelivery } from "./ReviewDelivery"; export type { ReviewStartParams } from "./ReviewStartParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e86900cbc725..7314cb4c68a2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -80,6 +80,7 @@ use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; use codex_protocol::protocol::TokenUsage as CoreTokenUsage; use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope; +use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile; use codex_protocol::user_input::ByteRange as CoreByteRange; use codex_protocol::user_input::TextElement as CoreTextElement; use codex_protocol::user_input::UserInput as CoreUserInput; @@ -1081,74 +1082,56 @@ impl From for CoreNetworkPermissions { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] #[ts(export_to = "v2/")] -pub struct AdditionalPermissionProfile { +pub struct RequestPermissionProfile { pub network: Option, pub file_system: Option, - pub macos: Option, } -impl From for AdditionalPermissionProfile { - fn from(value: CorePermissionProfile) -> Self { +impl From for RequestPermissionProfile { + fn from(value: CoreRequestPermissionProfile) -> Self { Self { network: value.network.map(AdditionalNetworkPermissions::from), file_system: value.file_system.map(AdditionalFileSystemPermissions::from), - macos: value.macos.map(AdditionalMacOsPermissions::from), } } } -impl From for CorePermissionProfile { - fn from(value: AdditionalPermissionProfile) -> Self { +impl From for CoreRequestPermissionProfile { + fn from(value: RequestPermissionProfile) -> Self { Self { network: value.network.map(CoreNetworkPermissions::from), file_system: value.file_system.map(CoreFileSystemPermissions::from), - macos: value.macos.map(CoreMacOsSeatbeltProfileExtensions::from), } } } -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct GrantedMacOsPermissions { - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub preferences: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub automations: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub launch_services: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub accessibility: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub calendar: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub reminders: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub contacts: Option, +pub struct AdditionalPermissionProfile { + pub network: Option, + pub file_system: Option, + pub macos: Option, } -impl From for CoreMacOsSeatbeltProfileExtensions { - fn from(value: GrantedMacOsPermissions) -> Self { +impl From for AdditionalPermissionProfile { + fn from(value: CorePermissionProfile) -> Self { Self { - macos_preferences: value - .preferences - .unwrap_or(CoreMacOsPreferencesPermission::None), - macos_automation: value - .automations - .unwrap_or(CoreMacOsAutomationPermission::None), - macos_launch_services: value.launch_services.unwrap_or(false), - macos_accessibility: value.accessibility.unwrap_or(false), - macos_calendar: value.calendar.unwrap_or(false), - macos_reminders: value.reminders.unwrap_or(false), - macos_contacts: value.contacts.unwrap_or(CoreMacOsContactsPermission::None), + network: value.network.map(AdditionalNetworkPermissions::from), + file_system: value.file_system.map(AdditionalFileSystemPermissions::from), + macos: value.macos.map(AdditionalMacOsPermissions::from), + } + } +} + +impl From for CorePermissionProfile { + fn from(value: AdditionalPermissionProfile) -> Self { + Self { + network: value.network.map(CoreNetworkPermissions::from), + file_system: value.file_system.map(CoreFileSystemPermissions::from), + macos: value.macos.map(CoreMacOsSeatbeltProfileExtensions::from), } } } @@ -1163,32 +1146,14 @@ pub struct GrantedPermissionProfile { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub file_system: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub macos: Option, } impl From for CorePermissionProfile { fn from(value: GrantedPermissionProfile) -> Self { - let macos = value.macos.and_then(|macos| { - if macos.preferences.is_none() - && macos.automations.is_none() - && macos.launch_services.is_none() - && macos.accessibility.is_none() - && macos.calendar.is_none() - && macos.reminders.is_none() - && macos.contacts.is_none() - { - None - } else { - Some(CoreMacOsSeatbeltProfileExtensions::from(macos)) - } - }); - Self { network: value.network.map(CoreNetworkPermissions::from), file_system: value.file_system.map(CoreFileSystemPermissions::from), - macos, + macos: None, } } } @@ -5570,7 +5535,7 @@ pub struct PermissionsRequestApprovalParams { pub turn_id: String, pub item_id: String, pub reason: Option, - pub permissions: AdditionalPermissionProfile, + pub permissions: RequestPermissionProfile, } v2_enum_from_core!( @@ -5997,192 +5962,144 @@ mod tests { } #[test] - fn permissions_request_approval_response_accepts_partial_macos_grants() { - let cases = vec![ - (json!({}), Some(GrantedMacOsPermissions::default()), None), - ( - json!({ - "preferences": "read_only", - }), - Some(GrantedMacOsPermissions { - preferences: Some(CoreMacOsPreferencesPermission::ReadOnly), - ..Default::default() - }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::ReadOnly, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: false, - macos_accessibility: false, - macos_calendar: false, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "automations": { - "bundle_ids": ["com.apple.Notes"], - }, - }), - Some(GrantedMacOsPermissions { - automations: Some(CoreMacOsAutomationPermission::BundleIds(vec![ - "com.apple.Notes".to_string(), - ])), - ..Default::default() + fn permissions_request_approval_uses_request_permission_profile() { + let params = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "reason": "Select a workspace root", + "permissions": { + "network": { + "enabled": true, + }, + "fileSystem": { + "read": ["/tmp/read-only"], + "write": ["/tmp/read-write"], + }, + }, + })) + .expect("permissions request should deserialize"); + + assert_eq!( + params.permissions, + RequestPermissionProfile { + network: Some(AdditionalNetworkPermissions { + enabled: Some(true), }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::BundleIds(vec![ - "com.apple.Notes".to_string(), + file_system: Some(AdditionalFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + .expect("path must be absolute"), ]), - macos_launch_services: false, - macos_accessibility: false, - macos_calendar: false, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "launchServices": true, - }), - Some(GrantedMacOsPermissions { - launch_services: Some(true), - ..Default::default() - }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: true, - macos_accessibility: false, - macos_calendar: false, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "accessibility": true, - }), - Some(GrantedMacOsPermissions { - accessibility: Some(true), - ..Default::default() - }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: false, - macos_accessibility: true, - macos_calendar: false, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "calendar": true, - }), - Some(GrantedMacOsPermissions { - calendar: Some(true), - ..Default::default() - }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: false, - macos_accessibility: false, - macos_calendar: true, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "reminders": true, - }), - Some(GrantedMacOsPermissions { - reminders: Some(true), - ..Default::default() - }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: false, - macos_accessibility: false, - macos_calendar: false, - macos_reminders: true, - macos_contacts: CoreMacOsContactsPermission::None, - }), - ), - ( - json!({ - "contacts": "read_only", }), - Some(GrantedMacOsPermissions { - contacts: Some(CoreMacOsContactsPermission::ReadOnly), - ..Default::default() + } + ); + + assert_eq!( + CoreRequestPermissionProfile::from(params.permissions), + CoreRequestPermissionProfile { + network: Some(CoreNetworkPermissions { + enabled: Some(true), }), - Some(CoreMacOsSeatbeltProfileExtensions { - macos_preferences: CoreMacOsPreferencesPermission::None, - macos_automation: CoreMacOsAutomationPermission::None, - macos_launch_services: false, - macos_accessibility: false, - macos_calendar: false, - macos_reminders: false, - macos_contacts: CoreMacOsContactsPermission::ReadOnly, + file_system: Some(CoreFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + .expect("path must be absolute"), + ]), }), - ), - ]; + } + ); + } - for (macos_json, expected_granted_macos, expected_core_macos) in cases { - let response = serde_json::from_value::(json!({ - "permissions": { - "macos": macos_json, + #[test] + fn permissions_request_approval_rejects_macos_permissions() { + let err = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "reason": "Select a workspace root", + "permissions": { + "network": null, + "fileSystem": null, + "macos": { + "preferences": "read_only", + "automations": "none", + "launchServices": false, + "accessibility": false, + "calendar": false, + "reminders": false, + "contacts": "none", }, - })) - .expect("partial macos permissions response should deserialize"); - - assert_eq!( - response.permissions, - GrantedPermissionProfile { - macos: expected_granted_macos, - ..Default::default() - } - ); + }, + })) + .expect_err("permissions request should reject macos permissions"); - assert_eq!( - CorePermissionProfile::from(response.permissions), - CorePermissionProfile { - macos: expected_core_macos, - ..Default::default() - } - ); - } + assert!( + err.to_string().contains("unknown field `macos`"), + "unexpected error: {err}" + ); } #[test] - fn permissions_request_approval_response_omits_ungranted_macos_keys_when_serialized() { - let response = PermissionsRequestApprovalResponse { - permissions: GrantedPermissionProfile { - macos: Some(GrantedMacOsPermissions { - accessibility: Some(true), - ..Default::default() - }), - ..Default::default() + fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { + let response = serde_json::from_value::(json!({ + "permissions": { + "network": { + "enabled": true, + }, + "fileSystem": { + "read": ["/tmp/read-only"], + "write": ["/tmp/read-write"], + }, }, - scope: PermissionGrantScope::Turn, - }; + })) + .expect("permissions response should deserialize"); assert_eq!( - serde_json::to_value(response).expect("response should serialize"), - json!({ - "permissions": { - "macos": { - "accessibility": true, - }, - }, - "scope": "turn", - }) + response.permissions, + GrantedPermissionProfile { + network: Some(AdditionalNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(AdditionalFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + .expect("path must be absolute"), + ]), + }), + } + ); + + assert_eq!( + CorePermissionProfile::from(response.permissions), + CorePermissionProfile { + network: Some(CoreNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(CoreFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + .expect("path must be absolute"), + ]), + }), + macos: None, + } ); } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2a64d3cf7eb8..7b37dd782567 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -940,7 +940,7 @@ Order of messages: ### Permission requests -The built-in `request_permissions` tool sends an `item/permissions/requestApproval` JSON-RPC request to the client with the requested permission profile. Today that commonly means additional filesystem access, but the payload is intentionally general so future requests can include non-filesystem permissions too. This request is part of the v2 protocol surface. +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`. ```json { diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 4afd4cc2441d..0595110fa0a2 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -110,7 +110,6 @@ use codex_core::sandboxing::intersect_permission_profiles; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; -use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; @@ -775,7 +774,7 @@ pub(crate) async fn apply_bespoke_event_handling( turn_id: request.turn_id.clone(), item_id: request.call_id.clone(), reason: request.reason, - permissions: CorePermissionProfile::from(request.permissions).into(), + permissions: request.permissions.into(), }; let (pending_request_id, rx) = outgoing .send_request(ServerRequestPayload::PermissionsRequestApproval(params)) diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index 561a0fc741c3..5a0679415d0f 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -94,7 +94,6 @@ async fn request_permissions_round_trip() -> Result<()> { read: None, write: Some(vec![requested_writes[0].clone()]), }), - macos: None, }, scope: PermissionGrantScope::Turn, })?, From bdcc54d516a4f9555c490c69d044fcce221052c9 Mon Sep 17 00:00:00 2001 From: Jack Mousseau Date: Fri, 13 Mar 2026 22:18:42 -0700 Subject: [PATCH 2/2] Fix unit test file paths --- .../app-server-protocol/src/protocol/v2.rs | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 7314cb4c68a2..bc2e51d36c70 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -5963,6 +5963,16 @@ mod tests { #[test] fn permissions_request_approval_uses_request_permission_profile() { + let read_only_path = if cfg!(windows) { + r"C:\tmp\read-only" + } else { + "/tmp/read-only" + }; + let read_write_path = if cfg!(windows) { + r"C:\tmp\read-write" + } else { + "/tmp/read-write" + }; let params = serde_json::from_value::(json!({ "threadId": "thr_123", "turnId": "turn_123", @@ -5973,8 +5983,8 @@ mod tests { "enabled": true, }, "fileSystem": { - "read": ["/tmp/read-only"], - "write": ["/tmp/read-write"], + "read": [read_only_path], + "write": [read_write_path], }, }, })) @@ -5988,11 +5998,11 @@ mod tests { }), file_system: Some(AdditionalFileSystemPermissions { read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) .expect("path must be absolute"), ]), write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), }), @@ -6007,11 +6017,11 @@ mod tests { }), file_system: Some(CoreFileSystemPermissions { read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) .expect("path must be absolute"), ]), write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), }), @@ -6050,14 +6060,24 @@ mod tests { #[test] fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { + let read_only_path = if cfg!(windows) { + r"C:\tmp\read-only" + } else { + "/tmp/read-only" + }; + let read_write_path = if cfg!(windows) { + r"C:\tmp\read-write" + } else { + "/tmp/read-write" + }; let response = serde_json::from_value::(json!({ "permissions": { "network": { "enabled": true, }, "fileSystem": { - "read": ["/tmp/read-only"], - "write": ["/tmp/read-write"], + "read": [read_only_path], + "write": [read_write_path], }, }, })) @@ -6071,11 +6091,11 @@ mod tests { }), file_system: Some(AdditionalFileSystemPermissions { read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) .expect("path must be absolute"), ]), write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), }), @@ -6090,11 +6110,11 @@ mod tests { }), file_system: Some(CoreFileSystemPermissions { read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-only")) + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) .expect("path must be absolute"), ]), write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from("/tmp/read-write")) + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) .expect("path must be absolute"), ]), }),