From 1a9efd473b2cccfc7c82d442d5557be6974db133 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Wed, 10 Jun 2026 17:33:56 -0700 Subject: [PATCH] [codex] Remove redundant plugin app auth state (#27465) ## Summary - remove the redundant `needsAuth` field from `AppSummary` and generated app-server schemas - stop `plugin/read` from querying Apps MCP solely to hydrate unused connector auth state - preserve `plugin/install.appsNeedingAuth` membership and `app/list.isAccessible` as the authentication signals ## Why Codex App and TUI do not consume `plugin/read.plugin.apps[].needsAuth`. Hydrating it could establish an Apps MCP connection and discover tools on a cold `plugin/read` request, adding avoidable latency. The plugin APIs are still marked under development, so removing this wire field is preferable to retaining a misleading default. ## Verification - `just write-app-server-schema` - `just fmt` - `just test -p codex-app-server-protocol` - `just test -p codex-app-server plugin_install_uses_remote_apps_needing_auth_response` - `just test -p codex-app-server plugin_install_returns_apps_needing_auth` - `just test -p codex-app-server plugin_read_returns_plugin_details_with_bundle_contents` - `just test -p codex-tui plugin_detail_popup_snapshot_shows_install_actions_and_capability_summaries` - `$xin-build` simplify and debug reviews --- .../codex_app_server_protocol.schemas.json | 6 +- .../codex_app_server_protocol.v2.schemas.json | 6 +- .../schema/json/v2/PluginInstallResponse.json | 6 +- .../schema/json/v2/PluginReadResponse.json | 6 +- .../schema/typescript/v2/AppSummary.ts | 2 +- .../src/protocol/v2/apps.rs | 2 - codex-rs/app-server/README.md | 2 +- .../src/request_processors/plugins.rs | 64 +---- .../tests/suite/v2/plugin_install.rs | 3 - .../app-server/tests/suite/v2/plugin_read.rs | 263 ------------------ codex-rs/tui/src/chatwidget/tests/helpers.rs | 5 +- .../chatwidget/tests/popups_and_settings.rs | 4 +- 12 files changed, 13 insertions(+), 356 deletions(-) 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 fcf014d26..ba23f67a6 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 @@ -6433,15 +6433,11 @@ }, "name": { "type": "string" - }, - "needsAuth": { - "type": "boolean" } }, "required": [ "id", - "name", - "needsAuth" + "name" ], "type": "object" }, 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 ee0415e13..2ff8c8b55 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 @@ -711,15 +711,11 @@ }, "name": { "type": "string" - }, - "needsAuth": { - "type": "boolean" } }, "required": [ "id", - "name", - "needsAuth" + "name" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json index 2ca7fda46..b02af0bf5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallResponse.json @@ -21,15 +21,11 @@ }, "name": { "type": "string" - }, - "needsAuth": { - "type": "boolean" } }, "required": [ "id", - "name", - "needsAuth" + "name" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json index 5a4c2e5b0..40720a524 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -25,15 +25,11 @@ }, "name": { "type": "string" - }, - "needsAuth": { - "type": "boolean" } }, "required": [ "id", - "name", - "needsAuth" + "name" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts index 586c76f8f..3cdb17d70 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppSummary.ts @@ -5,4 +5,4 @@ /** * EXPERIMENTAL - app metadata summary for plugin responses. */ -export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, needsAuth: boolean, }; +export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs index 9f46525e6..9d4ccbd3a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs @@ -111,7 +111,6 @@ pub struct AppSummary { pub name: String, pub description: Option, pub install_url: Option, - pub needs_auth: bool, } impl From for AppSummary { @@ -121,7 +120,6 @@ impl From for AppSummary { name: value.name, description: value.description, install_url: value.install_url, - needs_auth: false, } } } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0532ffa70..42ac510a8 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -206,7 +206,7 @@ Example with notification opt-out: - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). -- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). +- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**). - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 246ca92be..08fc99d01 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -1035,14 +1035,7 @@ impl PluginRequestProcessor { } None => None, }; - let environment_manager = self.thread_manager.environment_manager(); - let app_summaries = load_plugin_app_summaries( - &config, - &outcome.plugin.apps, - Arc::clone(&environment_manager), - self.thread_manager.mcp_manager(), - ) - .await; + let app_summaries = load_plugin_app_summaries(&config, &outcome.plugin.apps).await; let visible_skills = outcome .plugin .skills @@ -1118,14 +1111,7 @@ impl PluginRequestProcessor { .cloned() .map(codex_plugin::AppConnectorId) .collect::>(); - let environment_manager = self.thread_manager.environment_manager(); - let app_summaries = load_plugin_app_summaries( - &config, - &plugin_apps, - Arc::clone(&environment_manager), - self.thread_manager.mcp_manager(), - ) - .await; + let app_summaries = load_plugin_app_summaries(&config, &plugin_apps).await; remote_plugin_detail_to_info(remote_detail, app_summaries) } }; @@ -1582,7 +1568,6 @@ impl PluginRequestProcessor { name: connector.name, description: connector.description, install_url: connector.install_url, - needs_auth: true, }) .collect() } @@ -1887,8 +1872,6 @@ impl PluginRequestProcessor { async fn load_plugin_app_summaries( config: &Config, plugin_apps: &[codex_plugin::AppConnectorId], - environment_manager: Arc, - mcp_manager: Arc, ) -> Vec { if plugin_apps.is_empty() { return Vec::new(); @@ -1906,49 +1889,9 @@ async fn load_plugin_app_summaries( }; let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps); - - let accessible_connectors = - match connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager( - config, - /*force_refetch*/ false, - environment_manager, - mcp_manager, - ) - .await - { - Ok(status) if status.codex_apps_ready => status.connectors, - Ok(_) => { - return plugin_connectors - .into_iter() - .map(AppSummary::from) - .collect(); - } - Err(err) => { - warn!("failed to load app auth state for plugin/read: {err:#}"); - return plugin_connectors - .into_iter() - .map(AppSummary::from) - .collect(); - } - }; - - let accessible_ids = accessible_connectors - .iter() - .map(|connector| connector.id.as_str()) - .collect::>(); - plugin_connectors .into_iter() - .map(|connector| { - let needs_auth = !accessible_ids.contains(connector.id.as_str()); - AppSummary { - id: connector.id, - name: connector.name, - description: connector.description, - install_url: connector.install_url, - needs_auth, - } - }) + .map(AppSummary::from) .collect() } @@ -1983,7 +1926,6 @@ fn plugin_apps_needing_auth( name: connector.name, description: connector.description, install_url: connector.install_url, - needs_auth: true, }) .collect() } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index ca490ad5e..1a39c1f8b 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -327,7 +327,6 @@ async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> { name: "alpha".to_string(), description: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), - needs_auth: true, }], } ); @@ -1001,7 +1000,6 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { name: "Alpha".to_string(), description: Some("Alpha connector".to_string()), install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), - needs_auth: true, }], } ); @@ -1089,7 +1087,6 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { name: "Alpha".to_string(), description: Some("Alpha connector".to_string()), install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), - needs_auth: true, }], } ); diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 4d8291a32..7f90bc125 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -1,6 +1,3 @@ -use std::borrow::Cow; -use std::sync::Arc; -use std::sync::Mutex as StdMutex; use std::time::Duration; use anyhow::Result; @@ -8,15 +5,6 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; -use axum::Json; -use axum::Router; -use axum::extract::State; -use axum::http::HeaderMap; -use axum::http::StatusCode; -use axum::http::Uri; -use axum::http::header::AUTHORIZATION; -use axum::routing::get; -use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppTemplateSummary; use codex_app_server_protocol::AppTemplateUnavailableReason; use codex_app_server_protocol::HookEventName; @@ -37,21 +25,8 @@ use codex_app_server_protocol::RequestId; use codex_config::types::AuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; -use rmcp::handler::server::ServerHandler; -use rmcp::model::JsonObject; -use rmcp::model::ListToolsResult; -use rmcp::model::Meta; -use rmcp::model::ServerCapabilities; -use rmcp::model::ServerInfo; -use rmcp::model::Tool; -use rmcp::model::ToolAnnotations; -use rmcp::transport::StreamableHttpServerConfig; -use rmcp::transport::StreamableHttpService; -use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; use serde_json::json; use tempfile::TempDir; -use tokio::net::TcpListener; -use tokio::task::JoinHandle; use tokio::time::timeout; use wiremock::Mock; use wiremock::MockServer; @@ -1483,104 +1458,11 @@ enabled = false response.plugin.apps[0].install_url.as_deref(), Some("https://chatgpt.com/apps/gmail/gmail") ); - assert_eq!(response.plugin.apps[0].needs_auth, true); assert_eq!(response.plugin.mcp_servers.len(), 1); assert_eq!(response.plugin.mcp_servers[0], "demo"); Ok(()) } -#[tokio::test] -async fn plugin_read_returns_app_needs_auth() -> Result<()> { - let connectors = vec![ - AppInfo { - id: "alpha".to_string(), - name: "Alpha".to_string(), - description: Some("Alpha connector".to_string()), - logo_url: Some("https://example.com/alpha.png".to_string()), - logo_url_dark: None, - distribution_channel: Some("featured".to_string()), - branding: None, - app_metadata: None, - labels: None, - install_url: None, - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - }, - AppInfo { - id: "beta".to_string(), - name: "Beta".to_string(), - description: Some("Beta connector".to_string()), - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - branding: None, - app_metadata: None, - labels: None, - install_url: None, - is_accessible: false, - is_enabled: true, - plugin_display_names: Vec::new(), - }, - ]; - let tools = vec![connector_tool("beta", "Beta App")?]; - let (server_url, server_handle) = start_apps_server(connectors, tools).await?; - - let codex_home = TempDir::new()?; - write_connectors_config(codex_home.path(), &server_url)?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - - let repo_root = TempDir::new()?; - write_plugin_marketplace( - repo_root.path(), - "debug", - "sample-plugin", - "./sample-plugin", - )?; - write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?; - let marketplace_path = - AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - - let mut mcp = TestAppServer::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_plugin_read_request(PluginReadParams { - marketplace_path: Some(marketplace_path), - remote_marketplace_name: None, - plugin_name: "sample-plugin".to_string(), - }) - .await?; - - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginReadResponse = to_response(response)?; - - assert_eq!( - response - .plugin - .apps - .iter() - .map(|app| (app.id.as_str(), app.needs_auth)) - .collect::>(), - vec![("alpha", true), ("beta", false)] - ); - - server_handle.abort(); - let _ = server_handle.await; - Ok(()) -} - #[tokio::test] async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> { let codex_home = TempDir::new()?; @@ -1846,151 +1728,6 @@ plugins = true Ok(()) } -#[derive(Clone)] -struct AppsServerState { - response: Arc>, -} - -#[derive(Clone)] -struct PluginReadMcpServer { - tools: Arc>>, -} - -impl ServerHandler for PluginReadMcpServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) - } - - fn list_tools( - &self, - _request: Option, - _context: rmcp::service::RequestContext, - ) -> impl std::future::Future> + Send + '_ - { - let tools = self.tools.clone(); - async move { - let tools = tools - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .clone(); - Ok(ListToolsResult { - tools, - next_cursor: None, - meta: None, - }) - } - } -} - -async fn start_apps_server( - connectors: Vec, - tools: Vec, -) -> Result<(String, JoinHandle<()>)> { - let state = Arc::new(AppsServerState { - response: Arc::new(StdMutex::new( - json!({ "apps": connectors, "next_token": null }), - )), - }); - let tools = Arc::new(StdMutex::new(tools)); - - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let mcp_service = StreamableHttpService::new( - { - let tools = tools.clone(); - move || { - Ok(PluginReadMcpServer { - tools: tools.clone(), - }) - } - }, - Arc::new(LocalSessionManager::default()), - StreamableHttpServerConfig::default(), - ); - let router = Router::new() - .route("/connectors/directory/list", get(list_directory_connectors)) - .route( - "/connectors/directory/list_workspace", - get(list_directory_connectors), - ) - .with_state(state) - .nest_service("/api/codex/ps/mcp", mcp_service); - - let handle = tokio::spawn(async move { - let _ = axum::serve(listener, router).await; - }); - - Ok((format!("http://{addr}"), handle)) -} - -async fn list_directory_connectors( - State(state): State>, - headers: HeaderMap, - uri: Uri, -) -> Result { - let bearer_ok = headers - .get(AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value == "Bearer chatgpt-token"); - let account_ok = headers - .get("chatgpt-account-id") - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value == "account-123"); - let external_logos_ok = uri - .query() - .is_some_and(|query| query.split('&').any(|pair| pair == "external_logos=true")); - - if !bearer_ok || !account_ok { - Err(StatusCode::UNAUTHORIZED) - } else if !external_logos_ok { - Err(StatusCode::BAD_REQUEST) - } else { - let response = state - .response - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .clone(); - Ok(Json(response)) - } -} - -fn connector_tool(connector_id: &str, connector_name: &str) -> Result { - let schema: JsonObject = serde_json::from_value(json!({ - "type": "object", - "additionalProperties": false - }))?; - let mut tool = Tool::new( - Cow::Owned(format!("connector_{connector_id}")), - Cow::Borrowed("Connector test tool"), - Arc::new(schema), - ); - tool.annotations = Some(ToolAnnotations::new().read_only(true)); - - let mut meta = Meta::new(); - meta.0 - .insert("connector_id".to_string(), json!(connector_id)); - meta.0 - .insert("connector_name".to_string(), json!(connector_name)); - tool.meta = Some(meta); - Ok(tool) -} - -fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> { - std::fs::write( - codex_home.join("config.toml"), - format!( - r#" -chatgpt_base_url = "{base_url}" -mcp_oauth_credentials_store = "file" - -[features] -plugins = true -connectors = true -"# - ), - ) -} - fn write_remote_plugin_catalog_config( codex_home: &std::path::Path, base_url: &str, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 214ca969a..995124b8c 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1414,7 +1414,7 @@ pub(super) fn plugins_test_detail( description: Option<&str>, skills: &[&str], hooks: &[(codex_app_server_protocol::HookEventName, usize)], - apps: &[(&str, bool)], + apps: &[&str], mcp_servers: &[&str], ) -> PluginDetail { PluginDetail { @@ -1449,12 +1449,11 @@ pub(super) fn plugins_test_detail( .collect(), apps: apps .iter() - .map(|(name, needs_auth)| AppSummary { + .map(|name| AppSummary { id: format!("{name}-id"), name: (*name).to_string(), description: Some(format!("{name} app")), install_url: Some(format!("https://example.test/{name}")), - needs_auth: *needs_auth, }) .collect(), app_templates: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index ae44cf00c..66015126a 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -654,7 +654,7 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa (codex_app_server_protocol::HookEventName::PreToolUse, 1), (codex_app_server_protocol::HookEventName::Stop, 2), ], - &[("Figma", true), ("Slack", false)], + &["Figma", "Slack"], &["figma-mcp", "docs-mcp"], ), }), @@ -698,7 +698,7 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() { (codex_app_server_protocol::HookEventName::PreToolUse, 1), (codex_app_server_protocol::HookEventName::Stop, 2), ], - &[("Figma", true), ("Slack", false)], + &["Figma", "Slack"], &["figma-mcp", "docs-mcp"], ), }),