Add request permissions tool (#13092)

Adds a built-in `request_permissions` tool and wires it through the
Codex core, protocol, and app-server layers so a running turn can ask
the client for additional permissions instead of relying on a static
session policy.

The new flow emits a `RequestPermissions` event from core, tracks the
pending request by call ID, forwards it through app-server v2 as an
`item/permissions/requestApproval` request, and resumes the tool call
once the client returns an approved subset of the requested permission
profile.
This commit is contained in:
Jack Mousseau
2026-03-08 20:23:06 -07:00
committed by GitHub
parent 4ad3b59de3
commit e6b93841c5
48 changed files with 3332 additions and 130 deletions
+1
View File
@@ -15,5 +15,6 @@ pub mod parse_command;
pub mod permissions;
pub mod plan_tool;
pub mod protocol;
pub mod request_permissions;
pub mod request_user_input;
pub mod user_input;
+28
View File
@@ -170,9 +170,13 @@ impl TryFrom<MacOsAutomationPermissionDe> for MacOsAutomationPermission {
#[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 = "accessibility")]
pub macos_accessibility: bool,
#[serde(alias = "calendar")]
pub macos_calendar: bool,
}
@@ -1504,6 +1508,30 @@ mod tests {
);
}
#[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"],
"accessibility": true,
"calendar": true
}))
.expect("deserialize macos permissions");
assert_eq!(
permissions,
MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
macos_automation: MacOsAutomationPermission::BundleIds(vec![
"com.apple.Notes".to_string(),
]),
macos_accessibility: true,
macos_calendar: true,
}
);
}
#[test]
fn macos_automation_permission_deserializes_all_and_none() {
let all = serde_json::from_str::<MacOsAutomationPermission>("\"all\"")
+13
View File
@@ -41,6 +41,8 @@ use crate::num_format::format_with_separators;
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crate::parse_command::ParsedCommand;
use crate::plan_tool::UpdatePlanArgs;
use crate::request_permissions::RequestPermissionsEvent;
use crate::request_permissions::RequestPermissionsResponse;
use crate::request_user_input::RequestUserInputResponse;
use crate::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -69,6 +71,7 @@ pub use crate::permissions::FileSystemSandboxKind;
pub use crate::permissions::FileSystemSandboxPolicy;
pub use crate::permissions::FileSystemSpecialPath;
pub use crate::permissions::NetworkSandboxPolicy;
pub use crate::request_permissions::RequestPermissionsArgs;
pub use crate::request_user_input::RequestUserInputEvent;
/// Open/close tags for special user-input blocks. Used across crates to avoid
@@ -356,6 +359,14 @@ pub enum Op {
response: RequestUserInputResponse,
},
/// Resolve a request_permissions tool call.
RequestPermissionsResponse {
/// Call id for the in-flight request.
id: String,
/// User-granted permissions.
response: RequestPermissionsResponse,
},
/// Resolve a dynamic tool call request.
DynamicToolResponse {
/// Call id for the in-flight request.
@@ -1144,6 +1155,8 @@ pub enum EventMsg {
ExecApprovalRequest(ExecApprovalRequestEvent),
RequestPermissions(RequestPermissionsEvent),
RequestUserInput(RequestUserInputEvent),
DynamicToolCallRequest(DynamicToolCallRequest),
@@ -0,0 +1,30 @@
use crate::models::PermissionProfile;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestPermissionsArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub permissions: PermissionProfile,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestPermissionsResponse {
pub permissions: PermissionProfile,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestPermissionsEvent {
/// Responses API call id for the associated tool call, if available.
pub call_id: String,
/// Turn ID that this request belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub permissions: PermissionProfile,
}