diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index d709628ee5..2dd10592e0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -6212,6 +6212,11 @@ pub struct DynamicToolCallParams { pub arguments: JsonValue, } +/// Request sent to app clients when a model asks for a narrow permission grant. +/// +/// The client owns the confirmation UI and may return fewer permissions than +/// requested. Treat `suggested_scope` as the model's requested lifetime, not as +/// an already-approved scope. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -6234,6 +6239,11 @@ v2_enum_from_core!( } ); +/// Response from an app client after the narrow permission grant UI resolves. +/// +/// Returning an empty permission profile is a denial. Returning a non-empty +/// profile grants only those permissions, and core applies them for the +/// specified scope. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -6243,6 +6253,7 @@ pub struct PermissionsRequestApprovalResponse { pub scope: PermissionGrantScope, } +/// A built-in permission-mode preset that app clients can confirm. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "kebab-case")] #[ts(rename_all = "kebab-case", export_to = "v2/")] @@ -6254,6 +6265,7 @@ pub enum PermissionPresetId { } impl PermissionPresetId { + /// Converts the app-server preset identifier into the core protocol type. pub fn to_core(self) -> CorePermissionPresetId { match self { Self::Auto => CorePermissionPresetId::Auto, @@ -6275,6 +6287,11 @@ impl From for PermissionPresetId { } } +/// Request sent to app clients when a model asks to switch permission modes. +/// +/// Core resolves the requested preset before emitting this payload, so clients +/// should render the provided settings and label rather than recomputing policy +/// choices from the preset id. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -6298,6 +6315,7 @@ v2_enum_from_core!( } ); +/// Response from an app client after the permission preset picker resolves. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5cda87a37e..14d4a90f6e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3279,6 +3279,13 @@ impl Session { rx_approve } + /// Sends a narrow permission grant request to the active client and waits for the user's decision. + /// + /// The returned response contains the permissions the client says the user + /// granted, which may be empty or narrower than the requested profile. A + /// caller that treats the original request as granted would bypass the + /// confirmation contract and could run a later tool with permissions the + /// user declined. pub async fn request_permissions( &self, turn_context: &TurnContext, @@ -3335,6 +3342,13 @@ impl Session { rx_response.await.ok() } + /// Sends a permission preset request to the active client and waits for the user's decision. + /// + /// Core resolves and validates the preset before emitting the event so the + /// client confirms concrete settings rather than interpreting policy names. + /// The settings are not applied by this function; they become active only + /// when `notify_request_permission_preset_response` receives an accepted + /// response for the pending call id. pub async fn request_permission_preset( &self, turn_context: &TurnContext, @@ -3420,6 +3434,12 @@ impl Session { }) } + /// Applies an accepted permission preset response and completes the pending tool call. + /// + /// Responses for unknown call ids are logged and ignored. Accepted + /// responses are re-resolved against the current session before applying so + /// a stale client response cannot select a preset that is no longer + /// available. pub async fn notify_request_permission_preset_response( &self, call_id: &str, @@ -3660,6 +3680,12 @@ impl Session { } } + /// Records an approved narrow permission grant and completes the pending tool call. + /// + /// Turn-scoped grants are stored on the active turn, while session-scoped + /// grants are stored in session state after the turn lock is released. + /// Unknown call ids are ignored because they may be late replies from a + /// client prompt that was already resolved or abandoned. pub async fn notify_request_permissions_response( &self, call_id: &str, diff --git a/codex-rs/core/src/tools/handlers/request_permission_preset.rs b/codex-rs/core/src/tools/handlers/request_permission_preset.rs index 2778bb5274..f130e53b58 100644 --- a/codex-rs/core/src/tools/handlers/request_permission_preset.rs +++ b/codex-rs/core/src/tools/handlers/request_permission_preset.rs @@ -8,6 +8,7 @@ use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +/// Handles model requests to open the permission preset confirmation UI. pub struct RequestPermissionPresetHandler; impl ToolHandler for RequestPermissionPresetHandler { diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 28fc7e0f40..cb045d46ae 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -13,6 +13,7 @@ use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +/// Handles model requests to open the narrow permission confirmation UI. pub struct RequestPermissionsHandler; #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] @@ -30,6 +31,7 @@ struct RequestPermissionsToolOutput { message: &'static str, } +/// Builds the model-visible result after the user has resolved the confirmation UI. fn output_for_response(response: RequestPermissionsResponse) -> RequestPermissionsToolOutput { if response.permissions.is_empty() { RequestPermissionsToolOutput { diff --git a/codex-rs/protocol/src/request_permission_preset.rs b/codex-rs/protocol/src/request_permission_preset.rs index 15c05d8643..dd2f24a1e2 100644 --- a/codex-rs/protocol/src/request_permission_preset.rs +++ b/codex-rs/protocol/src/request_permission_preset.rs @@ -1,3 +1,10 @@ +//! Protocol types for conversational permission-mode preset requests. +//! +//! These messages represent the narrow bridge between a model tool call and a +//! client-owned confirmation UI. Core resolves a requested preset into concrete +//! sandbox, approval, and reviewer settings before emitting the event, and the +//! settings only become active after the client returns an accepted response. + use crate::config_types::ApprovalsReviewer; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; @@ -6,6 +13,7 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +/// A built-in permission-mode preset that can be requested conversationally. #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "kebab-case")] #[ts(rename_all = "kebab-case")] @@ -17,6 +25,7 @@ pub enum PermissionPresetId { } impl PermissionPresetId { + /// Returns the stable kebab-case identifier used in tool arguments and UI payloads. pub fn as_str(self) -> &'static str { match self { Self::Auto => "auto", @@ -26,6 +35,7 @@ impl PermissionPresetId { } } + /// Parses a stable preset identifier into a permission preset enum value. pub fn from_id(id: &str) -> Option { match id { "auto" => Some(Self::Auto), @@ -37,6 +47,7 @@ impl PermissionPresetId { } } +/// Arguments passed by the model when it asks the client to open the preset picker. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionPresetArgs { pub preset: PermissionPresetId, @@ -44,6 +55,7 @@ pub struct RequestPermissionPresetArgs { pub reason: Option, } +/// The user's completed decision from the permission preset confirmation UI. #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] pub enum RequestPermissionPresetDecision { @@ -51,6 +63,7 @@ pub enum RequestPermissionPresetDecision { Declined, } +/// The result returned to the model after the permission preset picker is resolved. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionPresetResponse { pub decision: RequestPermissionPresetDecision, @@ -58,6 +71,7 @@ pub struct RequestPermissionPresetResponse { pub message: String, } +/// Event sent from core to a client so the client can confirm a preset change. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionPresetEvent { /// Responses API call id for the associated tool call, if available. diff --git a/codex-rs/protocol/src/request_permissions.rs b/codex-rs/protocol/src/request_permissions.rs index 11fe2b3c4e..6894601687 100644 --- a/codex-rs/protocol/src/request_permissions.rs +++ b/codex-rs/protocol/src/request_permissions.rs @@ -1,3 +1,10 @@ +//! Protocol types for conversational requests to grant narrow permissions. +//! +//! These messages are scoped to concrete filesystem and network grants, not +//! broad sandbox-mode changes. The model asks for a specific permission profile, +//! core normalizes it, and the client returns the permissions the user actually +//! granted so partial approvals and denials stay explicit. + use crate::models::FileSystemPermissions; use crate::models::NetworkPermissions; use crate::models::PermissionProfile; @@ -6,6 +13,7 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +/// How long an approved narrow permission grant should remain active. #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] pub enum PermissionGrantScope { @@ -14,6 +22,7 @@ pub enum PermissionGrantScope { Session, } +/// A narrow permission profile that can be requested through the confirmation UI. #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(deny_unknown_fields)] pub struct RequestPermissionProfile { @@ -22,6 +31,7 @@ pub struct RequestPermissionProfile { } impl RequestPermissionProfile { + /// Returns true when no filesystem or network permission was requested or granted. pub fn is_empty(&self) -> bool { self.network.is_none() && self.file_system.is_none() } @@ -45,6 +55,7 @@ impl From for RequestPermissionProfile { } } +/// Arguments passed by the model when it asks the client to grant specific permissions. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionsArgs { #[serde(skip_serializing_if = "Option::is_none")] @@ -54,6 +65,7 @@ pub struct RequestPermissionsArgs { pub scope: PermissionGrantScope, } +/// The permissions the user granted after resolving the confirmation UI. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionsResponse { pub permissions: RequestPermissionProfile, @@ -61,6 +73,7 @@ pub struct RequestPermissionsResponse { pub scope: PermissionGrantScope, } +/// Event sent from core to a client so the client can confirm a narrow permission grant. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestPermissionsEvent { /// Responses API call id for the associated tool call, if available. diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index 5ac6fca238..911ae4528c 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -22,20 +22,19 @@ impl ElicitationRequestKey { } } +/// Tracks which interactive prompts are still unresolved in the thread-event buffer. +/// +/// Thread snapshots are replayed when switching threads or agents. Most events +/// should replay verbatim, but interactive prompts such as approvals, +/// `request_user_input`, MCP elicitations, permission grants, and permission +/// preset pickers must only replay while they are still pending. +/// +/// The state is updated from inbound events, outbound ops that resolve a prompt, +/// and buffer eviction. It keeps both fast lookup sets for snapshot filtering +/// and turn-indexed queues so `TurnComplete` and `TurnAborted` can clear stale +/// prompts tied to a turn. `request_user_input` removal is FIFO because the +/// overlay answers queued prompts in FIFO order for a shared `turn_id`. #[derive(Debug, Default)] -// Tracks which interactive prompts are still unresolved in the thread-event buffer. -// -// Thread snapshots are replayed when switching threads/agents. Most events should replay -// verbatim, but interactive prompts (approvals, request_user_input, MCP elicitations) must -// only replay if they are still pending. This state is updated from: -// - inbound events (`note_event`) -// - outbound ops that resolve a prompt (`note_outbound_op`) -// - buffer eviction (`note_evicted_event`) -// -// We keep both fast lookup sets (for snapshot filtering by call_id/request key) and -// turn-indexed queues/vectors so `TurnComplete`/`TurnAborted` can clear stale prompts tied -// to a turn. `request_user_input` removal is FIFO because the overlay answers queued prompts -// in FIFO order for a shared `turn_id`. pub(super) struct PendingInteractiveReplayState { exec_approval_call_ids: HashSet, exec_approval_call_ids_by_turn_id: HashMap>, diff --git a/codex-rs/utils/approval-presets/src/lib.rs b/codex-rs/utils/approval-presets/src/lib.rs index 525e6fdaf5..5b14a121cf 100644 --- a/codex-rs/utils/approval-presets/src/lib.rs +++ b/codex-rs/utils/approval-presets/src/lib.rs @@ -1,3 +1,9 @@ +//! Built-in permission presets shared by clients that render permission pickers. +//! +//! This crate keeps preset definitions independent from any single UI. Callers +//! decide which presets are available in their environment, then apply the +//! returned policies only after the relevant confirmation surface resolves. + use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; @@ -100,6 +106,11 @@ pub fn builtin_permission_presets( presets } +/// Finds a built-in permission preset by stable identifier. +/// +/// The availability flags must match the client surface that will display the +/// preset. Passing broader flags than the UI uses can let a model request a +/// preset that the user never had a chance to select. pub fn find_builtin_permission_preset( id: &str, include_read_only: bool,