From 5f03ec622b67e84b5e185fc1ae9b397a79e634b3 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 12 Apr 2026 21:37:59 -0300 Subject: [PATCH] feat(app-server): add permission request capabilities Add the `supportedServerRequests` initialize capability across the app-server protocol, clients, and connection session state. This lets surfaces explicitly advertise which conversational permission approval requests they know how to receive. Keep this branch limited to the opt-in contract and the bookkeeping needed to store that capability per connection. Delivery and UI handling stay in later stacked PRs so existing clients remain backward compatible and the review surface stays small. --- .../analytics/src/analytics_client_tests.rs | 2 + codex-rs/app-server-client/src/lib.rs | 47 +++++++++++++++++++ codex-rs/app-server-client/src/remote.rs | 7 +++ .../schema/json/ClientRequest.json | 18 +++++++ .../codex_app_server_protocol.schemas.json | 18 +++++++ .../codex_app_server_protocol.v2.schemas.json | 18 +++++++ .../schema/json/v1/InitializeParams.json | 18 +++++++ .../typescript/InitializeCapabilities.ts | 5 ++ .../SupportedServerRequestMethod.ts | 8 ++++ .../schema/typescript/index.ts | 1 + codex-rs/app-server-protocol/src/lib.rs | 1 + .../src/protocol/common.rs | 41 ++++++++++++++++ .../app-server-protocol/src/protocol/v1.rs | 14 ++++++ codex-rs/app-server-test-client/src/lib.rs | 1 + codex-rs/app-server/README.md | 10 +++- .../app-server/src/codex_message_processor.rs | 25 +++++++--- codex-rs/app-server/src/lib.rs | 10 +++- codex-rs/app-server/src/message_processor.rs | 44 ++++++++++++----- codex-rs/app-server/src/thread_state.rs | 22 ++++++--- .../tests/suite/v2/experimental_api.rs | 6 +++ .../app-server/tests/suite/v2/initialize.rs | 1 + .../tests/suite/v2/thread_status.rs | 1 + codex-rs/debug-client/src/client.rs | 1 + codex-rs/exec/src/lib.rs | 12 ----- codex-rs/tui/src/chatwidget.rs | 1 + codex-rs/tui/src/lib.rs | 16 +++++++ codex-rs/tui/src/onboarding/auth.rs | 1 + 27 files changed, 310 insertions(+), 39 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index e5aca39b8e..d8ab1eae68 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -500,6 +500,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize }, capabilities: Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), }, @@ -591,6 +592,7 @@ async fn compaction_event_ingests_custom_fact() { }, capabilities: Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), }, diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 4eadb0924d..f836a253fc 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -39,6 +39,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result as JsonRpcResult; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_core::config_loader::CloudRequirementsLoader; @@ -367,6 +368,8 @@ pub struct InProcessClientStartArgs { pub client_version: String, /// Whether experimental APIs are requested at initialize time. pub experimental_api: bool, + /// Server-initiated requests this client can render and answer. + pub supported_server_requests: Vec, /// Notification methods this client opts out of receiving. pub opt_out_notification_methods: Vec, /// Queue capacity for command/event channels (clamped to at least 1). @@ -378,6 +381,11 @@ impl InProcessClientStartArgs { pub fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, + supported_server_requests: if self.supported_server_requests.is_empty() { + None + } else { + Some(self.supported_server_requests.clone()) + }, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { @@ -988,6 +996,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, + supported_server_requests: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity, }) @@ -999,6 +1008,41 @@ mod tests { start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await } + #[tokio::test] + async fn in_process_initialize_params_include_supported_server_requests() { + let expected_supported = vec![ + SupportedServerRequestMethod::PermissionsRequestApproval, + SupportedServerRequestMethod::PermissionPresetRequestApproval, + ]; + let args = InProcessClientStartArgs { + arg0_paths: Arg0DispatchPaths::default(), + config: Arc::new(build_test_config().await), + cli_overrides: Vec::new(), + loader_overrides: LoaderOverrides::default(), + cloud_requirements: CloudRequirementsLoader::default(), + feedback: CodexFeedback::new(), + environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + config_warnings: Vec::new(), + session_source: SessionSource::Cli, + enable_codex_api_key_env: false, + client_name: "codex-app-server-client-test".to_string(), + client_version: "0.0.0-test".to_string(), + experimental_api: true, + supported_server_requests: expected_supported.clone(), + opt_out_notification_methods: Vec::new(), + channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, + }; + + let capabilities = args + .initialize_params() + .capabilities + .expect("capabilities should be populated"); + assert_eq!( + capabilities.supported_server_requests, + Some(expected_supported) + ); + } + async fn start_test_remote_server(handler: F) -> String where F: FnOnce(tokio_tungstenite::WebSocketStream) -> Fut @@ -1163,6 +1207,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, + supported_server_requests: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 8, } @@ -1607,6 +1652,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, + supported_server_requests: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 1, }) @@ -2000,6 +2046,7 @@ mod tests { client_name: "codex-app-server-client-test".to_string(), client_version: "0.0.0-test".to_string(), experimental_api: true, + supported_server_requests: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, } diff --git a/codex-rs/app-server-client/src/remote.rs b/codex-rs/app-server-client/src/remote.rs index 51015f8e7b..fe1e85274f 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -36,6 +36,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result as JsonRpcResult; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_utils_rustls_provider::ensure_rustls_crypto_provider; use futures::SinkExt; use futures::StreamExt; @@ -64,6 +65,7 @@ pub struct RemoteAppServerConnectArgs { pub client_name: String, pub client_version: String, pub experimental_api: bool, + pub supported_server_requests: Vec, pub opt_out_notification_methods: Vec, pub channel_capacity: usize, } @@ -72,6 +74,11 @@ impl RemoteAppServerConnectArgs { fn initialize_params(&self) -> InitializeParams { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, + supported_server_requests: if self.supported_server_requests.is_empty() { + None + } else { + Some(self.supported_server_requests.clone()) + }, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 76f7abdfd2..004da7c2e0 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1022,6 +1022,16 @@ "array", "null" ] + }, + "supportedServerRequests": { + "description": "Server request methods this connection knows how to render and answer.", + "items": { + "$ref": "#/definitions/SupportedServerRequestMethod" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" @@ -2569,6 +2579,14 @@ }, "type": "object" }, + "SupportedServerRequestMethod": { + "description": "Server-initiated request methods a client can opt into during initialize.", + "enum": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], + "type": "string" + }, "TextElement": { "properties": { "byteRange": { 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 15b31cc0ef..267c4fd5f3 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 @@ -2295,6 +2295,16 @@ "array", "null" ] + }, + "supportedServerRequests": { + "description": "Server request methods this connection knows how to render and answer.", + "items": { + "$ref": "#/definitions/SupportedServerRequestMethod" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" @@ -4721,6 +4731,14 @@ ], "title": "ServerRequest" }, + "SupportedServerRequestMethod": { + "description": "Server-initiated request methods a client can opt into during initialize.", + "enum": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], + "type": "string" + }, "ToolRequestUserInputAnswer": { "description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index dd053d77d2..a94e319772 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -5347,6 +5347,16 @@ "array", "null" ] + }, + "supportedServerRequests": { + "description": "Server request methods this connection knows how to render and answer.", + "items": { + "$ref": "#/definitions/SupportedServerRequestMethod" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" @@ -10257,6 +10267,14 @@ } ] }, + "SupportedServerRequestMethod": { + "description": "Server-initiated request methods a client can opt into during initialize.", + "enum": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], + "type": "string" + }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index 6048b82242..7d0da1e7a8 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -39,9 +39,27 @@ "array", "null" ] + }, + "supportedServerRequests": { + "description": "Server request methods this connection knows how to render and answer.", + "items": { + "$ref": "#/definitions/SupportedServerRequestMethod" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" + }, + "SupportedServerRequestMethod": { + "description": "Server-initiated request methods a client can opt into during initialize.", + "enum": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], + "type": "string" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 5d42cc4852..8cf667561e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -1,6 +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 { SupportedServerRequestMethod } from "./SupportedServerRequestMethod"; /** * Client-declared capabilities negotiated during initialize. @@ -10,6 +11,10 @@ export type InitializeCapabilities = { * Opt into receiving experimental API methods and fields. */ experimentalApi: boolean, +/** + * Server request methods this connection knows how to render and answer. + */ +supportedServerRequests?: Array | null, /** * Exact notification method names that should be suppressed for this * connection (for example `thread/started`). diff --git a/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts b/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts new file mode 100644 index 0000000000..6225ebd119 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SupportedServerRequestMethod.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Server-initiated request methods a client can opt into during initialize. + */ +export type SupportedServerRequestMethod = "item/permissions/requestApproval" | "item/permissionPreset/requestApproval"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 2a35207896..94b2a14526 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -67,6 +67,7 @@ export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; +export type { SupportedServerRequestMethod } from "./SupportedServerRequestMethod"; export type { ThreadId } from "./ThreadId"; export type { Tool } from "./Tool"; export type { Verbosity } from "./Verbosity"; diff --git a/codex-rs/app-server-protocol/src/lib.rs b/codex-rs/app-server-protocol/src/lib.rs index d5c2f4b243..ee48ff3a80 100644 --- a/codex-rs/app-server-protocol/src/lib.rs +++ b/codex-rs/app-server-protocol/src/lib.rs @@ -37,6 +37,7 @@ pub use protocol::v1::InterruptConversationResponse; pub use protocol::v1::LoginApiKeyParams; pub use protocol::v1::Profile; pub use protocol::v1::SandboxSettings; +pub use protocol::v1::SupportedServerRequestMethod; pub use protocol::v1::Tools; pub use protocol::v1::UserSavedConfig; pub use protocol::v2::*; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 3450e41534..378685c3c7 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1097,6 +1097,10 @@ mod tests { }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, + supported_server_requests: Some(vec![ + v1::SupportedServerRequestMethod::PermissionsRequestApproval, + v1::SupportedServerRequestMethod::PermissionPresetRequestApproval, + ]), opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -1117,6 +1121,10 @@ mod tests { }, "capabilities": { "experimentalApi": true, + "supportedServerRequests": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -1142,6 +1150,10 @@ mod tests { }, "capabilities": { "experimentalApi": true, + "supportedServerRequests": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -1162,6 +1174,10 @@ mod tests { }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, + supported_server_requests: Some(vec![ + v1::SupportedServerRequestMethod::PermissionsRequestApproval, + v1::SupportedServerRequestMethod::PermissionPresetRequestApproval, + ]), opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -1173,6 +1189,31 @@ mod tests { Ok(()) } + #[test] + fn deserialize_initialize_without_supported_server_requests() -> Result<()> { + let request: ClientRequest = serde_json::from_value(json!({ + "method": "initialize", + "id": 42, + "params": { + "clientInfo": { + "name": "codex_vscode", + "title": "Codex VS Code Extension", + "version": "0.1.0" + }, + "capabilities": { + "experimentalApi": true + } + } + }))?; + + let ClientRequest::Initialize { params, .. } = request else { + panic!("expected initialize request"); + }; + let capabilities = params.capabilities.expect("capabilities"); + assert_eq!(capabilities.supported_server_requests, None); + Ok(()) + } + #[test] fn conversation_id_serializes_as_plain_string() -> Result<()> { let id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 6aa2e9fa30..f7d5c313bd 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -46,12 +46,26 @@ pub struct InitializeCapabilities { /// Opt into receiving experimental API methods and fields. #[serde(default)] pub experimental_api: bool, + /// Server request methods this connection knows how to render and answer. + #[ts(optional = nullable)] + pub supported_server_requests: Option>, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). #[ts(optional = nullable)] pub opt_out_notification_methods: Option>, } +/// Server-initiated request methods a client can opt into during initialize. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)] +pub enum SupportedServerRequestMethod { + #[serde(rename = "item/permissions/requestApproval")] + #[ts(rename = "item/permissions/requestApproval")] + PermissionsRequestApproval, + #[serde(rename = "item/permissionPreset/requestApproval")] + #[ts(rename = "item/permissionPreset/requestApproval")] + PermissionPresetRequestApproval, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeResponse { diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index dd548012a0..c3a1e89800 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1559,6 +1559,7 @@ impl CodexClient { .map(|method| (*method).to_string()) .collect(), ), + supported_server_requests: None, }), }, }; diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 94f86c7ebb..a325e2e458 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -89,6 +89,8 @@ Clients must send a single `initialize` request per transport connection before `initialize.params.capabilities` also supports per-connection notification opt-out via `optOutNotificationMethods`, which is a list of exact method names to suppress for that connection. Matching is exact (no wildcards/prefixes). Unknown method names are accepted and ignored. +Clients can also advertise `supportedServerRequests` during initialize. This is a per-connection opt-in list of server-initiated request methods the surface knows how to render and answer. App-server uses it for conversational permission confirmation flows so existing clients keep their current behavior until they explicitly opt in. + Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter. **Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If @@ -111,7 +113,7 @@ Example (from OpenAI's official VSCode extension): } ``` -Example with notification opt-out: +Example with notification opt-out and explicit server-request support: ```json { @@ -125,6 +127,10 @@ Example with notification opt-out: }, "capabilities": { "experimentalApi": true, + "supportedServerRequests": [ + "item/permissions/requestApproval", + "item/permissionPreset/requestApproval" + ], "optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"] } } @@ -1071,6 +1077,8 @@ the client can offer session-scoped and/or persistent approval choices. ### Permission requests +Clients must opt into conversational permission confirmation request methods by listing them in `initialize.params.capabilities.supportedServerRequests`. The opt-in is per connection and per method, which lets older clients continue to initialize without promising UI they do not implement. + 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 diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6762b9e129..a613539586 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -117,6 +117,7 @@ use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; @@ -3738,9 +3739,13 @@ impl CodexMessageProcessor { self.thread_manager.subscribe_thread_created() } - pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) { + pub(crate) async fn connection_initialized( + &self, + connection_id: ConnectionId, + supported_server_requests: HashSet, + ) { self.thread_state_manager - .connection_initialized(connection_id) + .connection_initialized(connection_id, supported_server_requests) .await; } @@ -9917,7 +9922,9 @@ mod tests { let connection = ConnectionId(1); let (cancel_tx, cancel_rx) = oneshot::channel(); - manager.connection_initialized(connection).await; + manager + .connection_initialized(connection, HashSet::new()) + .await; manager .try_ensure_connection_subscribed( thread_id, connection, /*experimental_raw_events*/ false, @@ -9963,8 +9970,12 @@ mod tests { let connection_b = ConnectionId(2); let (cancel_tx, mut cancel_rx) = oneshot::channel(); - manager.connection_initialized(connection_a).await; - manager.connection_initialized(connection_b).await; + manager + .connection_initialized(connection_a, HashSet::new()) + .await; + manager + .connection_initialized(connection_b, HashSet::new()) + .await; manager .try_ensure_connection_subscribed( thread_id, @@ -10007,7 +10018,9 @@ mod tests { let thread_id = ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?; let connection = ConnectionId(1); - manager.connection_initialized(connection).await; + manager + .connection_initialized(connection, HashSet::new()) + .await; let threads_to_unload = manager.remove_connection(connection).await; assert_eq!(threads_to_unload, Vec::::new()); diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index bfc87251ad..2b4f95bc57 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -797,7 +797,15 @@ pub async fn run_main_with_transport( connection_id, ) .await; - processor.connection_initialized(connection_id).await; + processor + .connection_initialized( + connection_id, + connection_state + .session + .supported_server_requests + .clone(), + ) + .await; connection_state .outbound_initialized .store(true, std::sync::atomic::Ordering::Release); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 8c2bba00a3..027a959a33 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -56,6 +56,7 @@ use codex_app_server_protocol::JSONRPCRequest; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::experimental_required_message; use codex_arg0::Arg0DispatchPaths; use codex_chatgpt::connectors; @@ -179,6 +180,7 @@ pub(crate) struct ConnectionSessionState { pub(crate) initialized: bool, pub(crate) experimental_api_enabled: bool, pub(crate) opted_out_notification_methods: HashSet, + pub(crate) supported_server_requests: HashSet, pub(crate) app_server_client_name: Option, pub(crate) client_version: Option, } @@ -456,9 +458,13 @@ impl MessageProcessor { } } - pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) { + pub(crate) async fn connection_initialized( + &self, + connection_id: ConnectionId, + supported_server_requests: HashSet, + ) { self.codex_message_processor - .connection_initialized(connection_id) + .connection_initialized(connection_id, supported_server_requests) .await; } @@ -561,19 +567,28 @@ impl MessageProcessor { // experimental API). Proposed direction is instance-global first-write-wins // with initialize-time mismatch rejection. let analytics_initialize_params = params.clone(); - let (experimental_api_enabled, opt_out_notification_methods) = - match params.capabilities { - Some(capabilities) => ( - capabilities.experimental_api, - capabilities - .opt_out_notification_methods - .unwrap_or_default(), - ), - None => (false, Vec::new()), - }; + let ( + experimental_api_enabled, + opt_out_notification_methods, + supported_server_requests, + ) = match params.capabilities { + Some(capabilities) => ( + capabilities.experimental_api, + capabilities + .opt_out_notification_methods + .unwrap_or_default(), + capabilities + .supported_server_requests + .unwrap_or_default() + .into_iter() + .collect(), + ), + None => (false, Vec::new(), HashSet::new()), + }; session.experimental_api_enabled = experimental_api_enabled; session.opted_out_notification_methods = opt_out_notification_methods.into_iter().collect(); + session.supported_server_requests = supported_server_requests; let ClientInfo { name, title: _title, @@ -649,7 +664,10 @@ impl MessageProcessor { // initialize handling for the specific connection. outbound_initialized.store(true, Ordering::Release); self.codex_message_processor - .connection_initialized(connection_id) + .connection_initialized( + connection_id, + session.supported_server_requests.clone(), + ) .await; } return; diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 11d6ad6bb3..99983c5003 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -1,6 +1,7 @@ use crate::outgoing_message::ConnectionId; use crate::outgoing_message::ConnectionRequestId; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::ThreadHistoryBuilder; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError; @@ -173,6 +174,8 @@ impl Default for ThreadEntry { #[derive(Default)] struct ThreadStateManagerInner { live_connections: HashSet, + supported_server_requests_by_connection: + HashMap>, threads: HashMap, thread_ids_by_connection: HashMap>, } @@ -187,12 +190,16 @@ impl ThreadStateManager { Self::default() } - pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) { - self.state - .lock() - .await - .live_connections - .insert(connection_id); + pub(crate) async fn connection_initialized( + &self, + connection_id: ConnectionId, + supported_server_requests: HashSet, + ) { + let mut state = self.state.lock().await; + state.live_connections.insert(connection_id); + state + .supported_server_requests_by_connection + .insert(connection_id, supported_server_requests); } pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec { @@ -357,6 +364,9 @@ impl ThreadStateManager { { let mut state = self.state.lock().await; state.live_connections.remove(&connection_id); + state + .supported_server_requests_by_connection + .remove(&connection_id); let thread_ids = state .thread_ids_by_connection .remove(&connection_id) diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 4a532aebc0..c1458adee0 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -33,6 +33,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) @@ -63,6 +64,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) @@ -99,6 +101,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) @@ -139,6 +142,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) @@ -176,6 +180,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) @@ -212,6 +217,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil default_client_info(), Some(InitializeCapabilities { experimental_api: false, + supported_server_requests: None, opt_out_notification_methods: None, }), ) diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 165160468f..2aad489548 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -158,6 +158,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu }, Some(InitializeCapabilities { experimental_api: true, + supported_server_requests: None, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), }), ), diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index ad90e4900a..7dabce9db5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -145,6 +145,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { }, Some(InitializeCapabilities { experimental_api: true, + supported_server_requests: None, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), }), ), diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 2ada10e377..0f1c113b27 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -103,6 +103,7 @@ impl AppServerClient { }, capabilities: Some(InitializeCapabilities { experimental_api: true, + supported_server_requests: None, opt_out_notification_methods: None, }), }, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 5c0797b14d..78f319acc1 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1510,18 +1510,6 @@ async fn handle_server_request( ) .await } - ServerRequest::PermissionPresetRequestApproval { request_id, params } => { - reject_server_request( - client, - request_id, - &method, - format!( - "permission preset approval is not supported in exec mode for thread `{}`", - params.thread_id - ), - ) - .await - } }; if let Err(err) = handle_result { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 31b2c97e7b..3cd0dba4b2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6692,6 +6692,7 @@ impl ChatWidget { EventMsg::RequestPermissions(ev) => { self.on_request_permissions(ev); } + EventMsg::RequestPermissionPreset(_) => {} EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev), EventMsg::TerminalInteraction(delta) => self.on_terminal_interaction(delta), EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 1e3b58452b..86fbfeb4d2 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -33,6 +33,7 @@ use codex_app_server_client::RemoteAppServerConnectArgs; use codex_app_server_protocol::Account as AppServerAccount; use codex_app_server_protocol::AuthMode as AppServerAuthMode; use codex_app_server_protocol::ConfigWarningNotification; +use codex_app_server_protocol::SupportedServerRequestMethod; use codex_app_server_protocol::Thread as AppServerThread; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey; @@ -351,6 +352,7 @@ async fn connect_remote_app_server( client_name: "codex-tui".to_string(), client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, + supported_server_requests: tui_supported_server_requests(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) @@ -459,6 +461,7 @@ where client_name: "codex-tui".to_string(), client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, + supported_server_requests: tui_supported_server_requests(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) @@ -467,6 +470,10 @@ where Ok(client) } +fn tui_supported_server_requests() -> Vec { + vec![SupportedServerRequestMethod::PermissionsRequestApproval] +} + async fn shutdown_app_server_if_present(app_server: Option) { if let Some(app_server) = app_server && let Err(err) = app_server.shutdown().await @@ -1814,6 +1821,15 @@ mod tests { } } + #[test] + fn tui_advertises_permission_confirmation_server_requests() { + let supported = tui_supported_server_requests(); + assert_eq!( + supported, + vec![SupportedServerRequestMethod::PermissionsRequestApproval] + ); + } + #[test] fn normalize_remote_addr_rejects_invalid_input() { let err = normalize_remote_addr("https://127.0.0.1:4500") diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 57bd58bf5a..a108e9b539 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -997,6 +997,7 @@ mod tests { client_name: "test".to_string(), client_version: "test".to_string(), experimental_api: true, + supported_server_requests: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, })