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, })