From ca39621beef9e9fe0cd2ebac20103250590ab21a Mon Sep 17 00:00:00 2001 From: Casey Chow Date: Fri, 10 Jul 2026 01:05:17 +0000 Subject: [PATCH] refactor(plugins): split host capability projection base --- codex-rs/Cargo.lock | 1 - .../analytics/src/analytics_client_tests.rs | 4 - codex-rs/app-server-client/src/lib.rs | 40 +-- codex-rs/app-server-client/src/remote.rs | 3 - .../schema/json/ClientRequest.json | 10 - .../codex_app_server_protocol.schemas.json | 10 - .../codex_app_server_protocol.v2.schemas.json | 10 - .../schema/json/v1/InitializeParams.json | 10 - .../typescript/InitializeCapabilities.ts | 4 - .../src/protocol/common.rs | 4 - .../app-server-protocol/src/protocol/v1.rs | 3 - codex-rs/app-server-test-client/src/lib.rs | 1 - codex-rs/app-server/README.md | 16 +- codex-rs/app-server/src/extensions.rs | 1 - codex-rs/app-server/src/in_process.rs | 11 +- codex-rs/app-server/src/lib.rs | 9 +- codex-rs/app-server/src/message_processor.rs | 114 ++---- codex-rs/app-server/src/request_processors.rs | 22 -- .../src/request_processors/apps_processor.rs | 63 +--- .../request_processors/catalog_processor.rs | 16 +- .../initialize_processor.rs | 10 - .../src/request_processors/mcp_processor.rs | 83 +---- .../src/request_processors/plugins.rs | 219 ++++------- .../request_processors/thread_processor.rs | 182 +++++----- .../thread_processor_tests.rs | 1 - .../src/request_processors/turn_processor.rs | 147 +++----- .../app-server/tests/suite/v2/attestation.rs | 1 - .../suite/v2/connection_handling_websocket.rs | 339 +----------------- .../tests/suite/v2/experimental_api.rs | 8 - .../app-server/tests/suite/v2/initialize.rs | 1 - .../tests/suite/v2/thread_status.rs | 1 - .../core-plugins/src/discoverable_tests.rs | 4 +- codex-rs/core-plugins/src/loader.rs | 7 +- codex-rs/core-plugins/src/manager.rs | 12 +- codex-rs/core-plugins/src/manager_tests.rs | 101 ++++-- codex-rs/core-plugins/src/manifest.rs | 26 +- codex-rs/core-plugins/src/remote.rs | 19 + codex-rs/core-plugins/src/remote_tests.rs | 23 ++ .../core-plugins/src/tool_suggest_metadata.rs | 22 +- codex-rs/core-skills/src/service.rs | 31 +- codex-rs/core-skills/src/service_tests.rs | 62 ---- codex-rs/core/src/agent/role.rs | 1 - codex-rs/core/src/agent/role_tests.rs | 5 - codex-rs/core/src/codex_thread.rs | 2 - codex-rs/core/src/config/config_tests.rs | 12 +- codex-rs/core/src/config/mod.rs | 21 +- codex-rs/core/src/connectors.rs | 2 - codex-rs/core/src/connectors_tests.rs | 10 - codex-rs/core/src/session/session.rs | 1 - codex-rs/core/tests/suite/plugins.rs | 104 ------ codex-rs/exec/src/lib.rs | 2 - codex-rs/ext/mcp/src/executor_plugin.rs | 14 - .../mcp/src/executor_plugin/provider_tests.rs | 2 - codex-rs/ext/mcp/tests/executor_plugin_mcp.rs | 98 ----- codex-rs/ext/skills/Cargo.toml | 1 - codex-rs/ext/skills/src/catalog.rs | 50 --- codex-rs/ext/skills/src/config.rs | 4 - codex-rs/ext/skills/src/extension.rs | 3 - codex-rs/ext/skills/src/provider.rs | 2 - codex-rs/ext/skills/src/provider/executor.rs | 43 +-- codex-rs/ext/skills/src/provider/host.rs | 1 - codex-rs/ext/skills/src/state.rs | 21 +- codex-rs/ext/skills/src/tools/mod.rs | 1 - .../tests/executor_file_system_authority.rs | 74 ---- codex-rs/ext/skills/tests/skills_extension.rs | 199 ---------- codex-rs/plugin/src/host_capabilities.rs | 47 +++ .../plugin/src/host_capabilities_tests.rs | 14 + codex-rs/plugin/src/lib.rs | 2 + codex-rs/plugin/src/load_outcome.rs | 22 +- codex-rs/plugin/src/manifest.rs | 20 +- codex-rs/plugin/src/provider_tests.rs | 5 +- codex-rs/thread-manager-sample/src/main.rs | 1 - codex-rs/tui/src/lib.rs | 2 - codex-rs/tui/src/onboarding/auth.rs | 1 - 74 files changed, 546 insertions(+), 1892 deletions(-) create mode 100644 codex-rs/plugin/src/host_capabilities.rs create mode 100644 codex-rs/plugin/src/host_capabilities_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 0035922a72..db4350e1f9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3990,7 +3990,6 @@ dependencies = [ name = "codex-skills-extension" version = "0.0.0" dependencies = [ - "codex-core-plugins", "codex-core-skills", "codex-exec-server", "codex-extension-api", diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 861366be0e..7b878e952c 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -801,7 +801,6 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact { request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), }, product_client_id: DEFAULT_ORIGINATOR.to_string(), @@ -1683,7 +1682,6 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), }, product_client_id: DEFAULT_ORIGINATOR.to_string(), @@ -1988,7 +1986,6 @@ async fn compaction_event_ingests_custom_fact() { request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), }, product_client_id: DEFAULT_ORIGINATOR.to_string(), @@ -2119,7 +2116,6 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), }, product_client_id: DEFAULT_ORIGINATOR.to_string(), diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index a49c16ba71..4eec8a2348 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -349,8 +349,6 @@ pub struct InProcessClientStartArgs { pub experimental_api: bool, /// Whether MCP servers may send `openai/form` elicitation requests. pub mcp_server_openai_form_elicitation: bool, - /// Host features available to runtime plugins. - pub host_capabilities: 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). @@ -376,8 +374,6 @@ impl InProcessClientStartArgs { Some(self.opt_out_notification_methods.clone()) }, mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation, - host_capabilities: (!self.host_capabilities.is_empty()) - .then(|| self.host_capabilities.clone()), }; InitializeParams { @@ -1049,7 +1045,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity, }) @@ -1244,7 +1239,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 8, } @@ -1263,20 +1257,6 @@ mod tests { ); } - #[test] - fn remote_initialize_params_forward_host_capabilities() { - let mut args = test_remote_connect_args("ws://localhost/rpc".to_string()); - args.host_capabilities = vec!["codex.inline_visualization".to_string()]; - - assert_eq!( - args.initialize_params() - .capabilities - .expect("initialize capabilities") - .host_capabilities, - Some(vec!["codex.inline_visualization".to_string()]) - ); - } - #[tokio::test] async fn typed_request_roundtrip_works() { let client = start_test_client(SessionSource::Exec).await; @@ -1548,7 +1528,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 8, }) @@ -1638,7 +1617,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 8, }) @@ -1659,7 +1637,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: 8, }) @@ -2264,22 +2241,18 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: true, - host_capabilities: vec!["codex.inline_visualization".to_string()], opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, } .into_runtime_start_args(); assert_eq!(runtime_args.config, config); - let initialize_capabilities = runtime_args - .initialize - .capabilities - .as_ref() - .expect("initialize capabilities"); - assert!(initialize_capabilities.mcp_server_openai_form_elicitation); - assert_eq!( - initialize_capabilities.host_capabilities, - Some(vec!["codex.inline_visualization".to_string()]) + assert!( + runtime_args + .initialize + .capabilities + .expect("initialize capabilities") + .mcp_server_openai_form_elicitation ); assert!(Arc::ptr_eq( &runtime_args.environment_manager, @@ -2317,7 +2290,6 @@ mod tests { client_version: "0.0.0-test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: 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 40796986ba..5575fe4ad4 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -87,7 +87,6 @@ pub struct RemoteAppServerConnectArgs { pub client_version: String, pub experimental_api: bool, pub mcp_server_openai_form_elicitation: bool, - pub host_capabilities: Vec, pub opt_out_notification_methods: Vec, pub channel_capacity: usize, } @@ -102,8 +101,6 @@ impl RemoteAppServerConnectArgs { Some(self.opt_out_notification_methods.clone()) }, mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation, - host_capabilities: (!self.host_capabilities.is_empty()) - .then(|| self.host_capabilities.clone()), }; InitializeParams { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index e59bad5b95..5da27a074c 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1279,16 +1279,6 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, - "hostCapabilities": { - "description": "Host features available to runtime plugins on this connection.", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "mcpServerOpenaiFormElicitation": { "description": "Allow downstream MCP servers to request OpenAI extended form elicitations.", "type": "boolean" 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 71adf424f8..a0f893b7f8 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 @@ -2923,16 +2923,6 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, - "hostCapabilities": { - "description": "Host features available to runtime plugins on this connection.", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "mcpServerOpenaiFormElicitation": { "description": "Allow downstream MCP servers to request OpenAI extended form elicitations.", "type": "boolean" 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 fc361afaab..0fe34feff3 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 @@ -7532,16 +7532,6 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, - "hostCapabilities": { - "description": "Host features available to runtime plugins on this connection.", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "mcpServerOpenaiFormElicitation": { "description": "Allow downstream MCP servers to request OpenAI extended form elicitations.", "type": "boolean" 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 3dbeddf00c..75f0860ddd 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -30,16 +30,6 @@ "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" }, - "hostCapabilities": { - "description": "Host features available to runtime plugins on this connection.", - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "mcpServerOpenaiFormElicitation": { "description": "Allow downstream MCP servers to request OpenAI extended form elicitations.", "type": "boolean" diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 9f42d4bf1e..dcc4dffb0b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -18,10 +18,6 @@ requestAttestation: boolean, * Allow downstream MCP servers to request OpenAI extended form elicitations. */ mcpServerOpenaiFormElicitation?: boolean, -/** - * Host features available to runtime plugins on this connection. - */ -hostCapabilities?: 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/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 0ea8615e7a..bf56f7f28a 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2209,7 +2209,6 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, - host_capabilities: Some(vec!["codex.inline_visualization".to_string()]), opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -2232,7 +2231,6 @@ mod tests { "experimentalApi": true, "requestAttestation": true, "mcpServerOpenaiFormElicitation": true, - "hostCapabilities": ["codex.inline_visualization"], "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -2260,7 +2258,6 @@ mod tests { "experimentalApi": true, "requestAttestation": true, "mcpServerOpenaiFormElicitation": true, - "hostCapabilities": ["codex.inline_visualization"], "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -2283,7 +2280,6 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, - host_capabilities: Some(vec!["codex.inline_visualization".to_string()]), opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index db2648f35d..dccea51a36 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -53,9 +53,6 @@ pub struct InitializeCapabilities { /// Allow downstream MCP servers to request OpenAI extended form elicitations. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub mcp_server_openai_form_elicitation: bool, - /// Host features available to runtime plugins on this connection. - #[ts(optional = nullable)] - pub host_capabilities: Option>, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). #[ts(optional = nullable)] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index c75be28b16..0e32365d12 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1671,7 +1671,6 @@ impl CodexClient { .collect(), ), mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), }, }; diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 40b4bdcfa9..6280cf5204 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -93,20 +93,6 @@ App-server then advertises the downstream `openai/form` MCP extension for threads started, resumed, or forked by that connection. Clients that cannot handle the request envelope omit the field or set it to `false`. -Clients declare host features available to runtime plugins with -`initialize.params.capabilities.hostCapabilities`. Capability names are opaque -compatibility signals, not authorization grants; omitted or `null` -capabilities are treated as an empty set. The capability set is fixed for the -lifetime of a loaded thread, so clients must use the same set when resuming or -performing model-visible runtime operations on that thread. Capability-independent -settings updates remain available; the next `turn/start` enforces compatibility. - -Plugin manifests declare all-of host requirements with -`{"requires":{"hostCapabilities":["codex.inline_visualization"]}}`. A plugin -whose requirements are not satisfied remains installed and manageable, but its -skills, MCP servers, apps, hooks, and model-facing summaries are excluded from -that runtime. - 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 @@ -1836,7 +1822,7 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat } } ``` -When `threadId` is provided, app feature gating (`Feature::Apps`) is evaluated using that thread's config snapshot, and the requesting connection must have the same host capability set as the loaded thread. When omitted, the latest global config is used. +When `threadId` is provided, app feature gating (`Feature::Apps`) is evaluated using that thread's config snapshot. When omitted, the latest global config is used. `app/list` returns after both accessible apps and directory apps are loaded. Set `forceRefetch: true` to bypass app caches and fetch fresh data from sources. Cache entries are only replaced when those refetches succeed. diff --git a/codex-rs/app-server/src/extensions.rs b/codex-rs/app-server/src/extensions.rs index c4949ee544..ed040aa8d9 100644 --- a/codex-rs/app-server/src/extensions.rs +++ b/codex-rs/app-server/src/extensions.rs @@ -91,7 +91,6 @@ where include_instructions: config.include_skill_instructions, bundled_skills_enabled: config.bundled_skills_enabled(), orchestrator_skills_enabled: config.orchestrator_skills_enabled, - host_capabilities: config.host_capabilities.clone(), }, ); Arc::new(builder.build()) diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 114e28c7a3..d8a2bc2276 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -501,16 +501,13 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult { match created { Ok(thread_id) => { - let connections = if session.initialized() { - vec![( - IN_PROCESS_CONNECTION_ID, - session.host_capabilities(), - )] + let connection_ids = if session.initialized() { + vec![IN_PROCESS_CONNECTION_ID] } else { - Vec::new() + Vec::::new() }; processor - .try_attach_thread_listener(thread_id, connections) + .try_attach_thread_listener(thread_id, connection_ids) .await; } Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index cb28365246..91da0c1645 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -1125,19 +1125,16 @@ pub async fn run_main_with_transport_options( created = thread_created_rx.recv(), if listen_for_threads => { match created { Ok(thread_id) => { - let mut initialized_connections = Vec::new(); + let mut initialized_connection_ids = Vec::new(); for (connection_id, connection_state) in &connections { if connection_state.session.initialized() { - initialized_connections.push(( - *connection_id, - connection_state.session.host_capabilities(), - )); + initialized_connection_ids.push(*connection_id); } } processor .try_attach_thread_listener( thread_id, - initialized_connections, + initialized_connection_ids, ) .await; } diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index bd4a6d5c56..62eff72660 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -19,7 +19,6 @@ use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::RequestContext; use crate::request_processors::AccountRequestProcessor; -use crate::request_processors::AppServerClientInfo; use crate::request_processors::AppsRequestProcessor; use crate::request_processors::CatalogRequestProcessor; use crate::request_processors::CommandExecRequestProcessor; @@ -66,7 +65,6 @@ use codex_arg0::Arg0DispatchPaths; use codex_chatgpt::workspace_settings; use codex_core::ThreadManager; use codex_core::config::Config; -use codex_core::config::HostCapabilities; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; use codex_goal_extension::GoalService; @@ -143,7 +141,6 @@ pub(crate) struct InitializedConnectionSessionState { pub(crate) client_version: String, pub(crate) request_attestation: bool, pub(crate) supports_openai_form_elicitation: bool, - pub(crate) host_capabilities: HostCapabilities, } impl Default for ConnectionSessionState { @@ -200,13 +197,6 @@ impl ConnectionSessionState { .get() .is_some_and(|session| session.supports_openai_form_elicitation) } - - pub(crate) fn host_capabilities(&self) -> HostCapabilities { - self.initialized - .get() - .map(|session| session.host_capabilities.clone()) - .unwrap_or_default() - } pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> { self.initialized.set(session).map_err(|_| ()) } @@ -678,10 +668,10 @@ impl MessageProcessor { pub(crate) async fn try_attach_thread_listener( &self, thread_id: ThreadId, - connections: Vec<(ConnectionId, HostCapabilities)>, + connection_ids: Vec, ) { self.thread_processor - .try_attach_thread_listener(thread_id, connections) + .try_attach_thread_listener(thread_id, connection_ids) .await; } @@ -818,12 +808,9 @@ impl MessageProcessor { ); let serialization_scope = codex_request.serialization_scope(); - let app_server_client_info = AppServerClientInfo { - name: session.app_server_client_name().map(str::to_string), - version: session.client_version().map(str::to_string), - }; + let app_server_client_name = session.app_server_client_name().map(str::to_string); + let client_version = session.client_version().map(str::to_string); let supports_openai_form_elicitation = session.supports_openai_form_elicitation(); - let host_capabilities = session.host_capabilities(); let error_request_id = connection_request_id.clone(); let rpc_gate = Arc::clone(&session.rpc_gate); let processor = Arc::clone(self); @@ -837,9 +824,9 @@ impl MessageProcessor { connection_request_id, codex_request, request_context, - app_server_client_info, + app_server_client_name, + client_version, supports_openai_form_elicitation, - host_capabilities, ) .await; if let Err(error) = result { @@ -867,9 +854,9 @@ impl MessageProcessor { connection_request_id: ConnectionRequestId, codex_request: ClientRequest, request_context: RequestContext, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let connection_id = connection_request_id.connection_id; let request_id = ConnectionRequestId { @@ -921,7 +908,7 @@ impl MessageProcessor { .remote_control_processor .enable( params.is_some_and(|params| params.ephemeral), - app_server_client_info.name.as_deref(), + app_server_client_name.as_deref(), ) .await .map(|response| Some(response.into())), @@ -929,7 +916,7 @@ impl MessageProcessor { .remote_control_processor .disable( params.is_some_and(|params| params.ephemeral), - app_server_client_info.name.as_deref(), + app_server_client_name.as_deref(), ) .await .map(|response| Some(response.into())), @@ -939,7 +926,7 @@ impl MessageProcessor { .map(|response| Some(response.into())), ClientRequest::RemoteControlPairingStart { params, .. } => self .remote_control_processor - .pairing_start(params, app_server_client_info.name.as_deref()) + .pairing_start(params, app_server_client_name.as_deref()) .await .map(|response| Some(response.into())), ClientRequest::RemoteControlPairingStatus { params, .. } => self @@ -1023,9 +1010,9 @@ impl MessageProcessor { .thread_start( request_id.clone(), params, - app_server_client_info.clone(), + app_server_client_name.clone(), + client_version.clone(), supports_openai_form_elicitation, - host_capabilities.clone(), request_context, ) .await @@ -1040,10 +1027,10 @@ impl MessageProcessor { .thread_resume( request_id.clone(), params, - app_server_client_info.clone(), + app_server_client_name.clone(), + client_version.clone(), /*supports_openai_form_elicitation*/ supports_openai_form_elicitation, - host_capabilities.clone(), ) .await } @@ -1052,10 +1039,10 @@ impl MessageProcessor { .thread_fork( request_id.clone(), params, - app_server_client_info.clone(), + app_server_client_name.clone(), + client_version.clone(), /*supports_openai_form_elicitation*/ supports_openai_form_elicitation, - host_capabilities.clone(), ) .await } @@ -1116,7 +1103,7 @@ impl MessageProcessor { } ClientRequest::ThreadCompactStart { params, .. } => { self.thread_processor - .thread_compact_start(&request_id, params, host_capabilities.clone()) + .thread_compact_start(&request_id, params) .await } ClientRequest::ThreadBackgroundTerminalsClean { params, .. } => { @@ -1136,12 +1123,7 @@ impl MessageProcessor { } ClientRequest::ThreadRollback { params, .. } => { self.thread_processor - .thread_rollback( - &request_id, - params, - app_server_client_info.name.as_deref(), - host_capabilities.clone(), - ) + .thread_rollback(&request_id, params, app_server_client_name.as_deref()) .await } ClientRequest::ThreadList { params, .. } => { @@ -1176,17 +1158,13 @@ impl MessageProcessor { self.thread_processor.conversation_summary(params).await } ClientRequest::SkillsList { params, .. } => { - self.catalog_processor - .skills_list(params, host_capabilities.clone()) - .await + self.catalog_processor.skills_list(params).await } ClientRequest::SkillsExtraRootsSet { params, .. } => { self.catalog_processor.skills_extra_roots_set(params).await } ClientRequest::HooksList { params, .. } => { - self.catalog_processor - .hooks_list(params, host_capabilities.clone()) - .await + self.catalog_processor.hooks_list(params).await } ClientRequest::MarketplaceAdd { params, .. } => { self.marketplace_processor.marketplace_add(params).await @@ -1227,17 +1205,13 @@ impl MessageProcessor { self.plugin_processor.plugin_share_delete(params).await } ClientRequest::AppsList { params, .. } => { - self.apps_processor - .apps_list(&request_id, params, host_capabilities.clone()) - .await + self.apps_processor.apps_list(&request_id, params).await } ClientRequest::SkillsConfigWrite { params, .. } => { self.catalog_processor.skills_config_write(params).await } ClientRequest::PluginInstall { params, .. } => { - self.plugin_processor - .plugin_install(&request_id, params, host_capabilities.clone()) - .await + self.plugin_processor.plugin_install(params).await } ClientRequest::PluginUninstall { params, .. } => { self.plugin_processor.plugin_uninstall(params).await @@ -1266,84 +1240,74 @@ impl MessageProcessor { .turn_start( request_id.clone(), params, - app_server_client_info, + app_server_client_name.clone(), + client_version.clone(), /*supports_openai_form_elicitation*/ supports_openai_form_elicitation, - host_capabilities, ) .await } ClientRequest::ThreadInjectItems { params, .. } => { - self.turn_processor - .thread_inject_items(params, host_capabilities.clone()) - .await + self.turn_processor.thread_inject_items(params).await } ClientRequest::TurnSteer { params, .. } => { - self.turn_processor - .turn_steer(&request_id, params, host_capabilities.clone()) - .await + self.turn_processor.turn_steer(&request_id, params).await } ClientRequest::TurnInterrupt { params, .. } => { self.turn_processor - .turn_interrupt(&request_id, params, host_capabilities.clone()) + .turn_interrupt(&request_id, params) .await } ClientRequest::ThreadRealtimeStart { params, .. } => { self.turn_processor - .thread_realtime_start(&request_id, params, host_capabilities.clone()) + .thread_realtime_start(&request_id, params) .await } ClientRequest::ThreadRealtimeAppendAudio { params, .. } => { self.turn_processor - .thread_realtime_append_audio(&request_id, params, host_capabilities.clone()) + .thread_realtime_append_audio(&request_id, params) .await } ClientRequest::ThreadRealtimeAppendText { params, .. } => { self.turn_processor - .thread_realtime_append_text(&request_id, params, host_capabilities.clone()) + .thread_realtime_append_text(&request_id, params) .await } ClientRequest::ThreadRealtimeAppendSpeech { params, .. } => { self.turn_processor - .thread_realtime_append_speech(&request_id, params, host_capabilities.clone()) + .thread_realtime_append_speech(&request_id, params) .await } ClientRequest::ThreadRealtimeStop { params, .. } => { self.turn_processor - .thread_realtime_stop(&request_id, params, host_capabilities.clone()) + .thread_realtime_stop(&request_id, params) .await } ClientRequest::ThreadRealtimeListVoices { params: _, .. } => { self.turn_processor.thread_realtime_list_voices().await } ClientRequest::ReviewStart { params, .. } => { - self.turn_processor - .review_start(&request_id, params, host_capabilities.clone()) - .await + self.turn_processor.review_start(&request_id, params).await } ClientRequest::McpServerOauthLogin { params, .. } => { - self.mcp_processor - .mcp_server_oauth_login(&request_id, params, host_capabilities.clone()) - .await + self.mcp_processor.mcp_server_oauth_login(params).await } ClientRequest::McpServerRefresh { params, .. } => { - self.mcp_processor - .mcp_server_refresh(params, host_capabilities.clone()) - .await + self.mcp_processor.mcp_server_refresh(params).await } ClientRequest::McpServerStatusList { params, .. } => { self.mcp_processor - .mcp_server_status_list(&request_id, params, host_capabilities.clone()) + .mcp_server_status_list(&request_id, params) .await } ClientRequest::McpResourceRead { params, .. } => { self.mcp_processor - .mcp_resource_read(&request_id, params, host_capabilities.clone()) + .mcp_resource_read(&request_id, params) .await } ClientRequest::McpServerToolCall { params, .. } => { self.mcp_processor - .mcp_server_tool_call(&request_id, params, host_capabilities) + .mcp_server_tool_call(&request_id, params) .await } ClientRequest::WindowsSandboxSetupStart { params, .. } => { diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index ad9616d8d1..da88d0e067 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -326,7 +326,6 @@ use codex_core::ThreadConfigSnapshot; use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config::ConfigOverrides; -use codex_core::config::HostCapabilities; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; @@ -496,12 +495,6 @@ use uuid::Uuid; #[cfg(test)] use codex_app_server_protocol::ServerRequest; -#[derive(Clone, Debug, Default)] -pub(crate) struct AppServerClientInfo { - pub(crate) name: Option, - pub(crate) version: Option, -} - mod account_processor; mod apps_processor; mod catalog_processor; @@ -608,21 +601,6 @@ fn resolve_runtime_workspace_roots(workspace_roots: Vec) -> Vec resolved_roots } -async fn ensure_thread_host_capabilities( - thread_id: ThreadId, - thread: &CodexThread, - requested: &HostCapabilities, - operation: &str, -) -> Result<(), JSONRPCErrorError> { - let active = thread.config_snapshot().await.host_capabilities; - if &active != requested { - return Err(invalid_request(format!( - "cannot {operation} on thread {thread_id} with different host capabilities: requested={requested:?} active={active:?}" - ))); - } - Ok(()) -} - mod config_errors; mod request_errors; mod thread_delete; diff --git a/codex-rs/app-server/src/request_processors/apps_processor.rs b/codex-rs/app-server/src/request_processors/apps_processor.rs index 190d68f0bd..5ff7ec40db 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor.rs @@ -36,9 +36,8 @@ impl AppsRequestProcessor { &self, request_id: &ConnectionRequestId, params: AppsListParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.apps_list_inner(request_id, params, host_capabilities) + self.apps_list_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -47,19 +46,11 @@ impl AppsRequestProcessor { &self, request_id: &ConnectionRequestId, params: AppsListParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let installed_start = Instant::now(); let reload = params.force_refetch; let thread = if let Some(thread_id) = params.thread_id.as_deref() { - let (thread_id, loaded_thread) = self.load_thread(thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - loaded_thread.as_ref(), - &host_capabilities, - "list apps", - ) - .await?; + let (_, loaded_thread) = self.load_thread(thread_id).await?; Some(loaded_thread) } else { None @@ -69,7 +60,6 @@ impl AppsRequestProcessor { None => None, }; let mut config = self.load_latest_config(fallback_cwd).await?; - config.host_capabilities = host_capabilities; if let Some(thread) = thread { let _ = config @@ -141,7 +131,6 @@ impl AppsRequestProcessor { plugins_manager: Arc, installed_start: Instant, ) { - let connection_id = request_id.connection_id; let reload = params.force_refetch; let retry_params = params.clone(); let retry_config = config.clone(); @@ -150,7 +139,6 @@ impl AppsRequestProcessor { let retry_plugins_manager = Arc::clone(&plugins_manager); let result = Self::apps_list_response( &outgoing, - connection_id, params, config, environment_manager, @@ -173,7 +161,6 @@ impl AppsRequestProcessor { retry_params.force_refetch = true; if let Err(err) = Self::apps_list_response( &outgoing, - connection_id, retry_params, retry_config, retry_environment_manager, @@ -189,7 +176,6 @@ impl AppsRequestProcessor { async fn apps_list_response( outgoing: &Arc, - connection_id: ConnectionId, params: AppsListParams, config: Config, environment_manager: Arc, @@ -269,7 +255,7 @@ impl AppsRequestProcessor { accessible_loaded, all_loaded, ) { - send_app_list_updated_notification(outgoing, connection_id, merged.clone()).await; + send_app_list_updated_notification(outgoing, merged.clone()).await; last_notified_apps = Some(merged); } } @@ -329,7 +315,7 @@ impl AppsRequestProcessor { all_loaded, ) && last_notified_apps.as_ref() != Some(&merged) { - send_app_list_updated_notification(outgoing, connection_id, merged.clone()).await; + send_app_list_updated_notification(outgoing, merged.clone()).await; last_notified_apps = Some(merged.clone()); } @@ -458,47 +444,12 @@ fn paginate_apps( async fn send_app_list_updated_notification( outgoing: &Arc, - connection_id: ConnectionId, data: Vec, ) { let data = data.into_iter().map(app_info_to_api).collect(); outgoing - .send_server_notification_to_connections( - &[connection_id], - ServerNotification::AppListUpdated(AppListUpdatedNotification { data }), - ) + .send_server_notification(ServerNotification::AppListUpdated( + AppListUpdatedNotification { data }, + )) .await; } - -#[cfg(test)] -mod tests { - use super::*; - use crate::outgoing_message::OutgoingEnvelope; - use crate::outgoing_message::OutgoingMessage; - - #[tokio::test] - async fn app_list_updated_notification_targets_requesting_connection() { - let (tx, mut rx) = tokio::sync::mpsc::channel(1); - let outgoing = Arc::new(OutgoingMessageSender::new( - tx, - codex_analytics::AnalyticsEventsClient::disabled(), - )); - - send_app_list_updated_notification(&outgoing, ConnectionId(42), Vec::new()).await; - - let envelope = rx.recv().await.expect("notification should be sent"); - let OutgoingEnvelope::ToConnection { - connection_id, - message, - .. - } = envelope - else { - panic!("app list update should not be broadcast"); - }; - assert_eq!(connection_id, ConnectionId(42)); - assert!(matches!( - message, - OutgoingMessage::AppServerNotification(ServerNotification::AppListUpdated(_)) - )); - } -} diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index a5e2031c47..6cb4385151 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -120,9 +120,8 @@ impl CatalogRequestProcessor { pub(crate) async fn skills_list( &self, params: SkillsListParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.skills_list_response(params, host_capabilities) + self.skills_list_response(params) .await .map(|response| Some(response.into())) } @@ -130,9 +129,8 @@ impl CatalogRequestProcessor { pub(crate) async fn hooks_list( &self, params: HooksListParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.hooks_list_response(params, host_capabilities) + self.hooks_list_response(params) .await .map(|response| Some(response.into())) } @@ -490,7 +488,6 @@ impl CatalogRequestProcessor { async fn skills_list_response( &self, params: SkillsListParams, - host_capabilities: HostCapabilities, ) -> Result { let SkillsListParams { cwds, force_reload } = params; let cwds = if cwds.is_empty() { @@ -499,8 +496,7 @@ impl CatalogRequestProcessor { cwds }; - let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?; - config.host_capabilities = host_capabilities; + let config = self.load_latest_config(/*fallback_cwd*/ None).await?; let auth = self.auth_manager.auth().await; let workspace_codex_plugins_enabled = self .workspace_codex_plugins_enabled(&config, auth.as_ref()) @@ -599,7 +595,6 @@ impl CatalogRequestProcessor { async fn hooks_list_response( &self, params: HooksListParams, - host_capabilities: HostCapabilities, ) -> Result { let HooksListParams { cwds } = params; let cwds = if cwds.is_empty() { @@ -616,10 +611,7 @@ impl CatalogRequestProcessor { .config_manager .load_for_cwd( /*request_overrides*/ None, - ConfigOverrides { - host_capabilities: host_capabilities.clone(), - ..Default::default() - }, + ConfigOverrides::default(), Some(cwd.clone()), ) .await diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index 9e1f8661c0..cfdad27f50 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -71,15 +71,6 @@ impl InitializeRequestProcessor { let experimental_api_enabled = capabilities.experimental_api; let request_attestation = capabilities.request_attestation; let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation; - let host_capabilities = capabilities - .host_capabilities - .unwrap_or_default() - .into_iter() - .filter_map(|capability| { - let capability = capability.trim(); - (!capability.is_empty()).then(|| capability.to_string()) - }) - .collect(); let opt_out_notification_methods = capabilities .opt_out_notification_methods .unwrap_or_default(); @@ -107,7 +98,6 @@ impl InitializeRequestProcessor { client_version: version, request_attestation, supports_openai_form_elicitation, - host_capabilities, }) .is_err() { diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 62007dcf4c..ac2125c2cf 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -27,11 +27,9 @@ impl McpRequestProcessor { pub(crate) async fn mcp_server_oauth_login( &self, - request_id: &ConnectionRequestId, params: McpServerOauthLoginParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.mcp_server_oauth_login_response(request_id, params, host_capabilities) + self.mcp_server_oauth_login_response(params) .await .map(|response| Some(response.into())) } @@ -39,9 +37,8 @@ impl McpRequestProcessor { pub(crate) async fn mcp_server_refresh( &self, params: Option<()>, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.mcp_server_refresh_response(params, host_capabilities) + self.mcp_server_refresh_response(params) .await .map(|response| Some(response.into())) } @@ -50,9 +47,8 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: ListMcpServerStatusParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.list_mcp_server_status(request_id, params, host_capabilities) + self.list_mcp_server_status(request_id, params) .await .map(|()| None) } @@ -61,9 +57,8 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpResourceReadParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.read_mcp_resource(request_id, params, host_capabilities) + self.read_mcp_resource(request_id, params) .await .map(|()| None) } @@ -72,9 +67,8 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpServerToolCallParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.call_mcp_server_tool(request_id, params, host_capabilities) + self.call_mcp_server_tool(request_id, params) .await .map(|()| None) } @@ -82,7 +76,6 @@ impl McpRequestProcessor { async fn mcp_server_refresh_response( &self, _params: Option<()>, - _host_capabilities: HostCapabilities, ) -> Result { crate::mcp_refresh::queue_strict_refresh(&self.thread_manager, &self.config_manager) .await @@ -118,9 +111,7 @@ impl McpRequestProcessor { async fn mcp_server_oauth_login_response( &self, - request_id: &ConnectionRequestId, params: McpServerOauthLoginParams, - host_capabilities: HostCapabilities, ) -> Result { let McpServerOauthLoginParams { name, @@ -132,20 +123,12 @@ impl McpRequestProcessor { let auth = self.auth_manager.auth().await; let (mcp_config, runtime_context) = match thread_id.as_deref() { Some(thread_id) => { - let (thread_id, thread) = self.load_thread(thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "start MCP OAuth login", - ) - .await?; + let (_, thread) = self.load_thread(thread_id).await?; let runtime = thread.current_mcp_runtime().await; (runtime.config().clone(), runtime.runtime_context().clone()) } None => { - let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?; - config.host_capabilities = host_capabilities; + let config = self.load_latest_config(/*fallback_cwd*/ None).await?; let mcp_config = self .thread_manager .mcp_manager() @@ -217,7 +200,6 @@ impl McpRequestProcessor { let authorization_url = handle.authorization_url().to_string(); let notification_name = name.clone(); let notification_thread_id = thread_id; - let notification_connection_id = request_id.connection_id; let outgoing = Arc::clone(&self.outgoing); tokio::spawn(async move { @@ -234,12 +216,7 @@ impl McpRequestProcessor { error, }, ); - outgoing - .send_server_notification_to_connections( - &[notification_connection_id], - notification, - ) - .await; + outgoing.send_server_notification(notification).await; }); Ok(McpServerOauthLoginResponse { authorization_url }) @@ -249,35 +226,22 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: ListMcpServerStatusParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let request = request_id.clone(); let outgoing = Arc::clone(&self.outgoing); let (config, thread) = match params.thread_id.as_deref() { Some(thread_id) => { - let (thread_id, thread) = self.load_thread(thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "list MCP server status", - ) - .await?; + let (_, thread) = self.load_thread(thread_id).await?; let thread_config = thread.config().await; - let mut config = self + let config = self .config_manager .load_latest_config_for_thread(thread_config.as_ref()) .await .map_err(|err| internal_error(format!("failed to reload config: {err}")))?; - config.host_capabilities = host_capabilities.clone(); (config, Some(thread)) } - None => { - let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?; - config.host_capabilities = host_capabilities; - (config, None) - } + None => (self.load_latest_config(/*fallback_cwd*/ None).await?, None), }; let mcp_manager = self.thread_manager.mcp_manager(); let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache(); @@ -423,7 +387,6 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpResourceReadParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let outgoing = Arc::clone(&self.outgoing); let McpResourceReadParams { @@ -433,14 +396,7 @@ impl McpRequestProcessor { } = params; if let Some(thread_id) = thread_id { - let (thread_id, thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "read an MCP resource", - ) - .await?; + let (_, thread) = self.load_thread(&thread_id).await?; let request_id = request_id.clone(); tokio::spawn(async move { @@ -450,8 +406,7 @@ impl McpRequestProcessor { return Ok(()); } - let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?; - config.host_capabilities = host_capabilities; + let config = self.load_latest_config(/*fallback_cwd*/ None).await?; let mcp_manager = self.thread_manager.mcp_manager(); let mcp_config = mcp_manager.runtime_config(&config).await; let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache(); @@ -501,19 +456,11 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpServerToolCallParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let outgoing = Arc::clone(&self.outgoing); let thread_id = params.thread_id.clone(); - let (thread_id, thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "call an MCP tool", - ) - .await?; - let meta = with_mcp_tool_call_thread_id_meta(params.meta, ¶ms.thread_id); + let (_, thread) = self.load_thread(&thread_id).await?; + let meta = with_mcp_tool_call_thread_id_meta(params.meta, &thread_id); let request_id = request_id.clone(); tokio::spawn(async move { diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 53c3e74e58..688cfae917 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -23,7 +23,6 @@ use codex_core_plugins::remote_bundle::RemotePluginBundleInstallError; use codex_mcp::McpOAuthLoginSupport; use codex_mcp::oauth_login_support; use codex_mcp::should_retry_without_scopes; -use codex_plugin::AppConnectorId; use codex_plugin::PluginId; use codex_plugin::PluginTelemetryMetadata; use codex_protocol::auth::AuthMode as DomainAuthMode; @@ -89,39 +88,6 @@ fn local_plugin_interface_to_info(interface: PluginManifestInterface) -> PluginI } } -async fn load_plugin_auth_capabilities_for_host( - config: &Config, - plugin_root: &Path, - auth_mode: Option, -) -> (HashMap, Vec) { - if !plugin_auth_side_effects_available_for_host(plugin_root, &config.host_capabilities) { - return Default::default(); - } - - let (mcp_servers, app_declarations) = tokio::join!( - load_plugin_mcp_servers(plugin_root, auth_mode), - load_plugin_apps(plugin_root), - ); - ( - mcp_servers, - codex_plugin::app_connector_ids_from_declarations(&app_declarations), - ) -} - -fn plugin_auth_side_effects_available_for_host( - plugin_root: &Path, - host_capabilities: &HostCapabilities, -) -> bool { - let Some(manifest) = codex_core_plugins::manifest::load_plugin_manifest(plugin_root) else { - return false; - }; - !manifest - .requires - .host_capabilities - .iter() - .any(|capability| !host_capabilities.contains(capability)) -} - fn marketplace_plugin_source_to_info(source: MarketplacePluginSource) -> PluginSource { match source { MarketplacePluginSource::Local { path } => PluginSource::Local { path }, @@ -486,11 +452,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_install( &self, - request_id: &ConnectionRequestId, params: PluginInstallParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.plugin_install_response(request_id, params, host_capabilities) + self.plugin_install_response(params) .await .map(|response| Some(response.into())) } @@ -1458,9 +1422,7 @@ impl PluginRequestProcessor { async fn plugin_install_response( &self, - request_id: &ConnectionRequestId, params: PluginInstallParams, - host_capabilities: HostCapabilities, ) -> Result { let PluginInstallParams { marketplace_path, @@ -1471,12 +1433,7 @@ impl PluginRequestProcessor { (Some(marketplace_path), None) => marketplace_path, (None, Some(remote_marketplace_name)) => { return self - .remote_plugin_install_response( - request_id, - remote_marketplace_name, - plugin_name, - host_capabilities, - ) + .remote_plugin_install_response(remote_marketplace_name, plugin_name) .await; } (Some(_), Some(_)) | (None, None) => { @@ -1486,8 +1443,7 @@ impl PluginRequestProcessor { } }; let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); - let mut config = self.load_latest_config(config_cwd.clone()).await?; - config.host_capabilities = host_capabilities.clone(); + let config = self.load_latest_config(config_cwd.clone()).await?; let auth = self.auth_manager.auth().await; if !self @@ -1521,7 +1477,7 @@ impl PluginRequestProcessor { return Err(Self::plugin_install_error(err)); } }; - let mut config = match self.load_latest_config(config_cwd).await { + let config = match self.load_latest_config(config_cwd).await { Ok(config) => config, Err(err) => { warn!( @@ -1530,25 +1486,22 @@ impl PluginRequestProcessor { config } }; - config.host_capabilities = host_capabilities; self.on_effective_plugins_changed(); - let (plugin_mcp_servers, plugin_apps) = load_plugin_auth_capabilities_for_host( - &config, + let plugin_mcp_servers = load_plugin_mcp_servers( result.installed_path.as_path(), auth.as_ref().map(CodexAuth::auth_mode), ) .await; if !plugin_mcp_servers.is_empty() { - self.start_plugin_mcp_oauth_logins( - request_id.connection_id, - &config, - plugin_mcp_servers, - ) - .await; + self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers) + .await; } + let plugin_app_declarations = load_plugin_apps(result.installed_path.as_path()).await; + let plugin_apps = + codex_plugin::app_connector_ids_from_declarations(&plugin_app_declarations); let apps_needing_auth = self .plugin_apps_needing_auth_for_install( &config, @@ -1566,13 +1519,10 @@ impl PluginRequestProcessor { async fn remote_plugin_install_response( &self, - request_id: &ConnectionRequestId, remote_marketplace_name: String, remote_plugin_id: String, - host_capabilities: HostCapabilities, ) -> Result { - let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?; - config.host_capabilities = host_capabilities; + let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if !config.features.enabled(Feature::Plugins) { return Err(invalid_request(format!( "remote plugin install is not enabled for marketplace {remote_marketplace_name}" @@ -1716,72 +1666,66 @@ impl PluginRequestProcessor { self.analytics_events_client .track_plugin_installed(plugin_metadata); - let auth_side_effects_available = plugin_auth_side_effects_available_for_host( - result.installed_path.as_path(), - &config.host_capabilities, - ); - let (plugin_mcp_servers, plugin_apps) = load_plugin_auth_capabilities_for_host( - &config, + let plugin_mcp_servers = load_plugin_mcp_servers( result.installed_path.as_path(), auth.as_ref().map(CodexAuth::auth_mode), ) .await; if !plugin_mcp_servers.is_empty() { - self.start_plugin_mcp_oauth_logins( - request_id.connection_id, - &config, - plugin_mcp_servers, - ) - .await; + self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers) + .await; } let is_chatgpt_auth = auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth); - let apps_needing_auth = - if let Some(app_ids_needing_auth) = install_result.app_ids_needing_auth { - if app_ids_needing_auth.is_empty() - || !config.features.apps_enabled_for_auth(is_chatgpt_auth) - || !auth_side_effects_available - { - Vec::new() - } else { - let plugin_apps = app_ids_needing_auth - .into_iter() - .map(AppConnectorId) - .collect::>(); - let app_category_by_id = remote_detail - .app_manifest - .as_ref() - .map(plugin_app_category_by_id_from_value) - .unwrap_or_default(); - let all_connectors = connectors::list_cached_all_connectors(&config, &[]) - .await - .unwrap_or_default(); - connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps) - .into_iter() - .map(|connector| { - let category = app_category_by_id - .get(&connector.id) - .cloned() - .or_else(|| connector.category()); - AppSummary { - category, - id: connector.id, - name: connector.name, - description: connector.description, - install_url: connector.install_url, - } - }) - .collect() - } + let apps_needing_auth = if let Some(app_ids_needing_auth) = + install_result.app_ids_needing_auth + { + if app_ids_needing_auth.is_empty() + || !config.features.apps_enabled_for_auth(is_chatgpt_auth) + { + Vec::new() } else { - self.plugin_apps_needing_auth_for_install( - &config, - is_chatgpt_auth, - &result.plugin_id.as_key(), - &plugin_apps, - ) - .await - }; + let plugin_apps = app_ids_needing_auth + .into_iter() + .map(codex_plugin::AppConnectorId) + .collect::>(); + let app_category_by_id = remote_detail + .app_manifest + .as_ref() + .map(plugin_app_category_by_id_from_value) + .unwrap_or_default(); + let all_connectors = connectors::list_cached_all_connectors(&config, &[]) + .await + .unwrap_or_default(); + connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps) + .into_iter() + .map(|connector| { + let category = app_category_by_id + .get(&connector.id) + .cloned() + .or_else(|| connector.category()); + AppSummary { + category, + id: connector.id, + name: connector.name, + description: connector.description, + install_url: connector.install_url, + } + }) + .collect() + } + } else { + let plugin_app_declarations = load_plugin_apps(result.installed_path.as_path()).await; + let plugin_apps = + codex_plugin::app_connector_ids_from_declarations(&plugin_app_declarations); + self.plugin_apps_needing_auth_for_install( + &config, + is_chatgpt_auth, + &result.plugin_id.as_key(), + &plugin_apps, + ) + .await + }; Ok(PluginInstallResponse { auth_policy: remote_detail.summary.auth_policy, @@ -1891,7 +1835,6 @@ impl PluginRequestProcessor { async fn start_plugin_mcp_oauth_logins( &self, - connection_id: ConnectionId, config: &Config, plugin_mcp_servers: HashMap, ) { @@ -1970,9 +1913,7 @@ impl PluginRequestProcessor { error, }, ); - outgoing - .send_server_notification_to_connections(&[connection_id], notification) - .await; + outgoing.send_server_notification(notification).await; }); } } @@ -2457,37 +2398,3 @@ fn remote_plugin_bundle_install_error_to_jsonrpc( ) -> JSONRPCErrorError { internal_error(format!("install remote plugin bundle: {err}")) } - -#[cfg(test)] -mod host_capability_tests { - use super::*; - - #[test] - fn post_install_auth_side_effects_require_host_capabilities() { - let plugin_root = tempfile::tempdir().expect("temp plugin root"); - let manifest_dir = plugin_root.path().join(".codex-plugin"); - std::fs::create_dir_all(&manifest_dir).expect("create manifest directory"); - std::fs::write( - manifest_dir.join("plugin.json"), - r#"{ - "name": "gated-auth-plugin", - "requires": { - "hostCapabilities": ["codex.inline_visualization"] - } - }"#, - ) - .expect("write plugin manifest"); - - let mut host_capabilities = HostCapabilities::new(); - assert!(!plugin_auth_side_effects_available_for_host( - plugin_root.path(), - &host_capabilities, - )); - - host_capabilities.insert("codex.inline_visualization".to_string()); - assert!(plugin_auth_side_effects_available_for_host( - plugin_root.path(), - &host_capabilities, - )); - } -} diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 8fdd26ada9..fc6a3281b4 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -451,17 +451,17 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadStartParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, request_context: RequestContext, ) -> Result, JSONRPCErrorError> { self.thread_start_inner( request_id, params, - app_server_client_info, + app_server_client_name, + app_server_client_version, supports_openai_form_elicitation, - host_capabilities, request_context, ) .await @@ -482,16 +482,16 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadResumeParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { self.thread_resume_inner( request_id, params, - app_server_client_info, + app_server_client_name, + app_server_client_version, supports_openai_form_elicitation, - host_capabilities, ) .await .map(|()| None) @@ -501,16 +501,16 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadForkParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { self.thread_fork_inner( request_id, params, - app_server_client_info, + app_server_client_name, + app_server_client_version, supports_openai_form_elicitation, - host_capabilities, ) .await .map(|()| None) @@ -629,9 +629,8 @@ impl ThreadRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadCompactStartParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_compact_start_inner(request_id, params, host_capabilities) + self.thread_compact_start_inner(request_id, params) .await .map(|response| Some(response.into())) } @@ -669,13 +668,12 @@ impl ThreadRequestProcessor { request_id: &ConnectionRequestId, params: ThreadRollbackParams, app_server_client_name: Option<&str>, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { if app_server_client_name != Some(CODEX_TUI_CLIENT_NAME) { self.send_thread_rollback_deprecation_notice(request_id.connection_id) .await; } - self.thread_rollback_inner(request_id, params, host_capabilities) + self.thread_rollback_inner(request_id, params) .await .map(|()| None) } @@ -804,13 +802,19 @@ impl ThreadRequestProcessor { async fn set_app_server_client_info( thread: &CodexThread, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, ) -> Result<(), JSONRPCErrorError> { - let AppServerClientInfo { name, version } = app_server_client_info; - let mcp_elicitations_auto_deny = - xcode_26_4_mcp_elicitations_auto_deny(name.as_deref(), version.as_deref()); + let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny( + app_server_client_name.as_deref(), + app_server_client_version.as_deref(), + ); thread - .set_app_server_client_info(name, version, mcp_elicitations_auto_deny) + .set_app_server_client_info( + app_server_client_name, + app_server_client_version, + mcp_elicitations_auto_deny, + ) .await .map_err(|err| internal_error(format!("failed to set app server client info: {err}"))) } @@ -927,9 +931,9 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadStartParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, request_context: RequestContext, ) -> Result<(), JSONRPCErrorError> { let ThreadStartParams { @@ -981,7 +985,6 @@ impl ThreadRequestProcessor { developer_instructions, personality, ); - typesafe_overrides.host_capabilities = host_capabilities; typesafe_overrides.ephemeral = ephemeral; let listener_task_context = ListenerTaskContext { thread_manager: Arc::clone(&self.thread_manager), @@ -1004,7 +1007,8 @@ impl ThreadRequestProcessor { listener_task_context, config_manager, request_id, - app_server_client_info, + app_server_client_name, + app_server_client_version, supports_openai_form_elicitation, config, typesafe_overrides, @@ -1080,7 +1084,8 @@ impl ThreadRequestProcessor { listener_task_context: ListenerTaskContext, config_manager: ConfigManager, request_id: ConnectionRequestId, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, config_overrides: Option>, typesafe_overrides: ConfigOverrides, @@ -1249,7 +1254,12 @@ impl ThreadRequestProcessor { Some("ready"), ); - Self::set_app_server_client_info(thread.as_ref(), app_server_client_info).await?; + Self::set_app_server_client_info( + thread.as_ref(), + app_server_client_name, + app_server_client_version, + ) + .await?; let instruction_sources = thread.legacy_instruction_sources().await; let config_snapshot = thread @@ -1751,17 +1761,14 @@ impl ThreadRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRollbackParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { - self.thread_rollback_start(request_id, params, host_capabilities) - .await + self.thread_rollback_start(request_id, params).await } async fn thread_rollback_start( &self, request_id: &ConnectionRequestId, params: ThreadRollbackParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let ThreadRollbackParams { thread_id, @@ -1773,13 +1780,6 @@ impl ThreadRequestProcessor { } let (thread_id, thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "roll back", - ) - .await?; let request = request_id.clone(); @@ -1821,18 +1821,10 @@ impl ThreadRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadCompactStartParams, - host_capabilities: HostCapabilities, ) -> Result { let ThreadCompactStartParams { thread_id } = params; - let (thread_id, thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "start compaction", - ) - .await?; + let (_, thread) = self.load_thread(&thread_id).await?; self.submit_core_op(request_id, thread.as_ref(), Op::Compact) .await .map_err(|err| internal_error(format!("failed to start compaction: {err}")))?; @@ -2635,35 +2627,30 @@ impl ThreadRequestProcessor { pub(crate) async fn try_attach_thread_listener( &self, thread_id: ThreadId, - connections: Vec<(ConnectionId, HostCapabilities)>, + connection_ids: Vec, ) { - let Ok(thread) = self.thread_manager.get_thread(thread_id).await else { - return; - }; - let config_snapshot = thread.config_snapshot().await; - let thread_host_capabilities = config_snapshot.host_capabilities.clone(); - let loaded_thread = build_thread_from_snapshot( - thread_id, - thread.session_configured().session_id.to_string(), - &config_snapshot, - thread.rollout_path(), - ); - self.thread_watch_manager.upsert_thread(loaded_thread).await; - let raw_events_enabled = if let Some(parent_thread_id) = config_snapshot.parent_thread_id { - self.thread_state_manager - .thread_state(parent_thread_id) - .await - .lock() - .await - .experimental_raw_events - } else { - false - }; - - for (connection_id, host_capabilities) in connections { - if host_capabilities != thread_host_capabilities { - continue; + let mut raw_events_enabled = false; + if let Ok(thread) = self.thread_manager.get_thread(thread_id).await { + let config_snapshot = thread.config_snapshot().await; + let loaded_thread = build_thread_from_snapshot( + thread_id, + thread.session_configured().session_id.to_string(), + &config_snapshot, + thread.rollout_path(), + ); + self.thread_watch_manager.upsert_thread(loaded_thread).await; + if let Some(parent_thread_id) = config_snapshot.parent_thread_id { + raw_events_enabled = self + .thread_state_manager + .thread_state(parent_thread_id) + .await + .lock() + .await + .experimental_raw_events; } + } + + for connection_id in connection_ids { log_listener_attach_result( self.ensure_conversation_listener(thread_id, connection_id, raw_events_enabled) .await, @@ -2678,9 +2665,9 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadResumeParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id) && self @@ -2710,7 +2697,7 @@ impl ThreadRequestProcessor { return Ok(()); } let redact_resume_payloads = - should_redact_thread_resume_payloads(app_server_client_info.name.as_deref()); + should_redact_thread_resume_payloads(app_server_client_name.as_deref()); let _thread_list_state_permit = match self.acquire_thread_list_state_permit().await { Ok(permit) => permit, @@ -2723,8 +2710,8 @@ impl ThreadRequestProcessor { .resume_running_thread( &request_id, ¶ms, - &app_server_client_info, - &host_capabilities, + app_server_client_name.clone(), + app_server_client_version.clone(), ) .await { @@ -2795,7 +2782,6 @@ impl ThreadRequestProcessor { developer_instructions, personality, ); - typesafe_overrides.host_capabilities = host_capabilities; self.load_and_apply_persisted_resume_metadata( &thread_history, &mut request_overrides, @@ -2836,9 +2822,12 @@ impl ThreadRequestProcessor { session_configured, .. }) => { - if let Err(err) = - Self::set_app_server_client_info(codex_thread.as_ref(), app_server_client_info) - .await + if let Err(err) = Self::set_app_server_client_info( + codex_thread.as_ref(), + app_server_client_name, + app_server_client_version, + ) + .await { self.outgoing.send_error(request_id, err).await; return Ok(()); @@ -3019,8 +3008,8 @@ impl ThreadRequestProcessor { &self, request_id: &ConnectionRequestId, params: &ThreadResumeParams, - app_server_client_info: &AppServerClientInfo, - host_capabilities: &HostCapabilities, + app_server_client_name: Option, + app_server_client_version: Option, ) -> Result { let running_thread = if params.history.is_some() { if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id) @@ -3080,12 +3069,6 @@ impl ThreadRequestProcessor { ))); } let config_snapshot = existing_thread.config_snapshot().await; - if &config_snapshot.host_capabilities != host_capabilities { - return Err(invalid_request(format!( - "cannot resume running thread {existing_thread_id} with different host capabilities: requested={host_capabilities:?} active={:?}", - config_snapshot.host_capabilities - ))); - } let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot); if !mismatch_details.is_empty() { let has_subscribers = !self @@ -3130,7 +3113,7 @@ impl ThreadRequestProcessor { ); } let redact_resume_payloads = - should_redact_thread_resume_payloads(app_server_client_info.name.as_deref()); + should_redact_thread_resume_payloads(app_server_client_name.as_deref()); let history_items = source_thread .history .take() @@ -3153,7 +3136,8 @@ impl ThreadRequestProcessor { .await?; Self::set_app_server_client_info( existing_thread.as_ref(), - app_server_client_info.clone(), + app_server_client_name, + app_server_client_version, ) .await?; @@ -3451,9 +3435,9 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadForkParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let ThreadForkParams { thread_id, @@ -3545,7 +3529,6 @@ impl ThreadRequestProcessor { developer_instructions, /*personality*/ None, ); - typesafe_overrides.host_capabilities = host_capabilities; typesafe_overrides.ephemeral = ephemeral.then_some(true); // Derive a Config using the same logic as new conversation, honoring overrides if provided. let config = self @@ -3584,7 +3567,12 @@ impl ThreadRequestProcessor { err => internal_error(format!("error forking thread: {err}")), })?; - Self::set_app_server_client_info(forked_thread.as_ref(), app_server_client_info).await?; + Self::set_app_server_client_info( + forked_thread.as_ref(), + app_server_client_name, + app_server_client_version, + ) + .await?; if session_configured.rollout_path.is_some() && let Some(name) = source_thread_name.clone() { diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 8da0f306e7..8c0d59efed 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -783,7 +783,6 @@ mod thread_processor_behavior_tests { parent_thread_id: None, thread_source: None, originator: "test_originator".to_string(), - host_capabilities: Default::default(), }; assert_eq!( diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 16d0e04773..3ff64cd34e 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -156,17 +156,17 @@ impl TurnRequestProcessor { &self, request_id: ConnectionRequestId, params: TurnStartParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { validate_user_input_image_urls(¶ms.input)?; self.turn_start_inner( request_id, params, - app_server_client_info, + app_server_client_name, + app_server_client_version, /*supports_openai_form_elicitation*/ supports_openai_form_elicitation, - host_capabilities, ) .await .map(|response| Some(response.into())) @@ -175,9 +175,8 @@ impl TurnRequestProcessor { pub(crate) async fn thread_inject_items( &self, params: ThreadInjectItemsParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_inject_items_response_inner(params, host_capabilities) + self.thread_inject_items_response_inner(params) .await .map(|response| Some(response.into())) } @@ -196,10 +195,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: TurnSteerParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { validate_user_input_image_urls(¶ms.input)?; - self.turn_steer_inner(request_id, params, host_capabilities) + self.turn_steer_inner(request_id, params) .await .map(|response| Some(response.into())) } @@ -208,9 +206,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: TurnInterruptParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.turn_interrupt_inner(request_id, params, host_capabilities) + self.turn_interrupt_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -219,9 +216,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeStartParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_realtime_start_inner(request_id, params, host_capabilities) + self.thread_realtime_start_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -230,9 +226,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendAudioParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_realtime_append_audio_inner(request_id, params, host_capabilities) + self.thread_realtime_append_audio_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -241,9 +236,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendTextParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_realtime_append_text_inner(request_id, params, host_capabilities) + self.thread_realtime_append_text_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -252,9 +246,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendSpeechParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_realtime_append_speech_inner(request_id, params, host_capabilities) + self.thread_realtime_append_speech_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -263,9 +256,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeStopParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.thread_realtime_stop_inner(request_id, params, host_capabilities) + self.thread_realtime_stop_inner(request_id, params) .await .map(|response| response.map(Into::into)) } @@ -285,9 +277,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ReviewStartParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { - self.review_start_inner(request_id, params, host_capabilities) + self.review_start_inner(request_id, params) .await .map(|()| None) } @@ -452,9 +443,9 @@ impl TurnRequestProcessor { &self, request_id: ConnectionRequestId, params: TurnStartParams, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, supports_openai_form_elicitation: bool, - host_capabilities: HostCapabilities, ) -> Result { let (thread_id, thread) = self.load_thread(¶ms.thread_id) @@ -462,13 +453,6 @@ impl TurnRequestProcessor { .inspect_err(|error| { self.track_error_response(&request_id, error, /*error_type*/ None); })?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "start a turn", - ) - .await?; self.ensure_direct_input_allowed(&request_id, thread.as_ref()) .await?; if let Err(error) = Self::validate_v2_input_limit(¶ms.input) { @@ -479,11 +463,15 @@ impl TurnRequestProcessor { ); return Err(error); } - Self::set_app_server_client_info(thread.as_ref(), app_server_client_info) - .await - .inspect_err(|error| { - self.track_error_response(&request_id, error, /*error_type*/ None); - })?; + Self::set_app_server_client_info( + thread.as_ref(), + app_server_client_name, + app_server_client_version, + ) + .await + .inspect_err(|error| { + self.track_error_response(&request_id, error, /*error_type*/ None); + })?; thread .set_openai_form_elicitation_support(supports_openai_form_elicitation) .await @@ -814,16 +802,8 @@ impl TurnRequestProcessor { async fn thread_inject_items_response_inner( &self, params: ThreadInjectItemsParams, - host_capabilities: HostCapabilities, ) -> Result { - let (thread_id, thread) = self.load_thread(¶ms.thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "inject items", - ) - .await?; + let (_, thread) = self.load_thread(¶ms.thread_id).await?; let items = params .items @@ -849,13 +829,19 @@ impl TurnRequestProcessor { async fn set_app_server_client_info( thread: &CodexThread, - app_server_client_info: AppServerClientInfo, + app_server_client_name: Option, + app_server_client_version: Option, ) -> Result<(), JSONRPCErrorError> { - let AppServerClientInfo { name, version } = app_server_client_info; - let mcp_elicitations_auto_deny = - xcode_26_4_mcp_elicitations_auto_deny(name.as_deref(), version.as_deref()); + let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny( + app_server_client_name.as_deref(), + app_server_client_version.as_deref(), + ); thread - .set_app_server_client_info(name, version, mcp_elicitations_auto_deny) + .set_app_server_client_info( + app_server_client_name, + app_server_client_version, + mcp_elicitations_auto_deny, + ) .await .map_err(|err| internal_error(format!("failed to set app server client info: {err}"))) } @@ -864,21 +850,13 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: TurnSteerParams, - host_capabilities: HostCapabilities, ) -> Result { - let (thread_id, thread) = - self.load_thread(¶ms.thread_id) - .await - .inspect_err(|error| { - self.track_error_response(request_id, error, /*error_type*/ None); - })?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - &host_capabilities, - "steer a turn", - ) - .await?; + let (_, thread) = self + .load_thread(¶ms.thread_id) + .await + .inspect_err(|error| { + self.track_error_response(request_id, error, /*error_type*/ None); + })?; self.ensure_direct_input_allowed(request_id, thread.as_ref()) .await?; @@ -981,16 +959,8 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, thread_id: &str, - host_capabilities: &HostCapabilities, ) -> Result)>, JSONRPCErrorError> { let (thread_id, thread) = self.load_thread(thread_id).await?; - ensure_thread_host_capabilities( - thread_id, - thread.as_ref(), - host_capabilities, - "use realtime", - ) - .await?; match self .ensure_conversation_listener( @@ -1020,10 +990,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeStartParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let Some((_, thread)) = self - .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities) + .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id) .await? else { return Ok(None); @@ -1065,10 +1034,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendAudioParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let Some((_, thread)) = self - .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities) + .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id) .await? else { return Ok(None); @@ -1093,10 +1061,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendTextParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let Some((_, thread)) = self - .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities) + .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id) .await? else { return Ok(None); @@ -1122,10 +1089,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeAppendSpeechParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let Some((_, thread)) = self - .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities) + .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id) .await? else { return Ok(None); @@ -1148,10 +1114,9 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ThreadRealtimeStopParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let Some((_, thread)) = self - .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities) + .prepare_realtime_conversation_thread(request_id, ¶ms.thread_id) .await? else { return Ok(None); @@ -1251,7 +1216,7 @@ impl TurnRequestProcessor { )) })?; - let mut config = parent_thread.config().await.as_ref().clone(); + let mut config = self.config.as_ref().clone(); if let Some(review_model) = &config.review_model { config.model = Some(review_model.clone()); } @@ -1344,7 +1309,6 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ReviewStartParams, - host_capabilities: HostCapabilities, ) -> Result<(), JSONRPCErrorError> { let ReviewStartParams { thread_id, @@ -1353,13 +1317,6 @@ impl TurnRequestProcessor { } = params; let (parent_thread_id, parent_thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - parent_thread_id, - parent_thread.as_ref(), - &host_capabilities, - "start a review", - ) - .await?; let (review_request, display_text) = Self::review_request_from_target(target)?; match delivery.unwrap_or(ApiReviewDelivery::Inline).to_core() { CoreReviewDelivery::Inline => { @@ -1390,19 +1347,11 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: TurnInterruptParams, - host_capabilities: HostCapabilities, ) -> Result, JSONRPCErrorError> { let TurnInterruptParams { thread_id, turn_id } = params; let is_startup_interrupt = turn_id.is_empty(); let (thread_uuid, thread) = self.load_thread(&thread_id).await?; - ensure_thread_host_capabilities( - thread_uuid, - thread.as_ref(), - &host_capabilities, - "interrupt a turn", - ) - .await?; // Record turn interrupts so we can reply when TurnAborted arrives. Startup // interrupts do not have a turn and are acknowledged after submission. diff --git a/codex-rs/app-server/tests/suite/v2/attestation.rs b/codex-rs/app-server/tests/suite/v2/attestation.rs index 0c8f53b27e..9df1571627 100644 --- a/codex-rs/app-server/tests/suite/v2/attestation.rs +++ b/codex-rs/app-server/tests/suite/v2/attestation.rs @@ -85,7 +85,6 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand request_attestation: true, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ), ) diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs index e7a159ba25..ece66c8405 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs @@ -8,7 +8,6 @@ use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ConfigWarningNotification; -use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; @@ -18,14 +17,10 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; -use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; -use codex_app_server_protocol::TurnStartParams; -use codex_app_server_protocol::UserInput as V2UserInput; use codex_core::config::set_project_trust_level; use codex_protocol::config_types::TrustLevel; -use core_test_support::responses; use futures::SinkExt; use futures::StreamExt; use hmac::Hmac; @@ -55,9 +50,6 @@ use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::tungstenite::http::HeaderValue; use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION; use tokio_tungstenite::tungstenite::http::header::ORIGIN; -use wiremock::Mock; -use wiremock::matchers::method; -use wiremock::matchers::path_regex; // macOS and Windows CI can spend tens of seconds starting the app-server test // binary under Bazel before it accepts JSON-RPC or reports its websocket bind @@ -70,12 +62,6 @@ pub(super) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); pub(super) type WsClient = WebSocketStream>; type HmacSha256 = Hmac; -fn body_contains(req: &wiremock::Request, text: &str) -> bool { - String::from_utf8(req.body.clone()) - .ok() - .is_some_and(|body| body.contains(text)) -} - #[tokio::test] async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; @@ -121,310 +107,6 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Ok(()) } -#[tokio::test] -async fn host_capabilities_are_normalized_and_sticky_for_loaded_threads() -> Result<()> { - const CHILD_PROMPT: &str = "child: verify capability-scoped auto-attach"; - const PARENT_PROMPT: &str = "spawn a child to test connection auto-attach"; - const SPAWN_CALL_ID: &str = "spawn-call-host-capabilities"; - - let server = responses::start_mock_server().await; - let spawn_args = serde_json::to_string(&json!({ - "message": CHILD_PROMPT, - "task_name": "capability_check", - }))?; - let _parent_turn = responses::mount_sse_once_match( - &server, - |req: &wiremock::Request| body_contains(req, PARENT_PROMPT), - responses::sse(vec![ - responses::ev_response_created("resp-parent-1"), - responses::ev_function_call_with_namespace( - SPAWN_CALL_ID, - "collaboration", - "spawn_agent", - &spawn_args, - ), - responses::ev_completed("resp-parent-1"), - ]), - ) - .await; - let child_response = responses::sse(vec![ - responses::ev_response_created("resp-child-1"), - responses::ev_assistant_message("msg-child-1", "child done"), - responses::ev_completed("resp-child-1"), - ]); - Mock::given(method("POST")) - .and(path_regex(".*/responses$")) - .and(|req: &wiremock::Request| { - body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID) - }) - .respond_with(responses::sse_response(child_response).set_delay(Duration::from_millis(250))) - .up_to_n_times(1) - .mount(&server) - .await; - let _parent_follow_up = responses::mount_sse_once_match( - &server, - |req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID), - responses::sse(vec![ - responses::ev_response_created("resp-parent-2"), - responses::ev_assistant_message("msg-parent-2", "parent done"), - responses::ev_completed("resp-parent-2"), - ]), - ) - .await; - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), "never")?; - let config_path = codex_home.path().join("config.toml"); - let config = std::fs::read_to_string(&config_path)?; - std::fs::write( - config_path, - format!("{config}\n[features]\nmulti_agent_v2 = true\n"), - )?; - - let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?; - - let mut owner = connect_websocket(bind_addr).await?; - send_initialize_request_with_host_capabilities( - &mut owner, - /*id*/ 1, - "ws_thread_owner", - Some(vec![" capability-a ", "", "capability-a", "capability-b"]), - ) - .await?; - read_response_for_id(&mut owner, /*id*/ 1) - .await - .context("owner initialize response")?; - - // A connection with a different capability set must not be auto-attached - // when the owner creates the thread. - let mut mismatched_client = connect_websocket(bind_addr).await?; - send_initialize_request_with_host_capabilities( - &mut mismatched_client, - /*id*/ 6, - "ws_mismatched_client", - Some(vec!["capability-c"]), - ) - .await?; - read_response_for_id(&mut mismatched_client, /*id*/ 6) - .await - .context("mismatched-client initialize response")?; - - // Equivalent capabilities with different ordering and without duplicates - // must be eligible for auto-attach after normalization. - let mut equivalent_client = connect_websocket(bind_addr).await?; - send_initialize_request_with_host_capabilities( - &mut equivalent_client, - /*id*/ 4, - "ws_equivalent_client", - Some(vec!["capability-b", "capability-a"]), - ) - .await?; - read_response_for_id(&mut equivalent_client, /*id*/ 4) - .await - .context("equivalent-client initialize response")?; - - let thread_id = start_thread(&mut owner, /*id*/ 2).await?; - read_notification_for_method(&mut mismatched_client, "thread/started") - .await - .context("mismatched-client global thread/started notification")?; - read_notification_for_method(&mut equivalent_client, "thread/started") - .await - .context("equivalent-client global thread/started notification")?; - send_request( - &mut owner, - "turn/start", - /*id*/ 3, - Some(serde_json::to_value(TurnStartParams { - thread_id: thread_id.clone(), - input: vec![V2UserInput::Text { - text: PARENT_PROMPT.to_string(), - text_elements: Vec::new(), - }], - model: Some("mock-model".to_string()), - ..Default::default() - })?), - ) - .await?; - read_response_for_id(&mut owner, /*id*/ 3) - .await - .context("owner turn/start response")?; - read_notification_for_method(&mut owner, "turn/completed") - .await - .context("owner turn/completed notification")?; - read_notification_for_method(&mut equivalent_client, "turn/completed") - .await - .context("equivalent auto-attached client child turn/completed notification")?; - - match timeout(Duration::from_millis(250), async { - loop { - if let JSONRPCMessage::Notification(notification) = - read_jsonrpc_message(&mut mismatched_client).await? - && notification.method == "turn/completed" - { - return Ok::<_, anyhow::Error>(notification); - } - } - }) - .await - { - Ok(Ok(_)) => bail!("mismatched client received thread-scoped turn/completed"), - Ok(Err(err)) => return Err(err), - Err(_) => {} - } - - send_request( - &mut equivalent_client, - "thread/resume", - /*id*/ 5, - Some(serde_json::to_value(ThreadResumeParams { - thread_id: thread_id.clone(), - ..Default::default() - })?), - ) - .await?; - read_response_for_id(&mut equivalent_client, /*id*/ 5) - .await - .context("equivalent-client resume response")?; - - send_request( - &mut mismatched_client, - "thread/resume", - /*id*/ 7, - Some(serde_json::to_value(ThreadResumeParams { - thread_id: thread_id.clone(), - ..Default::default() - })?), - ) - .await?; - let resume_error = read_error_for_id(&mut mismatched_client, /*id*/ 7) - .await - .context("mismatched-client resume error")?; - assert!( - resume_error - .error - .message - .contains("cannot resume running thread") - ); - - send_request( - &mut mismatched_client, - "turn/start", - /*id*/ 8, - Some(serde_json::to_value(TurnStartParams { - thread_id: thread_id.clone(), - model: Some("mock-model".to_string()), - ..Default::default() - })?), - ) - .await?; - let turn_error = read_error_for_id(&mut mismatched_client, /*id*/ 8) - .await - .context("mismatched-client turn/start error")?; - assert!(turn_error.error.message.contains("cannot start a turn")); - - send_request( - &mut mismatched_client, - "turn/steer", - /*id*/ 9, - Some(json!({ - "threadId": thread_id, - "expectedTurnId": "turn-under-test", - "input": [], - })), - ) - .await?; - let steer_error = read_error_for_id(&mut mismatched_client, /*id*/ 9) - .await - .context("mismatched-client turn/steer error")?; - assert!(steer_error.error.message.contains("cannot steer a turn")); - - send_request( - &mut mismatched_client, - "thread/inject_items", - /*id*/ 10, - Some(json!({ - "threadId": thread_id, - "items": [], - })), - ) - .await?; - let inject_error = read_error_for_id(&mut mismatched_client, /*id*/ 10) - .await - .context("mismatched-client thread/injectItems error")?; - assert!( - inject_error.error.message.contains("cannot inject items"), - "unexpected inject error: {}", - inject_error.error.message - ); - - send_request( - &mut mismatched_client, - "app/list", - /*id*/ 11, - Some(json!({ - "threadId": thread_id, - })), - ) - .await?; - let apps_error = read_error_for_id(&mut mismatched_client, /*id*/ 11) - .await - .context("mismatched-client app/list error")?; - assert!(apps_error.error.message.contains("cannot list apps")); - - send_request( - &mut mismatched_client, - "thread/rollback", - /*id*/ 12, - Some(json!({ - "threadId": thread_id, - "numTurns": 1, - })), - ) - .await?; - let rollback_error = read_error_for_id(&mut mismatched_client, /*id*/ 12) - .await - .context("mismatched-client thread/rollback error")?; - assert!(rollback_error.error.message.contains("cannot roll back")); - - send_request( - &mut mismatched_client, - "turn/interrupt", - /*id*/ 13, - Some(json!({ - "threadId": thread_id, - "turnId": "turn-under-test", - })), - ) - .await?; - let interrupt_error = read_error_for_id(&mut mismatched_client, /*id*/ 13) - .await - .context("mismatched-client turn/interrupt error")?; - assert!( - interrupt_error - .error - .message - .contains("cannot interrupt a turn") - ); - - send_request( - &mut mismatched_client, - "thread/settings/update", - /*id*/ 14, - Some(json!({ - "threadId": thread_id, - })), - ) - .await?; - read_response_for_id(&mut mismatched_client, /*id*/ 14) - .await - .context("mismatched-client thread/settings/update response")?; - - process - .kill() - .await - .context("failed to stop websocket app-server process")?; - Ok(()) -} - #[tokio::test] async fn thread_start_routes_project_exec_policy_warning_to_requester() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; @@ -1012,21 +694,6 @@ pub(super) async fn send_initialize_request( stream: &mut WsClient, id: i64, client_name: &str, -) -> Result<()> { - send_initialize_request_with_host_capabilities( - stream, - id, - client_name, - /*host_capabilities*/ None, - ) - .await -} - -async fn send_initialize_request_with_host_capabilities( - stream: &mut WsClient, - id: i64, - client_name: &str, - host_capabilities: Option>, ) -> Result<()> { let params = InitializeParams { client_info: ClientInfo { @@ -1034,11 +701,7 @@ async fn send_initialize_request_with_host_capabilities( title: Some("WebSocket Test Client".to_string()), version: "0.1.0".to_string(), }, - capabilities: host_capabilities.map(|host_capabilities| InitializeCapabilities { - experimental_api: true, - host_capabilities: Some(host_capabilities.into_iter().map(str::to_string).collect()), - ..Default::default() - }), + capabilities: None, }; send_request( stream, 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 cfbb4679fa..cc010b93eb 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -44,7 +44,6 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -81,7 +80,6 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -133,7 +131,6 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -173,7 +170,6 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -213,7 +209,6 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -268,7 +263,6 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -311,7 +305,6 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; @@ -353,7 +346,6 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil request_attestation: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 8df1033cf4..aba3f56078 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -232,7 +232,6 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu request_attestation: false, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ), ) 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 5f283fa515..dad1db88af 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -155,7 +155,6 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { request_attestation: false, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), mcp_server_openai_form_elicitation: false, - host_capabilities: None, }), ), ) diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index 2ebe9b7075..c06c988054 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -331,7 +331,7 @@ async fn reprojects_cached_configured_and_fallback_plugins_for_host_capabilities write_required_manifests(HOST_CAPABILITY_SESSION_OWNED_IAB); plugins .host_capabilities - .insert(HOST_CAPABILITY_INLINE_VISUALIZATION.to_string()); + .insert(HOST_CAPABILITY_INLINE_VISUALIZATION); let supported = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[configured_plugin_id], &[], &[]), @@ -424,7 +424,7 @@ source = "/tmp/{marketplace_name}" plugins .host_capabilities - .insert(HOST_CAPABILITY_INLINE_VISUALIZATION.to_string()); + .insert(HOST_CAPABILITY_INLINE_VISUALIZATION); let supported = list_discoverable_plugins( &plugins_manager, discovery_input( diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index 49405a4ec8..9246ebda3f 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -34,6 +34,7 @@ use codex_core_skills::loader::load_skills_from_roots; use codex_exec_server::LOCAL_FS; use codex_mcp::parse_plugin_mcp_config; use codex_plugin::AppDeclaration; +use codex_plugin::HostCapabilities; use codex_plugin::LoadedPlugin; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginHookSource; @@ -178,7 +179,7 @@ pub async fn load_plugin_hooks_from_layer_stack( extra_plugins: HashMap, store: &PluginStore, remote_global_catalog_active: bool, - host_capabilities: &HashSet, + host_capabilities: &HostCapabilities, ) -> PluginHookLoadOutcome { let mut plugins = load_plugins_from_layer_stack_with_scope( config_layer_stack, @@ -768,8 +769,8 @@ async fn load_plugin( manifest_description: None, root, enabled: plugin.enabled, - required_host_capabilities: Vec::new(), - missing_host_capabilities: Vec::new(), + required_host_capabilities: HostCapabilities::default(), + missing_host_capabilities: HostCapabilities::default(), skill_roots: Vec::new(), disabled_skill_paths: HashSet::new(), has_enabled_skills: false, diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 9d2669e74e..b81e338f3b 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -72,6 +72,7 @@ use codex_hooks::plugin_hook_declarations; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; +use codex_plugin::HostCapabilities; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; use codex_plugin::PluginIdError; @@ -110,7 +111,7 @@ pub struct PluginsConfigInput { pub plugins_enabled: bool, pub remote_plugin_enabled: bool, pub chatgpt_base_url: String, - pub host_capabilities: HashSet, + pub host_capabilities: HostCapabilities, } impl PluginsConfigInput { @@ -125,7 +126,7 @@ impl PluginsConfigInput { plugins_enabled, remote_plugin_enabled, chatgpt_base_url, - host_capabilities: HashSet::new(), + host_capabilities: HostCapabilities::default(), } } } @@ -591,7 +592,7 @@ impl PluginsManager { fn resolve_loaded_plugins_for_runtime( &self, mut plugins: Vec, - host_capabilities: &HashSet, + host_capabilities: &HostCapabilities, ) -> PluginLoadOutcome { let auth_mode = self.auth_mode(); for plugin in &mut plugins { @@ -1257,6 +1258,9 @@ impl PluginsManager { !installed_plugin_ids.contains(plugin.config_id.as_str()) && !installed_remote_plugin_ids.contains(plugin.remote_plugin_id.as_str()) && !disabled_plugin_ids.contains(plugin.config_id.as_str()) + && plugin + .requirements + .is_satisfied_by(&input.plugins_config.host_capabilities) }) .map(|plugin| { DiscoverableTool::from(DiscoverablePluginInfo { @@ -1692,7 +1696,7 @@ impl PluginsManager { marketplace_name: &str, plugin: &ConfiguredMarketplacePlugin, skill_config_rules: &SkillConfigRules, - host_capabilities: &HashSet, + host_capabilities: &HostCapabilities, ) -> Result, MarketplaceError> { let fragment = self .tool_suggest_metadata_cache diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 3a2a12fc92..097fc8de32 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -45,6 +45,7 @@ use codex_core_skills::SkillsService; use codex_core_skills::config_rules::SkillConfigRules; use codex_login::CodexAuth; use codex_plugin::AppDeclaration; +use codex_plugin::HostCapabilities; use codex_plugin::PluginId; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::HookEventName; @@ -808,10 +809,7 @@ fn write_host_capability_projection_plugin(codex_home: &Path) -> std::path::Path } fn set_host_capabilities(config: &mut PluginsConfigInput, capabilities: &[&str]) { - config.host_capabilities = capabilities - .iter() - .map(|capability| (*capability).to_string()) - .collect(); + config.host_capabilities = HostCapabilities::from_names(capabilities); } async fn load_plugins_from_config( @@ -932,8 +930,8 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() { ), root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(), enabled: true, - required_host_capabilities: Vec::new(), - missing_host_capabilities: Vec::new(), + required_host_capabilities: HostCapabilities::default(), + missing_host_capabilities: HostCapabilities::default(), skill_roots: vec![plugin_root.join("skills").abs()], disabled_skill_paths: HashSet::new(), has_enabled_skills: true, @@ -1015,17 +1013,14 @@ async fn plugin_host_capability_projection_is_all_of_and_reprojects_cached_plugi assert_eq!(plugin.error, None); assert_eq!( plugin.missing_host_capabilities, - missing_host_capabilities - .iter() - .map(|capability| (*capability).to_string()) - .collect::>() + HostCapabilities::from_names(missing_host_capabilities) ); assert_eq!( plugin.required_host_capabilities, - vec![ + HostCapabilities::from_names([ HOST_CAPABILITY_INLINE_VISUALIZATION.to_string(), HOST_CAPABILITY_SESSION_OWNED_IAB.to_string(), - ] + ]) ); assert_eq!(plugin.skill_roots, vec![plugin_root.join("skills").abs()]); assert_eq!( @@ -2355,8 +2350,8 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions manifest_description: None, root: AbsolutePathBuf::try_from(plugin_root).unwrap(), enabled: false, - required_host_capabilities: Vec::new(), - missing_host_capabilities: Vec::new(), + required_host_capabilities: HostCapabilities::default(), + missing_host_capabilities: HostCapabilities::default(), skill_roots: Vec::new(), disabled_skill_paths: HashSet::new(), has_enabled_skills: false, @@ -2532,8 +2527,8 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() { manifest_description: None, root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(), enabled: true, - required_host_capabilities: Vec::new(), - missing_host_capabilities: Vec::new(), + required_host_capabilities: HostCapabilities::default(), + missing_host_capabilities: HostCapabilities::default(), skill_roots: Vec::new(), disabled_skill_paths: HashSet::new(), has_enabled_skills: false, @@ -5171,13 +5166,17 @@ plugins = true "name": "slack", "release": { "display_name": "Slack", - "app_ids": ["connector_slack"] + "app_ids": ["connector_slack"], + "requires": {"hostCapabilities": []} } }, { "id": "plugin_github", "name": "github", - "release": {"display_name": "GitHub"} + "release": { + "display_name": "GitHub", + "requires": {"hostCapabilities": []} + } } ] })) @@ -5198,12 +5197,14 @@ plugins = true remote_plugin_id: "plugin_github".to_string(), display_name: "GitHub".to_string(), app_connector_ids: Vec::new(), + requirements: Default::default(), }, RecommendedPlugin { config_id: "slack@openai-curated-remote".to_string(), remote_plugin_id: "plugin_slack".to_string(), display_name: "Slack".to_string(), app_connector_ids: vec!["connector_slack".to_string()], + requirements: Default::default(), }, ], }; @@ -5222,7 +5223,7 @@ plugins = true } #[tokio::test] -async fn recommended_plugin_candidates_filter_installed_and_disabled_plugins() { +async fn recommended_plugin_candidates_filter_installed_disabled_and_unsupported_plugins() { let tmp = tempfile::tempdir().unwrap(); write_file( &tmp.path().join(CONFIG_TOML_FILE), @@ -5239,17 +5240,36 @@ plugins = true { "id": "plugin_linear", "name": "linear", - "release": {"display_name": "Linear"} + "release": { + "display_name": "Linear", + "requires": {"hostCapabilities": []} + } }, { "id": "plugin_github", "name": "github", - "release": {"display_name": "GitHub"} + "release": { + "display_name": "GitHub", + "requires": {"hostCapabilities": []} + } }, { "id": "plugin_slack", "name": "slack", - "release": {"display_name": "Slack"} + "release": { + "display_name": "Slack", + "requires": {"hostCapabilities": []} + } + }, + { + "id": "plugin_visualize", + "name": "visualize", + "release": { + "display_name": "Visualize", + "requires": { + "hostCapabilities": [HOST_CAPABILITY_INLINE_VISUALIZATION] + } + } } ] }))) @@ -5291,6 +5311,43 @@ plugins = true app_connector_ids: Vec::new(), })]) ); + + config + .host_capabilities + .insert(HOST_CAPABILITY_INLINE_VISUALIZATION); + let candidates = manager + .recommended_plugin_candidates_for_config(RecommendedPluginCandidatesInput { + plugins_config: &config, + loaded_plugins: &loaded_plugins, + auth: Some(&auth), + disabled_tools: &disabled_tools, + app_server_client_name: None, + }) + .await; + + assert_eq!( + candidates, + Some(vec![ + DiscoverableTool::from(DiscoverablePluginInfo { + id: "slack@openai-curated-remote".to_string(), + remote_plugin_id: Some("plugin_slack".to_string()), + name: "Slack".to_string(), + description: None, + has_skills: false, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), + }), + DiscoverableTool::from(DiscoverablePluginInfo { + id: "visualize@openai-curated-remote".to_string(), + remote_plugin_id: Some("plugin_visualize".to_string()), + name: "Visualize".to_string(), + description: None, + has_skills: false, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), + }), + ]) + ); } #[tokio::test] diff --git a/codex-rs/core-plugins/src/manifest.rs b/codex-rs/core-plugins/src/manifest.rs index 6e1bf529d5..2b395e877f 100644 --- a/codex-rs/core-plugins/src/manifest.rs +++ b/codex-rs/core-plugins/src/manifest.rs @@ -1,4 +1,5 @@ use codex_config::HooksFile; +use codex_plugin::HostCapabilities; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathConvention; use codex_utils_path_uri::PathUri; @@ -188,17 +189,7 @@ pub(crate) fn parse_plugin_manifest_uri( let version = version.trim(); (!version.is_empty()).then(|| version.to_string()) }); - let mut host_capabilities = Vec::new(); - for capability in requires.host_capabilities { - let capability = capability.trim(); - if !capability.is_empty() - && !host_capabilities - .iter() - .any(|existing| existing == capability) - { - host_capabilities.push(capability.to_string()); - } - } + let host_capabilities = HostCapabilities::from_names(requires.host_capabilities); let interface = interface.and_then(|interface| { let RawPluginManifestInterface { display_name, @@ -535,6 +526,7 @@ mod tests { use super::load_plugin_manifest; use codex_exec_server::EnvironmentManager; use codex_exec_server::LOCAL_ENVIRONMENT_ID; + use codex_plugin::HostCapabilities; use codex_plugin::PluginProvider; use codex_plugin::ResolvedPlugin; use codex_plugin::manifest::PluginManifest as GenericPluginManifest; @@ -757,10 +749,10 @@ mod tests { assert_eq!( manifest.requires, PluginManifestRequirements { - host_capabilities: vec![ - "codex.inline_visualization".to_string(), - "codex.code_mode".to_string(), - ], + host_capabilities: HostCapabilities::from_names([ + "codex.inline_visualization", + "codex.code_mode", + ]), } ); } @@ -788,7 +780,7 @@ mod tests { assert_eq!(manifest.version, Some("2.0.0".to_string())); assert_eq!( manifest.requires.host_capabilities, - vec!["codex.inline_visualization".to_string()] + HostCapabilities::from_names(["codex.inline_visualization"]) ); assert_eq!( manifest @@ -897,7 +889,7 @@ mod tests { assert_eq!( executor_plugin.manifest().requires, PluginManifestRequirements { - host_capabilities: vec!["codex.inline_visualization".to_string()], + host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]), } ); assert_eq!(executor_plugin, expected_plugin); diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index b9b94f4ded..d7e2a5692c 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -13,9 +13,11 @@ use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; use codex_plugin::AppConnectorId; use codex_plugin::AppDeclaration; +use codex_plugin::HostCapabilities; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; use codex_plugin::app_connector_ids_from_declarations; +use codex_plugin::manifest::PluginManifestRequirements; use codex_plugin::prompt_safe_plugin_description; use codex_utils_absolute_path::AbsolutePathBuf; use reqwest::RequestBuilder; @@ -263,6 +265,7 @@ pub struct RecommendedPlugin { pub remote_plugin_id: String, pub display_name: String, pub app_connector_ids: Vec, + pub requirements: PluginManifestRequirements, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -648,6 +651,15 @@ struct RecommendedPluginRelease { display_name: String, #[serde(default)] app_ids: Vec, + #[serde(default)] + requires: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RecommendedPluginRequirements { + #[serde(default)] + host_capabilities: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -894,7 +906,11 @@ fn recommended_plugins_mode(response: RecommendedPluginsResponse) -> Recommended let RecommendedPluginRelease { display_name, app_ids, + requires, } = plugin.release; + let Some(requires) = requires else { + return RecommendedPluginsMode::Legacy; + }; let display_name = non_empty_string(Some(&display_name)) .unwrap_or_else(|| plugin.name.clone()) .chars() @@ -913,6 +929,9 @@ fn recommended_plugins_mode(response: RecommendedPluginsResponse) -> Recommended remote_plugin_id: plugin.id, display_name, app_connector_ids, + requirements: PluginManifestRequirements { + host_capabilities: HostCapabilities::from_names(requires.host_capabilities), + }, }); } diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index 29fe8add19..cf00cba6b9 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -164,6 +164,7 @@ fn item(name: &str, display_name: &str) -> RecommendedPluginItem { release: RecommendedPluginRelease { display_name: display_name.to_string(), app_ids: Vec::new(), + requires: Some(RecommendedPluginRequirements::default()), }, } } @@ -218,6 +219,20 @@ fn recommended_plugins_require_remote_install_identity() { assert!(response.is_err()); } +#[test] +fn recommended_plugins_without_requirements_use_legacy_discovery() { + let mut plugin = item("github", "GitHub"); + plugin.release.requires = None; + + assert_eq!( + recommended_plugins_mode(RecommendedPluginsResponse { + enabled: Some(true), + plugins: vec![plugin], + }), + RecommendedPluginsMode::Legacy + ); +} + #[test] fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() { let mut plugins = (0..=52) @@ -234,6 +249,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() { release: RecommendedPluginRelease { display_name: "Disabled".to_string(), app_ids: Vec::new(), + requires: Some(RecommendedPluginRequirements::default()), }, }); plugins.push(RecommendedPluginItem { @@ -244,6 +260,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() { release: RecommendedPluginRelease { display_name: "Not Available".to_string(), app_ids: Vec::new(), + requires: Some(RecommendedPluginRequirements::default()), }, }); @@ -263,6 +280,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() { remote_plugin_id: "plugin_plugin-00".to_string(), display_name: "Plugin 00".to_string(), app_connector_ids: Vec::new(), + requirements: PluginManifestRequirements::default(), }) ); assert_eq!( @@ -272,6 +290,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() { remote_plugin_id: "plugin_plugin-49".to_string(), display_name: "Plugin 49".to_string(), app_connector_ids: Vec::new(), + requirements: PluginManifestRequirements::default(), }) ); } @@ -296,6 +315,7 @@ fn recommended_plugins_bound_model_visible_fields() { remote_plugin_id: "plugin_bounded".to_string(), display_name: "D".repeat(MAX_RECOMMENDED_PLUGIN_DISPLAY_NAME_LEN), app_connector_ids: Vec::new(), + requirements: PluginManifestRequirements::default(), }], } ); @@ -318,6 +338,7 @@ fn recommended_plugins_preserve_install_identity_and_normalize_app_ids() { "connector_two".to_string(), "connector_one".to_string(), ], + requires: Some(RecommendedPluginRequirements::default()), }, }], }); @@ -330,6 +351,7 @@ fn recommended_plugins_preserve_install_identity_and_normalize_app_ids() { remote_plugin_id: "plugin_connector_sample".to_string(), display_name: "Sample".to_string(), app_connector_ids: vec!["connector_one".to_string(), "connector_two".to_string(),], + requirements: PluginManifestRequirements::default(), }], } ); @@ -347,6 +369,7 @@ fn recommended_plugins_ignore_invalid_remote_plugin_ids() { release: RecommendedPluginRelease { display_name: "Sample".to_string(), app_ids: Vec::new(), + requires: Some(RecommendedPluginRequirements::default()), }, }], }); diff --git a/codex-rs/core-plugins/src/tool_suggest_metadata.rs b/codex-rs/core-plugins/src/tool_suggest_metadata.rs index d2943afadf..d1544b5705 100644 --- a/codex-rs/core-plugins/src/tool_suggest_metadata.rs +++ b/codex-rs/core-plugins/src/tool_suggest_metadata.rs @@ -1,14 +1,15 @@ use std::collections::HashMap; -use std::collections::HashSet; use std::sync::Arc; use std::sync::RwLock; use codex_core_skills::config_rules::SkillConfigRules; use codex_plugin::AppDeclaration; +use codex_plugin::HostCapabilities; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; use codex_plugin::PluginIdError; use codex_plugin::app_connector_ids_from_declarations; +use codex_plugin::manifest::PluginManifestRequirements; use codex_plugin::prompt_safe_plugin_description; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::Product; @@ -55,7 +56,7 @@ pub(crate) struct ToolSuggestMetadataFragment { config_name: String, display_name: String, description: Option, - required_host_capabilities: Vec, + requirements: PluginManifestRequirements, mcp_server_names: Vec, app_declarations: Vec, skill_inventory: Option, @@ -66,13 +67,9 @@ impl ToolSuggestMetadataFragment { &self, skill_config_rules: &SkillConfigRules, auth_mode: Option, - host_capabilities: &HashSet, + host_capabilities: &HostCapabilities, ) -> Option { - if self - .required_host_capabilities - .iter() - .any(|capability| !host_capabilities.contains(capability)) - { + if !self.requirements.is_satisfied_by(host_capabilities) { return None; } @@ -206,15 +203,14 @@ async fn load_plugin_metadata( )?; let MarketplacePluginSource::Local { path: plugin_root } = &plugin.source else { - let required_host_capabilities = match plugin.manifest_fallback.as_ref() { + let requirements = match plugin.manifest_fallback.as_ref() { Some(fallback) => { fallback .parse_for_listing() .ok_or_else(|| "invalid marketplace plugin manifest fallback".to_string())? .requires - .host_capabilities } - None => Vec::new(), + None => PluginManifestRequirements::default(), }; return Ok(Arc::new(ToolSuggestMetadataFragment { config_name: plugin.id.clone(), @@ -222,7 +218,7 @@ async fn load_plugin_metadata( description: prompt_safe_plugin_description(Some( &remote_plugin_install_required_description(&plugin.source), )), - required_host_capabilities, + requirements, mcp_server_names: Vec::new(), app_declarations: Vec::new(), skill_inventory: None, @@ -254,7 +250,7 @@ async fn load_plugin_metadata( config_name: plugin.id.clone(), display_name: plugin.name.clone(), description: prompt_safe_plugin_description(manifest.description.as_deref()), - required_host_capabilities: manifest.requires.host_capabilities, + requirements: manifest.requires, mcp_server_names, app_declarations, skill_inventory: Some(skill_inventory), diff --git a/codex-rs/core-skills/src/service.rs b/codex-rs/core-skills/src/service.rs index b68fcf09d4..b4e0d7396c 100644 --- a/codex-rs/core-skills/src/service.rs +++ b/codex-rs/core-skills/src/service.rs @@ -69,7 +69,7 @@ pub struct SkillsService { codex_home: AbsolutePathBuf, restriction_product: Option, extra_roots: RwLock>, - cache_by_cwd: RwLock>, + cache_by_cwd: RwLock>, cache_by_config: RwLock>, } @@ -173,17 +173,9 @@ impl SkillsService { fs: Option>, ) -> HostSkillsSnapshot { let use_cwd_cache = fs.is_some(); - let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack); - let bundled_skills_enabled = bundled_skills_enabled_from_stack(&input.config_layer_stack); - let cache_key = CwdSkillsCacheKey { - cwd: input.cwd.clone(), - effective_skill_roots: input.effective_skill_roots.clone(), - bundled_skills_enabled, - skill_config_rules: skill_config_rules.clone(), - }; if use_cwd_cache && !force_reload - && let Some(snapshot) = self.cached_snapshot_for_cwd(&cache_key) + && let Some(snapshot) = self.cached_snapshot_for_cwd(&input.cwd) { return snapshot; } @@ -196,9 +188,10 @@ impl SkillsService { self.extra_roots(), ) .await; - if !bundled_skills_enabled { + if !bundled_skills_enabled_from_stack(&input.config_layer_stack) { roots.retain(|root| root.scope != SkillScope::System); } + let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack); let snapshot = HostSkillsSnapshot::new(Arc::new( self.build_skill_outcome(input, roots, &skill_config_rules) .await, @@ -208,7 +201,7 @@ impl SkillsService { .cache_by_cwd .write() .unwrap_or_else(std::sync::PoisonError::into_inner); - cache.insert(cache_key, snapshot.clone()); + cache.insert(input.cwd.clone(), snapshot.clone()); } snapshot } @@ -250,10 +243,10 @@ impl SkillsService { info!("skills cache cleared ({cleared} entries)"); } - fn cached_snapshot_for_cwd(&self, cache_key: &CwdSkillsCacheKey) -> Option { + fn cached_snapshot_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option { match self.cache_by_cwd.read() { - Ok(cache) => cache.get(cache_key).cloned(), - Err(err) => err.into_inner().get(cache_key).cloned(), + Ok(cache) => cache.get(cwd).cloned(), + Err(err) => err.into_inner().get(cwd).cloned(), } } @@ -275,14 +268,6 @@ impl SkillsService { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct CwdSkillsCacheKey { - cwd: AbsolutePathBuf, - effective_skill_roots: Vec, - bundled_skills_enabled: bool, - skill_config_rules: SkillConfigRules, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct ConfigSkillsCacheKey { roots: Vec<(AbsolutePathBuf, u8, Option, Option)>, diff --git a/codex-rs/core-skills/src/service_tests.rs b/codex-rs/core-skills/src/service_tests.rs index 98ad36a6d3..6026975097 100644 --- a/codex-rs/core-skills/src/service_tests.rs +++ b/codex-rs/core-skills/src/service_tests.rs @@ -228,68 +228,6 @@ async fn skills_for_config_reuses_cache_for_same_effective_config() { assert_eq!(outcome2.skills, outcome1.skills); } -#[tokio::test] -async fn cwd_cache_separates_effective_plugin_skill_roots() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let config_layer_stack = config_stack(&codex_home, ""); - let skills_service = SkillsService::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - let skill_path = write_plugin_skill( - &codex_home, - "test", - "visualize", - "visualize", - "visualize", - "render inline visualizations", - ); - let plugin_root = plugin_skill_root_for_skill_path(&skill_path, "visualize@test", "visualize"); - - let supported_input = SkillsLoadInput::new( - cwd.path().abs(), - vec![plugin_root], - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let supported = skills_service - .snapshot_for_cwd( - &supported_input, - /*force_reload*/ false, - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - supported - .outcome() - .skills - .iter() - .any(|skill| skill.name == "visualize:visualize") - ); - - let unsupported_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let unsupported = skills_service - .snapshot_for_cwd( - &unsupported_input, - /*force_reload*/ false, - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - unsupported - .outcome() - .skills - .iter() - .all(|skill| skill.name != "visualize:visualize") - ); -} - #[tokio::test] async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() { let codex_home = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 88e656f730..b34aa8aa21 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -205,7 +205,6 @@ mod reload { ) -> ConfigOverrides { ConfigOverrides { cwd: Some(config.cwd.to_path_buf()), - host_capabilities: config.host_capabilities.clone(), model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()), service_tier: preserve_current_service_tier.then(|| config.service_tier.clone()), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index 99df15d8ab..b56531c90c 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -181,7 +181,6 @@ async fn apply_role_preserves_unspecified_keys() { .await; config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox")); config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper")); - config.host_capabilities = ["codex.inline_visualization".to_string()].into(); let role_path = write_role_config( &home, "effort-only.toml", @@ -203,10 +202,6 @@ async fn apply_role_preserves_unspecified_keys() { assert_eq!(config.model.as_deref(), Some("base-model")); assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); - assert_eq!( - config.host_capabilities, - ["codex.inline_visualization".to_string()].into() - ); assert_eq!( config.codex_linux_sandbox_exe, Some(PathBuf::from("/tmp/codex-linux-sandbox")) diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index a15dfd26f7..257a32733b 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -55,7 +55,6 @@ use std::sync::Arc; use tokio::sync::Mutex; use tokio::sync::watch; -use crate::config::HostCapabilities; use codex_rollout::state_db::StateDbHandle; #[derive(Clone, Debug)] @@ -81,7 +80,6 @@ pub struct ThreadConfigSnapshot { pub parent_thread_id: Option, pub thread_source: Option, pub originator: String, - pub host_capabilities: HostCapabilities, } /// Explains why `CodexThread::try_start_turn_if_idle` rejected an automatic diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index a575c94baa..1df2bb3e3f 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4741,12 +4741,7 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config() std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?; std::fs::write( plugin_root.join(".codex-plugin/plugin.json"), - r#"{ - "name": "sample", - "requires": { - "hostCapabilities": ["codex.inline_visualization"] - } -}"#, + r#"{"name":"sample"}"#, )?; std::fs::write( plugin_root.join(".mcp.json"), @@ -4813,7 +4808,6 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config() thread_layer_stack.effective_config().try_into()?, ConfigOverrides { cwd: Some(codex_home.path().to_path_buf()), - host_capabilities: ["codex.inline_visualization".to_string()].into(), ..Default::default() }, codex_home.abs(), @@ -4827,10 +4821,6 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config() let mcp_config = config.to_mcp_config(&plugins_manager).await; let configured_servers = mcp_config.mcp_server_catalog.configured_servers(); - assert_eq!( - config.host_capabilities, - ["codex.inline_visualization".to_string()].into() - ); assert_eq!( configured_servers.get("sample"), Some(&http_mcp("https://sample.example/mcp")) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 01cba62a54..5cd6748c05 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -120,7 +120,6 @@ use rmcp::model::UrlElicitationCapability; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; -use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::io::ErrorKind; @@ -611,9 +610,6 @@ pub enum ThreadStoreConfig { InMemory { id: String }, } -/// Opaque host features available to runtime plugins for one thread. -pub type HostCapabilities = BTreeSet; - /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { @@ -624,12 +620,6 @@ pub struct Config { /// Warnings collected during config load that should be shown on startup. pub startup_warnings: Vec, - /// Host features available to runtime plugins for this thread. - /// - /// This is supplied by the embedding client rather than config.toml and is - /// intentionally not persisted with the thread. - pub host_capabilities: HostCapabilities, - /// Optional override of model selection. pub model: Option, @@ -1508,14 +1498,12 @@ impl Config { /// Build the plugin-manager input from the effective config. pub fn plugins_config_input(&self) -> PluginsConfigInput { - let mut input = PluginsConfigInput::new( + PluginsConfigInput::new( self.config_layer_stack.clone(), self.features.enabled(Feature::Plugins), self.features.enabled(Feature::RemotePlugin), self.chatgpt_base_url.clone(), - ); - input.host_capabilities = self.host_capabilities.iter().cloned().collect(); - input + ) } /// Applies managed MCP requirements to servers supplied by one plugin. @@ -1689,7 +1677,6 @@ impl Config { ConfigOverrides { cwd: Some(self.cwd.to_path_buf()), default_zsh_path, - host_capabilities: self.host_capabilities.clone(), ..Default::default() }, refreshed_config.codex_home.clone(), @@ -2439,8 +2426,6 @@ pub struct ConfigOverrides { /// Explicit absolute runtime workspace roots for this session. When set, /// this is the full runtime root list rather than an additive override. pub workspace_roots: Option>, - /// Opaque host features supplied by the embedding client for this thread. - pub host_capabilities: HostCapabilities, } fn dedupe_absolute_paths(paths: &mut Vec) { @@ -3031,7 +3016,6 @@ impl Config { bypass_hook_trust, additional_writable_roots, workspace_roots: workspace_roots_override, - host_capabilities, } = overrides; let bypass_hook_trust = bypass_hook_trust.unwrap_or_default(); @@ -3815,7 +3799,6 @@ impl Config { workspace_roots: workspace_roots.clone(), workspace_roots_explicit, startup_warnings, - host_capabilities, permissions: Permissions { approval_policy: constrained_approval_policy.value, permission_profile_state, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 73ed28341a..a35b717cad 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -50,7 +50,6 @@ struct AccessibleConnectorsCacheKey { account_id: Option, chatgpt_user_id: Option, is_workspace_account: bool, - host_capabilities: crate::config::HostCapabilities, } #[derive(Clone)] @@ -360,7 +359,6 @@ fn accessible_connectors_cache_key( account_id, chatgpt_user_id, is_workspace_account, - host_capabilities: config.host_capabilities.clone(), } } diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 26bf75370b..e28f0f204e 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -224,12 +224,6 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal .expect("config should load"); let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true); let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None); - let mut other_host_config = config.clone(); - other_host_config - .host_capabilities - .insert("codex.inline_visualization".to_string()); - let other_host_cache_key = - accessible_connectors_cache_key(&other_host_config, /*auth*/ None); let tools = vec![ codex_app_tool( "calendar_list_events", @@ -247,10 +241,6 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal let cached = with_accessible_connectors_cache_cleared(|| { refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools); - assert_eq!( - read_cached_accessible_connectors(&other_host_cache_key), - None - ); read_cached_accessible_connectors(&cache_key).expect("cache should be populated") }); diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 35106d5253..7c59ec22b7 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -199,7 +199,6 @@ impl SessionConfiguration { parent_thread_id: self.parent_thread_id, thread_source: self.thread_source.clone(), originator: self.originator.clone(), - host_capabilities: self.original_config_do_not_use.host_capabilities.clone(), } } diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 5b34f4f9d8..aaf65baad0 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -29,7 +29,6 @@ use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use core_test_support::wait_for_mcp_server; -use pretty_assertions::assert_eq; use tempfile::TempDir; use wiremock::MockServer; @@ -275,109 +274,6 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<() Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn host_capabilities_gate_plugin_guidance_and_tools_all_of() -> Result<()> { - skip_if_no_network!(Ok(())); - let rmcp_test_server_bin = match stdio_server_bin() { - Ok(bin) => bin, - Err(err) => { - eprintln!("test_stdio_server binary not available, skipping test: {err}"); - return Ok(()); - } - }; - - for (host_capabilities, expected_supported) in [ - (Vec::new(), false), - (vec!["capability-a"], false), - (vec!["capability-b"], false), - (vec!["capability-a", "capability-b"], true), - ( - vec!["capability-a", "capability-b", "capability-extra"], - true, - ), - ] { - let server = start_mock_server().await; - let mock = mount_plugin_tool_search_turn(&server).await; - let codex_home = Arc::new(TempDir::new()?); - write_plugin_skill_plugin(codex_home.as_ref()); - write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin); - std::fs::write( - sample_plugin_root(codex_home.as_ref()).join(".codex-plugin/plugin.json"), - format!( - r#"{{ - "name": "{SAMPLE_PLUGIN_DISPLAY_NAME}", - "description": "{SAMPLE_PLUGIN_DESCRIPTION}", - "requires": {{ - "hostCapabilities": ["capability-a", "capability-b"] - }} -}}"# - ), - )?; - - let mut builder = test_codex() - .with_home(codex_home) - .with_auth(CodexAuth::from_api_key("Test API Key")) - .with_config(move |config| { - config.host_capabilities = host_capabilities - .iter() - .map(std::string::ToString::to_string) - .collect(); - }); - let test_codex = builder - .build(&server) - .await - .expect("create new conversation"); - let codex = Arc::clone(&test_codex.codex); - let startup = loop { - let event = codex - .next_event() - .await - .expect("stream ended unexpectedly while waiting for MCP startup"); - if let EventMsg::McpStartupComplete(startup) = event.msg { - break startup; - } - }; - let sample_ready = startup.ready.iter().any(|server| server == "sample"); - let sample_reported = sample_ready - || startup - .failed - .iter() - .any(|failure| failure.server == "sample") - || startup.cancelled.iter().any(|server| server == "sample"); - assert_eq!(sample_reported, expected_supported); - assert_eq!(sample_ready, expected_supported); - - codex - .submit(Op::UserInput { - items: vec![codex_protocol::user_input::UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - responsesapi_client_metadata: None, - additional_context: Default::default(), - thread_settings: Default::default(), - }) - .await?; - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - let requests = mock.requests(); - let developer_text = requests[0].message_input_texts("developer").join("\n\n"); - assert_eq!( - developer_text.contains("sample:sample-search: inspect sample data"), - expected_supported - ); - assert_eq!( - developer_text.contains(SAMPLE_PLUGIN_DESCRIPTION), - expected_supported - ); - let (_, echo_tool) = searched_plugin_tools(&requests[1]); - assert_eq!(echo_tool.is_some(), expected_supported); - } - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f7063c035a..1d9fd2e56d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -446,7 +446,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result ephemeral: ephemeral.then_some(true), bypass_hook_trust: bypass_hook_trust.then_some(true), additional_writable_roots: add_dir, - host_capabilities: Default::default(), }; let build_config = |overrides| { @@ -574,7 +573,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }; diff --git a/codex-rs/ext/mcp/src/executor_plugin.rs b/codex-rs/ext/mcp/src/executor_plugin.rs index 7e3e8c0028..98cfcd3615 100644 --- a/codex-rs/ext/mcp/src/executor_plugin.rs +++ b/codex-rs/ext/mcp/src/executor_plugin.rs @@ -24,7 +24,6 @@ mod provider; struct SelectedPluginMetadata { plugin_id: String, plugin_display_name: String, - required_host_capabilities: Vec, servers: Vec<(String, codex_config::McpServerConfig)>, connector_ids: Vec, } @@ -113,12 +112,6 @@ impl SelectedExecutorPluginMcpContributor { Some(SelectedPluginMetadata { plugin_id: plugin.plugin().selected_root_id().to_string(), plugin_display_name: plugin.plugin().manifest().display_name().to_string(), - required_host_capabilities: plugin - .plugin() - .manifest() - .requires - .host_capabilities - .clone(), servers, connector_ids, }) @@ -178,13 +171,6 @@ impl McpServerContributor for SelectedExecutorPluginMcpContributor { let Some(plugin) = self.metadata_for_root(&state, selected_root).await else { continue; }; - if !plugin - .required_host_capabilities - .iter() - .all(|capability| context.config().host_capabilities.contains(capability)) - { - continue; - } let mut servers = plugin.servers.iter().cloned().collect::>(); context .config() diff --git a/codex-rs/ext/mcp/src/executor_plugin/provider_tests.rs b/codex-rs/ext/mcp/src/executor_plugin/provider_tests.rs index 2bc4a27592..bc59e104a0 100644 --- a/codex-rs/ext/mcp/src/executor_plugin/provider_tests.rs +++ b/codex-rs/ext/mcp/src/executor_plugin/provider_tests.rs @@ -17,7 +17,6 @@ use codex_plugin::ResolvedPlugin; use codex_plugin::manifest::PluginManifest; use codex_plugin::manifest::PluginManifestMcpServers; use codex_plugin::manifest::PluginManifestPaths; -use codex_plugin::manifest::PluginManifestRequirements; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; @@ -401,7 +400,6 @@ fn resolved_plugin( version: None, description: None, keywords: Vec::new(), - requires: PluginManifestRequirements::default(), paths: PluginManifestPaths { skills: Vec::new(), mcp_servers, diff --git a/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs b/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs index 5616337271..afb6b0b923 100644 --- a/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs +++ b/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs @@ -147,104 +147,6 @@ async fn selected_plugin_package_is_contributed_without_servers_or_connectors() Ok(()) } -#[tokio::test] -async fn selected_plugin_contributions_require_all_host_capabilities_after_caching() -> TestResult { - let codex_home = tempfile::tempdir()?; - let plugin_root = tempfile::tempdir()?; - std::fs::create_dir_all(plugin_root.path().join(".codex-plugin"))?; - std::fs::write( - plugin_root.path().join(".codex-plugin/plugin.json"), - r#"{ - "name": "gated-plugin", - "requires": { - "hostCapabilities": ["codex.inline_visualization", "codex.code_mode"] - }, - "apps": "./apps.json", - "interface": {"displayName": "Gated Plugin"} -}"#, - )?; - std::fs::write( - plugin_root.path().join(".mcp.json"), - r#"{"mcpServers":{"gated":{"command":"gated-command"}}}"#, - )?; - std::fs::write( - plugin_root.path().join("apps.json"), - r#"{"apps":{"demo":{"id":"connector_demo"}}}"#, - )?; - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .build() - .await?; - let mut builder = ExtensionRegistryBuilder::new(); - codex_mcp_extension::install_executor_plugins( - &mut builder, - Arc::new(EnvironmentManager::default_for_tests()), - ); - let registry = builder.build(); - let mut thread_init = ExtensionDataInit::new(); - thread_init.insert(vec![SelectedCapabilityRoot { - id: "selected-root".to_string(), - location: CapabilityRootLocation::Environment { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - path: PathUri::from_host_native_path(plugin_root.path())?, - }, - }]); - let thread_store = ExtensionData::new_with_init("test-thread", thread_init.clone()); - let available_environment_ids = vec![LOCAL_ENVIRONMENT_ID.to_string()]; - let contributor = ®istry.mcp_server_contributors()[0]; - - let unsupported = contributor - .contribute(McpServerContributionContext::for_step( - &config, - &thread_init, - &thread_store, - "test_originator", - &available_environment_ids, - )) - .await; - assert!(unsupported.is_empty()); - - config.host_capabilities = ["codex.inline_visualization".to_string()].into(); - let partially_supported = contributor - .contribute(McpServerContributionContext::for_step( - &config, - &thread_init, - &thread_store, - "test_originator", - &available_environment_ids, - )) - .await; - assert!(partially_supported.is_empty()); - - config.host_capabilities = [ - "codex.code_mode".to_string(), - "codex.inline_visualization".to_string(), - ] - .into(); - let supported = contributor - .contribute(McpServerContributionContext::for_step( - &config, - &thread_init, - &thread_store, - "test_originator", - &available_environment_ids, - )) - .await; - assert_eq!(supported.len(), 2); - assert!(matches!( - &supported[0], - McpServerContribution::SelectedPlugin { name, .. } if name == "gated" - )); - assert!(matches!( - &supported[1], - McpServerContribution::SelectedPluginPackage { connector_ids, .. } - if connector_ids.as_slice() == ["connector_demo"] - )); - - Ok(()) -} - async fn selected_plugin_contributions( config: &Config, plugin_root: &std::path::Path, diff --git a/codex-rs/ext/skills/Cargo.toml b/codex-rs/ext/skills/Cargo.toml index 5256e63e45..35827ac578 100644 --- a/codex-rs/ext/skills/Cargo.toml +++ b/codex-rs/ext/skills/Cargo.toml @@ -15,7 +15,6 @@ workspace = true [dependencies] codex-core-skills = { workspace = true } -codex-core-plugins = { workspace = true } codex-exec-server = { workspace = true } codex-extension-api = { workspace = true } codex-mcp = { workspace = true } diff --git a/codex-rs/ext/skills/src/catalog.rs b/codex-rs/ext/skills/src/catalog.rs index d666537289..59977a47ca 100644 --- a/codex-rs/ext/skills/src/catalog.rs +++ b/codex-rs/ext/skills/src/catalog.rs @@ -1,6 +1,5 @@ use codex_core_skills::model::SkillDependencies; use codex_utils_path_uri::PathUri; -use std::collections::BTreeSet; /// Source authority that owns a skill package and must be used to read it. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -118,7 +117,6 @@ pub struct SkillCatalogEntry { pub dependencies: Option, pub enabled: bool, pub prompt_visible: bool, - pub(crate) required_host_capabilities: Vec, } impl SkillCatalogEntry { @@ -140,7 +138,6 @@ impl SkillCatalogEntry { dependencies: None, enabled: true, prompt_visible: true, - required_host_capabilities: Vec::new(), } } @@ -169,21 +166,6 @@ impl SkillCatalogEntry { self } - pub fn with_required_host_capabilities( - mut self, - required_host_capabilities: Vec, - ) -> Self { - self.required_host_capabilities = required_host_capabilities; - self - } - - /// Returns whether every host capability required by this package is available. - pub fn host_requirements_satisfied_by(&self, host_capabilities: &BTreeSet) -> bool { - self.required_host_capabilities - .iter() - .all(|capability| host_capabilities.contains(capability)) - } - pub(crate) fn rendered_path(&self) -> &str { self.display_path .as_deref() @@ -196,9 +178,6 @@ impl SkillCatalogEntry { pub struct SkillCatalog { pub entries: Vec, pub warnings: Vec, - /// Warnings whose visibility depends on the host capabilities available at projection time. - #[doc(hidden)] - pub host_capability_gated_warnings: Vec, } impl SkillCatalog { @@ -207,8 +186,6 @@ impl SkillCatalog { self.push_entry(entry); } self.warnings.extend(other.warnings); - self.host_capability_gated_warnings - .extend(other.host_capability_gated_warnings); } pub fn push_entry(&mut self, entry: SkillCatalogEntry) { @@ -222,33 +199,6 @@ impl SkillCatalog { self.entries.push(entry); } - - pub(crate) fn extend_warnings_with_host_requirements( - &mut self, - warnings: Vec, - required_host_capabilities: &[String], - ) { - if warnings.is_empty() { - return; - } - if required_host_capabilities.is_empty() { - self.warnings.extend(warnings); - } else { - self.host_capability_gated_warnings - .push(HostCapabilityGatedWarnings { - required_host_capabilities: required_host_capabilities.to_vec(), - warnings, - }); - } - } -} - -/// Warning group carried in raw catalogs until host capabilities are projected. -#[doc(hidden)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HostCapabilityGatedWarnings { - pub required_host_capabilities: Vec, - pub warnings: Vec, } /// Contents returned after resolving a skill resource through its owner. diff --git a/codex-rs/ext/skills/src/config.rs b/codex-rs/ext/skills/src/config.rs index 517e7b1342..3d903cafb8 100644 --- a/codex-rs/ext/skills/src/config.rs +++ b/codex-rs/ext/skills/src/config.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeSet; - /// Host-supplied configuration used by the skills extension. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SkillsExtensionConfig { @@ -9,6 +7,4 @@ pub struct SkillsExtensionConfig { pub bundled_skills_enabled: bool, /// Whether orchestrator-owned skills are eligible for discovery. pub orchestrator_skills_enabled: bool, - /// Opaque host features available to runtime plugins for this thread. - pub host_capabilities: BTreeSet, } diff --git a/codex-rs/ext/skills/src/extension.rs b/codex-rs/ext/skills/src/extension.rs index fed24ed0fa..a632ab6824 100644 --- a/codex-rs/ext/skills/src/extension.rs +++ b/codex-rs/ext/skills/src/extension.rs @@ -123,7 +123,6 @@ where include_bundled_skills: config.bundled_skills_enabled, include_orchestrator_skills: thread_state.orchestrator_skills_enabled(), mcp_resources: session_store.get::(), - host_capabilities: config.host_capabilities.clone(), }, &thread_state, ) @@ -161,7 +160,6 @@ where include_bundled_skills: config.bundled_skills_enabled, include_orchestrator_skills: false, mcp_resources: input.session_store.get::(), - host_capabilities: config.host_capabilities.clone(), }, ) .await; @@ -233,7 +231,6 @@ where include_bundled_skills: config.bundled_skills_enabled, include_orchestrator_skills: thread_state.orchestrator_skills_enabled(), mcp_resources: session_store.get::(), - host_capabilities: config.host_capabilities.clone(), }; let mut catalog = self.list_skills(query, &thread_state).await; if let Some(executor_skills) = turn_store.get::() { diff --git a/codex-rs/ext/skills/src/provider.rs b/codex-rs/ext/skills/src/provider.rs index 0ccd68b653..405592e934 100644 --- a/codex-rs/ext/skills/src/provider.rs +++ b/codex-rs/ext/skills/src/provider.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::future::Future; use std::pin::Pin; use std::sync::Arc; @@ -32,7 +31,6 @@ pub struct SkillListQuery { pub include_bundled_skills: bool, pub include_orchestrator_skills: bool, pub mcp_resources: Option>, - pub host_capabilities: BTreeSet, } #[derive(Clone, Debug)] diff --git a/codex-rs/ext/skills/src/provider/executor.rs b/codex-rs/ext/skills/src/provider/executor.rs index ce9925aa19..9938035e92 100644 --- a/codex-rs/ext/skills/src/provider/executor.rs +++ b/codex-rs/ext/skills/src/provider/executor.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use codex_core_plugins::ExecutorPluginProvider; use codex_core_skills::loader::EnvironmentSkillMetadata; use codex_core_skills::loader::load_environment_skills_from_root; use codex_exec_server::EnvironmentManager; @@ -27,7 +26,6 @@ use crate::provider::SkillSearchRequest; #[derive(Clone, Debug)] pub struct ExecutorSkillProvider { environment_manager: Arc, - plugin_provider: ExecutorPluginProvider, restriction_product: Option, } @@ -37,7 +35,6 @@ impl ExecutorSkillProvider { restriction_product: Option, ) -> Self { Self { - plugin_provider: ExecutorPluginProvider::new(Arc::clone(&environment_manager)), environment_manager, restriction_product, } @@ -49,26 +46,7 @@ impl SkillProvider for ExecutorSkillProvider { Box::pin(async move { let mut catalog = SkillCatalog::default(); for selected_root in query.executor_roots { - let selected_root_id = selected_root.id.clone(); - let required_host_capabilities = match self - .plugin_provider - .resolve_bound(&selected_root) - .await - { - Ok(Some(plugin)) => plugin - .plugin() - .manifest() - .requires - .host_capabilities - .clone(), - Ok(None) => Vec::new(), - Err(err) => { - catalog.warnings.push(format!( - "Selected capability root `{selected_root_id}` could not be resolved as a plugin: {err}" - )); - continue; - } - }; + let selected_root_id = selected_root.id; let CapabilityRootLocation::Environment { environment_id, path, @@ -77,12 +55,9 @@ impl SkillProvider for ExecutorSkillProvider { SkillAuthority::new(SkillSourceKind::Executor, selected_root_id.clone()); let Some(environment) = self.environment_manager.get_environment(&environment_id) else { - catalog.extend_warnings_with_host_requirements( - vec![format!( - "Selected capability root `{selected_root_id}` references unavailable environment `{environment_id}`." - )], - &required_host_capabilities, - ); + catalog.warnings.push(format!( + "Selected capability root `{selected_root_id}` references unavailable environment `{environment_id}`." + )); continue; }; let file_system = environment.get_filesystem(); @@ -92,17 +67,13 @@ impl SkillProvider for ExecutorSkillProvider { self.restriction_product, ) .await; - catalog.extend_warnings_with_host_requirements( - outcome.warnings, - &required_host_capabilities, - ); + catalog.warnings.extend(outcome.warnings); for skill in outcome.skills { catalog.push_entry(catalog_entry_from_skill( &skill, authority.clone(), &selected_root_id, &environment_id, - &required_host_capabilities, )); } } @@ -162,7 +133,6 @@ fn catalog_entry_from_skill( authority: SkillAuthority, selected_root_id: &str, environment_id: &str, - required_host_capabilities: &[String], ) -> SkillCatalogEntry { let skill_path = skill.path_to_skills_md.inferred_native_path_string(); let normalized_path = match skill.path_to_skills_md.infer_path_convention() { @@ -186,8 +156,7 @@ fn catalog_entry_from_skill( ) .with_short_description(skill.short_description.clone()) .with_display_path(display_path) - .with_dependencies(skill.dependencies.clone()) - .with_required_host_capabilities(required_host_capabilities.to_vec()); + .with_dependencies(skill.dependencies.clone()); if skill.allows_implicit_invocation() { entry diff --git a/codex-rs/ext/skills/src/provider/host.rs b/codex-rs/ext/skills/src/provider/host.rs index a662b22996..2cee694216 100644 --- a/codex-rs/ext/skills/src/provider/host.rs +++ b/codex-rs/ext/skills/src/provider/host.rs @@ -95,7 +95,6 @@ fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog { ) }) .collect(), - ..Default::default() }; for (skill, enabled) in outcome.skills_with_enabled() { diff --git a/codex-rs/ext/skills/src/state.rs b/codex-rs/ext/skills/src/state.rs index c87ee50fcf..2094f34496 100644 --- a/codex-rs/ext/skills/src/state.rs +++ b/codex-rs/ext/skills/src/state.rs @@ -78,26 +78,13 @@ impl SkillsThreadState { mut query: SkillListQuery, ) -> SkillCatalog { let roots = std::mem::take(&mut query.executor_roots); - let host_capabilities = query.host_capabilities.clone(); let mut catalog = SkillCatalog::default(); for root in roots { query.executor_roots = vec![root.clone()]; - let mut root_catalog = self - .executor_root_catalog(providers, root, query.clone()) - .await; - root_catalog - .entries - .retain(|entry| entry.host_requirements_satisfied_by(&host_capabilities)); - for gated_warnings in std::mem::take(&mut root_catalog.host_capability_gated_warnings) { - if gated_warnings - .required_host_capabilities - .iter() - .all(|capability| host_capabilities.contains(capability)) - { - root_catalog.warnings.extend(gated_warnings.warnings); - } - } - catalog.extend(root_catalog); + catalog.extend( + self.executor_root_catalog(providers, root, query.clone()) + .await, + ); } catalog } diff --git a/codex-rs/ext/skills/src/tools/mod.rs b/codex-rs/ext/skills/src/tools/mod.rs index ae32131614..0d05064afb 100644 --- a/codex-rs/ext/skills/src/tools/mod.rs +++ b/codex-rs/ext/skills/src/tools/mod.rs @@ -73,7 +73,6 @@ impl SkillToolContext { include_bundled_skills: false, include_orchestrator_skills: true, mcp_resources: self.mcp_resources.clone(), - host_capabilities: self.thread_state.config().host_capabilities, }), ) .await diff --git a/codex-rs/ext/skills/tests/executor_file_system_authority.rs b/codex-rs/ext/skills/tests/executor_file_system_authority.rs index ceaf9af1ad..06bf062eac 100644 --- a/codex-rs/ext/skills/tests/executor_file_system_authority.rs +++ b/codex-rs/ext/skills/tests/executor_file_system_authority.rs @@ -265,7 +265,6 @@ async fn selected_root_id_distinguishes_identical_executor_paths() { include_bundled_skills: true, include_orchestrator_skills: false, mcp_resources: None, - host_capabilities: Default::default(), }) .await .expect("list executor skills"); @@ -300,79 +299,6 @@ async fn selected_root_id_distinguishes_identical_executor_paths() { std::fs::remove_dir_all(test_root).expect("remove skill directory"); } -#[tokio::test] -async fn executor_plugin_skills_preserve_all_host_capability_requirements() { - let test_root = create_local_skill_root("host-requirements").expect("create local skill root"); - std::fs::create_dir_all(test_root.join(".codex-plugin")).expect("create manifest directory"); - std::fs::write( - test_root.join(".codex-plugin/plugin.json"), - r#"{ - "name": "gated-skill-plugin", - "requires": { - "hostCapabilities": ["codex.inline_visualization", "codex.code_mode"] - } -}"#, - ) - .expect("write plugin manifest"); - std::fs::create_dir_all(test_root.join("broken")).expect("create broken skill directory"); - std::fs::write(test_root.join("broken/SKILL.md"), "missing frontmatter") - .expect("write broken skill"); - let provider = ExecutorSkillProvider::new_with_restriction_product( - Arc::new(EnvironmentManager::default_for_tests()), - /*restriction_product*/ None, - ); - let catalog = provider - .list(SkillListQuery { - turn_id: "turn-1".to_string(), - executor_roots: vec![SelectedCapabilityRoot { - id: "selected-root".to_string(), - location: CapabilityRootLocation::Environment { - environment_id: "local".to_string(), - path: PathUri::from_host_native_path(&test_root).expect("skill root URI"), - }, - }], - host_snapshot: None, - include_host_skills: false, - include_bundled_skills: true, - include_orchestrator_skills: false, - mcp_resources: None, - host_capabilities: Default::default(), - }) - .await - .expect("list executor skills"); - let [entry] = catalog.entries.as_slice() else { - panic!("expected one executor skill"); - }; - - assert!(catalog.warnings.is_empty()); - let [gated_warnings] = catalog.host_capability_gated_warnings.as_slice() else { - panic!("expected one host-gated warning group"); - }; - assert_eq!( - gated_warnings.required_host_capabilities, - vec![ - "codex.inline_visualization".to_string(), - "codex.code_mode".to_string(), - ] - ); - assert_eq!(gated_warnings.warnings.len(), 1); - assert!(!entry.host_requirements_satisfied_by(&Default::default())); - assert!( - !entry.host_requirements_satisfied_by(&["codex.inline_visualization".to_string()].into()) - ); - assert!( - entry.host_requirements_satisfied_by( - &[ - "codex.code_mode".to_string(), - "codex.inline_visualization".to_string(), - ] - .into() - ) - ); - - std::fs::remove_dir_all(test_root).expect("remove skill directory"); -} - fn create_local_skill_root(label: &str) -> io::Result { let id = NEXT_TEST_ROOT_ID.fetch_add(1, Ordering::Relaxed); let test_root = std::env::temp_dir().join(format!( diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 7107ae9134..76d0df7954 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -158,7 +158,6 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach "lint-fix/SKILL.md", )], warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::clone(&read_requests), list_calls: Some(Arc::clone(&list_calls)), @@ -336,196 +335,6 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach Ok(()) } -#[tokio::test] -async fn selected_executor_skills_require_all_host_capabilities_after_caching() -> TestResult { - let list_calls = Arc::new(AtomicUsize::new(0)); - let executor_provider = Arc::new(StaticSkillProvider { - catalog: SkillCatalog { - entries: vec![ - test_entry( - SkillSourceKind::Executor, - "env-1", - "executor/visualize", - "visualize/SKILL.md", - ) - .with_required_host_capabilities(vec![ - "codex.inline_visualization".to_string(), - "codex.code_mode".to_string(), - ]), - ], - warnings: Vec::new(), - host_capability_gated_warnings: vec![ - codex_skills_extension::catalog::HostCapabilityGatedWarnings { - required_host_capabilities: vec![ - "codex.inline_visualization".to_string(), - "codex.code_mode".to_string(), - ], - warnings: vec!["gated plugin skill warning".to_string()], - }, - ], - }, - read_requests: Arc::new(Mutex::new(Vec::new())), - list_calls: Some(Arc::clone(&list_calls)), - fail_first_list: false, - }); - let providers = SkillProviders::new().with_executor_provider(executor_provider); - let (event_tx, event_rx) = std::sync::mpsc::channel(); - let mut builder = - ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); - install_with_providers(&mut builder, providers, skills_extension_config); - let registry = builder.build(); - let session_store = ExtensionData::new("session"); - let thread_store = ExtensionData::new("thread"); - let selected_roots = vec![SelectedCapabilityRoot { - id: "visualize".to_string(), - location: CapabilityRootLocation::Environment { - environment_id: "env-1".to_string(), - path: PathUri::parse("file:///skills/visualize").expect("skill root URI"), - }, - }]; - let session_source = SessionSource::Cli; - let config = default_config(); - registry.thread_lifecycle_contributors()[0] - .on_thread_start(ThreadStartInput { - config: &config, - session_source: &session_source, - persistent_thread_state_available: true, - environments: &[], - session_store: &session_store, - thread_store: &thread_store, - }) - .await; - let turn_environment = TurnEnvironmentSelection { - environment_id: "env-1".to_string(), - cwd: PathUri::parse("file:///workspace").expect("cwd URI"), - }; - - let unsupported_turn_store = ExtensionData::new("turn-1"); - let unsupported = registry.context_contributors()[0] - .contribute_world_state(WorldStateContributionInput { - thread_id: codex_protocol::ThreadId::new(), - turn_id: "turn-1", - environments: std::slice::from_ref(&turn_environment), - ready_selected_capability_roots: &selected_roots, - session_store: &session_store, - thread_store: &thread_store, - turn_store: &unsupported_turn_store, - }) - .await; - assert!( - unsupported[0] - .render_diff(PreviousWorldStateSection::Absent) - .is_none() - ); - assert!(unsupported[0].snapshot()["body"].is_null()); - let unsupported_fragments = registry.turn_input_contributors()[0] - .contribute( - TurnInputContext { - turn_id: "turn-1".to_string(), - user_input: Vec::new(), - environments: Vec::new(), - }, - &session_store, - &thread_store, - &unsupported_turn_store, - ) - .await; - assert!(unsupported_fragments.is_empty()); - assert!(matches!( - event_rx.try_recv(), - Err(std::sync::mpsc::TryRecvError::Empty) - )); - - let mut partial_config = config.clone(); - partial_config.host_capabilities = ["codex.inline_visualization".to_string()].into(); - registry.config_contributors()[0].on_config_changed( - &session_store, - &thread_store, - &config, - &partial_config, - ); - let partial_turn_store = ExtensionData::new("turn-2"); - let partially_supported = registry.context_contributors()[0] - .contribute_world_state(WorldStateContributionInput { - thread_id: codex_protocol::ThreadId::new(), - turn_id: "turn-2", - environments: std::slice::from_ref(&turn_environment), - ready_selected_capability_roots: &selected_roots, - session_store: &session_store, - thread_store: &thread_store, - turn_store: &partial_turn_store, - }) - .await; - assert!(partially_supported[0].snapshot()["body"].is_null()); - let partially_supported_fragments = registry.turn_input_contributors()[0] - .contribute( - TurnInputContext { - turn_id: "turn-2".to_string(), - user_input: Vec::new(), - environments: Vec::new(), - }, - &session_store, - &thread_store, - &partial_turn_store, - ) - .await; - assert!(partially_supported_fragments.is_empty()); - assert!(matches!( - event_rx.try_recv(), - Err(std::sync::mpsc::TryRecvError::Empty) - )); - - let mut supported_config = partial_config.clone(); - supported_config.host_capabilities = [ - "codex.code_mode".to_string(), - "codex.inline_visualization".to_string(), - ] - .into(); - registry.config_contributors()[0].on_config_changed( - &session_store, - &thread_store, - &partial_config, - &supported_config, - ); - let supported_turn_store = ExtensionData::new("turn-3"); - let supported = registry.context_contributors()[0] - .contribute_world_state(WorldStateContributionInput { - thread_id: codex_protocol::ThreadId::new(), - turn_id: "turn-3", - environments: &[turn_environment], - ready_selected_capability_roots: &selected_roots, - session_store: &session_store, - thread_store: &thread_store, - turn_store: &supported_turn_store, - }) - .await; - assert!( - supported[0].snapshot()["body"] - .as_str() - .is_some_and(|body| body.contains("visualize")) - ); - let supported_fragments = registry.turn_input_contributors()[0] - .contribute( - TurnInputContext { - turn_id: "turn-3".to_string(), - user_input: Vec::new(), - environments: Vec::new(), - }, - &session_store, - &thread_store, - &supported_turn_store, - ) - .await; - assert!(supported_fragments.is_empty()); - let EventMsg::Warning(warning) = event_rx.try_recv()?.msg else { - panic!("expected gated warning for supported plugin"); - }; - assert_eq!(warning.message, "gated plugin skill warning"); - assert_eq!(1, list_calls.load(Ordering::Relaxed)); - - Ok(()) -} - #[tokio::test] async fn default_context_truncates_catalog_descriptions() -> TestResult { let description = "x".repeat(1_025); @@ -541,7 +350,6 @@ async fn default_context_truncates_catalog_descriptions() -> TestResult { catalog: SkillCatalog { entries: vec![entry], warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, @@ -592,7 +400,6 @@ async fn skills_list_truncates_catalog_descriptions_in_tool_output() -> TestResu catalog: SkillCatalog { entries: vec![entry], warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, @@ -663,7 +470,6 @@ async fn orchestrator_catalog_snapshot_caches_failure() -> TestResult { "skill://orchestrator/first/SKILL.md", )], warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: Some(Arc::clone(&list_calls)), @@ -745,7 +551,6 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te }) .collect(), warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::clone(&read_requests), list_calls: None, @@ -846,7 +651,6 @@ async fn prompt_hidden_skill_can_still_be_invoked() -> TestResult { .hidden_from_prompt(), ], warnings: Vec::new(), - ..Default::default() }, read_requests: Arc::clone(&read_requests), list_calls: None, @@ -977,7 +781,6 @@ struct TestConfig { include_instructions: bool, bundled_skills_enabled: bool, orchestrator_skills_enabled: bool, - host_capabilities: std::collections::BTreeSet, } fn default_config() -> TestConfig { @@ -985,7 +788,6 @@ fn default_config() -> TestConfig { include_instructions: true, bundled_skills_enabled: true, orchestrator_skills_enabled: true, - host_capabilities: Default::default(), } } @@ -994,7 +796,6 @@ fn skills_extension_config(config: &TestConfig) -> SkillsExtensionConfig { include_instructions: config.include_instructions, bundled_skills_enabled: config.bundled_skills_enabled, orchestrator_skills_enabled: config.orchestrator_skills_enabled, - host_capabilities: config.host_capabilities.clone(), } } diff --git a/codex-rs/plugin/src/host_capabilities.rs b/codex-rs/plugin/src/host_capabilities.rs new file mode 100644 index 0000000000..f736350e33 --- /dev/null +++ b/codex-rs/plugin/src/host_capabilities.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeSet; + +/// Opaque host features available to runtime plugin contributions. +/// +/// Capability names are case-sensitive. Construction trims surrounding +/// whitespace, drops empty names, and deduplicates values deterministically. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct HostCapabilities(BTreeSet); + +impl HostCapabilities { + pub fn from_names(names: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self( + names + .into_iter() + .filter_map(|name| { + let name = name.as_ref().trim(); + (!name.is_empty()).then(|| name.to_string()) + }) + .collect(), + ) + } + + pub fn contains(&self, capability: &str) -> bool { + self.0.contains(capability) + } + + pub fn insert(&mut self, capability: impl AsRef) -> bool { + let capability = capability.as_ref().trim(); + !capability.is_empty() && self.0.insert(capability.to_string()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(String::as_str) + } +} + +#[cfg(test)] +#[path = "host_capabilities_tests.rs"] +mod tests; diff --git a/codex-rs/plugin/src/host_capabilities_tests.rs b/codex-rs/plugin/src/host_capabilities_tests.rs new file mode 100644 index 0000000000..e0e5170dea --- /dev/null +++ b/codex-rs/plugin/src/host_capabilities_tests.rs @@ -0,0 +1,14 @@ +use pretty_assertions::assert_eq; + +use super::HostCapabilities; + +#[test] +fn capability_names_are_normalized_deterministically() { + let capabilities = + HostCapabilities::from_names([" capability-b ", "", "capability-a", "capability-b"]); + + assert_eq!( + capabilities.iter().collect::>(), + vec!["capability-a", "capability-b"] + ); +} diff --git a/codex-rs/plugin/src/lib.rs b/codex-rs/plugin/src/lib.rs index 8a00d8c5ef..57229da4b2 100644 --- a/codex-rs/plugin/src/lib.rs +++ b/codex-rs/plugin/src/lib.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; pub use codex_utils_plugins::mention_syntax; pub use codex_utils_plugins::plugin_namespace_for_skill_path; +mod host_capabilities; mod load_outcome; pub mod manifest; mod plugin_id; @@ -12,6 +13,7 @@ mod provider; use codex_config::HookEventsToml; use codex_utils_absolute_path::AbsolutePathBuf; +pub use host_capabilities::HostCapabilities; pub use load_outcome::EffectiveSkillRoots; pub use load_outcome::LoadedPlugin; pub use load_outcome::PluginLoadOutcome; diff --git a/codex-rs/plugin/src/load_outcome.rs b/codex-rs/plugin/src/load_outcome.rs index 24700e3dcc..1580cdd3cc 100644 --- a/codex-rs/plugin/src/load_outcome.rs +++ b/codex-rs/plugin/src/load_outcome.rs @@ -6,9 +6,11 @@ use codex_utils_plugins::PluginSkillRoot; use crate::AppConnectorId; use crate::AppDeclaration; +use crate::HostCapabilities; use crate::PluginCapabilitySummary; use crate::PluginHookSource; use crate::app_connector_ids_from_declarations; +use crate::manifest::PluginManifestRequirements; const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024; @@ -21,8 +23,8 @@ pub struct LoadedPlugin { pub manifest_description: Option, pub root: AbsolutePathBuf, pub enabled: bool, - pub required_host_capabilities: Vec, - pub missing_host_capabilities: Vec, + pub required_host_capabilities: HostCapabilities, + pub missing_host_capabilities: HostCapabilities, pub skill_roots: Vec, pub disabled_skill_paths: HashSet, pub has_enabled_skills: bool, @@ -42,13 +44,11 @@ impl LoadedPlugin { self.manifest_name.as_deref().unwrap_or(&self.config_name) } - pub fn apply_host_capabilities(&mut self, host_capabilities: &HashSet) { - self.missing_host_capabilities = self - .required_host_capabilities - .iter() - .filter(|capability| !host_capabilities.contains(*capability)) - .cloned() - .collect::>(); + pub fn apply_host_capabilities(&mut self, host_capabilities: &HostCapabilities) { + self.missing_host_capabilities = PluginManifestRequirements { + host_capabilities: self.required_host_capabilities.clone(), + } + .missing_from(host_capabilities); } } @@ -243,8 +243,8 @@ mod tests { manifest_description: None, root: test_path(config_name), enabled: true, - required_host_capabilities: Vec::new(), - missing_host_capabilities: Vec::new(), + required_host_capabilities: HostCapabilities::default(), + missing_host_capabilities: HostCapabilities::default(), skill_roots, disabled_skill_paths: HashSet::new(), has_enabled_skills: true, diff --git a/codex-rs/plugin/src/manifest.rs b/codex-rs/plugin/src/manifest.rs index 544901564c..8ba2b27693 100644 --- a/codex-rs/plugin/src/manifest.rs +++ b/codex-rs/plugin/src/manifest.rs @@ -1,5 +1,7 @@ use codex_config::HooksFile; +use crate::HostCapabilities; + /// Parsed plugin metadata parameterized by its resource locator representation. /// /// Host loading uses absolute paths, while resolved packages replace them with @@ -19,7 +21,23 @@ pub struct PluginManifest { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PluginManifestRequirements { /// Every listed capability must be advertised by the host. - pub host_capabilities: Vec, + pub host_capabilities: HostCapabilities, +} + +impl PluginManifestRequirements { + pub fn is_satisfied_by(&self, host_capabilities: &HostCapabilities) -> bool { + self.host_capabilities + .iter() + .all(|capability| host_capabilities.contains(capability)) + } + + pub fn missing_from(&self, host_capabilities: &HostCapabilities) -> HostCapabilities { + HostCapabilities::from_names( + self.host_capabilities + .iter() + .filter(|capability| !host_capabilities.contains(capability)), + ) + } } /// Component resources declared by a plugin manifest. diff --git a/codex-rs/plugin/src/provider_tests.rs b/codex-rs/plugin/src/provider_tests.rs index 044d687e2a..3f5f9daabf 100644 --- a/codex-rs/plugin/src/provider_tests.rs +++ b/codex-rs/plugin/src/provider_tests.rs @@ -1,6 +1,7 @@ use super::PluginResourceLocator; use super::ResolvedPlugin; use super::ResolvedPluginError; +use crate::HostCapabilities; use crate::manifest::PluginManifest; use crate::manifest::PluginManifestHooks; use crate::manifest::PluginManifestInterface; @@ -44,7 +45,7 @@ fn environment_descriptor_binds_every_manifest_resource() { description: None, keywords: Vec::new(), requires: PluginManifestRequirements { - host_capabilities: vec!["codex.inline_visualization".to_string()], + host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]), }, paths: PluginManifestPaths { skills: vec![path_uri(&skills)], @@ -81,7 +82,7 @@ fn environment_descriptor_binds_every_manifest_resource() { description: None, keywords: Vec::new(), requires: PluginManifestRequirements { - host_capabilities: vec!["codex.inline_visualization".to_string()], + host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]), }, paths: PluginManifestPaths { skills: vec![resource("executor-1", &skills)], diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 02ac05ff08..c35daaa497 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -176,7 +176,6 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R let mut config = Config { config_layer_stack: ConfigLayerStack::default(), startup_warnings: Vec::new(), - host_capabilities: Default::default(), bypass_hook_trust: false, model, service_tier: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index c6e7963e4f..dc8262f7db 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -400,7 +400,6 @@ async fn connect_remote_app_server( client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) @@ -565,7 +564,6 @@ where client_version: env!("CARGO_PKG_VERSION").to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, }) diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 92b4909ab8..619163bc66 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -1069,7 +1069,6 @@ mod tests { client_version: "test".to_string(), experimental_api: true, mcp_server_openai_form_elicitation: false, - host_capabilities: Vec::new(), opt_out_notification_methods: Vec::new(), channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, })