mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
docs(core): document conversational permissions
Explain the confirmation lifecycle for conversational sandbox presets and narrow permission grants across protocol, core, app-server, and TUI replay state. Call out where clients own confirmation, where core applies accepted responses, and how late or partial responses should be interpreted.
This commit is contained in:
@@ -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<CorePermissionPresetId> 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/")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Self> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -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<PermissionProfile> 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.
|
||||
|
||||
@@ -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<String>,
|
||||
exec_approval_call_ids_by_turn_id: HashMap<String, Vec<String>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user