From dd00efe78178b8c6c78c0ad78c20ec650c9d4cdf Mon Sep 17 00:00:00 2001 From: Leo Shimonaka Date: Thu, 16 Apr 2026 19:55:23 -0700 Subject: [PATCH] Move Computer Use tool suggestion to core (#18219) ## Summary Move the Computer Use tool suggestion into core Codex plugin discovery. Also search `openai-bundled` when listing suggested plugins, with test coverage for overlap between baked-in suggestions and `tool_suggest.discoverables`. ## Test plan Tested locally: - `cargo test -p codex-core list_tool_suggest_discoverable_plugins` --- codex-rs/core/src/plugins/discoverable.rs | 76 ++++++------ .../core/src/plugins/discoverable_tests.rs | 110 ++++++++++++++++++ codex-rs/core/src/plugins/manager.rs | 1 + codex-rs/core/src/plugins/mod.rs | 1 + 4 files changed, 154 insertions(+), 34 deletions(-) diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index e856815a7..6492bcb18 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -2,6 +2,7 @@ use anyhow::Context; use std::collections::HashSet; use tracing::warn; +use super::OPENAI_BUNDLED_MARKETPLACE_NAME; use super::OPENAI_CURATED_MARKETPLACE_NAME; use super::PluginCapabilitySummary; use super::PluginsManager; @@ -19,6 +20,12 @@ const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ "google-drive@openai-curated", "linear@openai-curated", "figma@openai-curated", + "computer-use@openai-bundled", +]; + +const TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST: &[&str] = &[ + OPENAI_BUNDLED_MARKETPLACE_NAME, + OPENAI_CURATED_MARKETPLACE_NAME, ]; pub(crate) async fn list_tool_suggest_discoverable_plugins( @@ -40,45 +47,46 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins( .list_marketplaces_for_config(config, &[]) .context("failed to list plugin marketplaces for tool suggestions")? .marketplaces; - let Some(curated_marketplace) = marketplaces - .into_iter() - .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) - else { - return Ok(Vec::new()); - }; - let curated_marketplace_name = curated_marketplace.name; - let mut discoverable_plugins = Vec::::new(); - for plugin in curated_marketplace.plugins { - if plugin.installed - || (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str()) - && !configured_plugin_ids.contains(plugin.id.as_str())) - { + for marketplace in marketplaces { + let marketplace_name = marketplace.name; + if !TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST.contains(&marketplace_name.as_str()) { continue; } - let plugin_id = plugin.id.clone(); - - match plugins_manager - .read_plugin_detail_for_marketplace_plugin(config, &curated_marketplace_name, plugin) - .await - { - Ok(plugin) => { - let plugin: PluginCapabilitySummary = plugin.into(); - discoverable_plugins.push(DiscoverablePluginInfo { - id: plugin.config_name, - name: plugin.display_name, - description: plugin.description, - has_skills: plugin.has_skills, - mcp_server_names: plugin.mcp_server_names, - app_connector_ids: plugin - .app_connector_ids - .into_iter() - .map(|connector_id| connector_id.0) - .collect(), - }); + for plugin in marketplace.plugins { + if plugin.installed + || (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str()) + && !configured_plugin_ids.contains(plugin.id.as_str())) + { + continue; + } + + let plugin_id = plugin.id.clone(); + + match plugins_manager + .read_plugin_detail_for_marketplace_plugin(config, &marketplace_name, plugin) + .await + { + Ok(plugin) => { + let plugin: PluginCapabilitySummary = plugin.into(); + discoverable_plugins.push(DiscoverablePluginInfo { + id: plugin.config_name, + name: plugin.display_name, + description: plugin.description, + has_skills: plugin.has_skills, + mcp_server_names: plugin.mcp_server_names, + app_connector_ids: plugin + .app_connector_ids + .into_iter() + .map(|connector_id| connector_id.0) + .collect(), + }); + } + Err(err) => { + warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}") + } } - Err(err) => warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}"), } } discoverable_plugins.sort_by(|left, right| { diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index f330418b0..c5f8e6927 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::plugins::PluginInstallRequest; use crate::plugins::test_support::load_plugins_config; +use crate::plugins::test_support::write_curated_plugin; use crate::plugins::test_support::write_curated_plugin_sha; use crate::plugins::test_support::write_file; use crate::plugins::test_support::write_openai_curated_marketplace; @@ -40,6 +41,115 @@ async fn list_tool_suggest_discoverable_plugins_returns_uninstalled_curated_plug ); } +#[tokio::test] +async fn list_tool_suggest_discoverable_plugins_deduplicates_allowlisted_configured_plugin() { + let codex_home = tempdir().expect("tempdir should succeed"); + let plugin_id = TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST + .iter() + .copied() + .find(|plugin_id| { + plugin_id + .rsplit_once('@') + .is_some_and(|(_plugin_name, marketplace_name)| { + marketplace_name == OPENAI_BUNDLED_MARKETPLACE_NAME + }) + }) + .expect("allowlist should include a bundled plugin"); + let (plugin_name, marketplace_name) = plugin_id + .rsplit_once('@') + .expect("plugin id should include a marketplace"); + let marketplace_root = codex_home + .path() + .join(format!(".tmp/marketplaces/{marketplace_name}")); + write_file( + &marketplace_root.join(".agents/plugins/marketplace.json"), + &format!( + r#"{{ + "name": "{marketplace_name}", + "plugins": [ + {{"name": "{plugin_name}", "source": {{"source": "local", "path": "./plugins/{plugin_name}"}}}} + ] +}} +"# + ), + ); + write_curated_plugin(&marketplace_root, plugin_name); + write_file( + &codex_home.path().join(crate::config::CONFIG_TOML_FILE), + &format!( + r#"[features] +plugins = true + +[marketplaces.{marketplace_name}] +source_type = "git" +source = "/tmp/{marketplace_name}" + +[tool_suggest] +discoverables = [{{ type = "plugin", id = "{plugin_id}" }}] +"# + ), + ); + + let config = load_plugins_config(codex_home.path()).await; + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); + + assert_eq!(discoverable_plugins.len(), 1); + assert_eq!(discoverable_plugins[0].id, plugin_id); +} + +#[tokio::test] +async fn list_tool_suggest_discoverable_plugins_ignores_missing_allowlisted_plugin() { + let codex_home = tempdir().expect("tempdir should succeed"); + let curated_root = crate::plugins::curated_plugins_repo_path(codex_home.path()); + write_openai_curated_marketplace(&curated_root, &["slack"]); + let marketplace_name = TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST + .iter() + .copied() + .filter_map(|plugin_id| plugin_id.rsplit_once('@')) + .find(|(_plugin_name, marketplace_name)| { + *marketplace_name == OPENAI_BUNDLED_MARKETPLACE_NAME + }) + .map(|(_plugin_name, marketplace_name)| marketplace_name) + .expect("allowlist should include a bundled plugin"); + let marketplace_root = codex_home + .path() + .join(format!(".tmp/marketplaces/{marketplace_name}")); + write_file( + &marketplace_root.join(".agents/plugins/marketplace.json"), + &format!( + r#"{{ + "name": "{marketplace_name}", + "plugins": [ + {{"name": "sample", "source": {{"source": "local", "path": "./plugins/sample"}}}} + ] +}} +"# + ), + ); + write_file( + &codex_home.path().join(crate::config::CONFIG_TOML_FILE), + &format!( + r#"[features] +plugins = true + +[marketplaces.{marketplace_name}] +source_type = "git" +source = "/tmp/{marketplace_name}" +"# + ), + ); + + let config = load_plugins_config(codex_home.path()).await; + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); + + assert_eq!(discoverable_plugins.len(), 1); + assert_eq!(discoverable_plugins[0].id, "slack@openai-curated"); +} + #[tokio::test] async fn list_tool_suggest_discoverable_plugins_returns_empty_when_plugins_feature_disabled() { let codex_home = tempdir().expect("tempdir should succeed"); diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index c83531150..620cf70a0 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -75,6 +75,7 @@ use tracing::warn; pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated"; pub const OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME: &str = "OpenAI Curated"; +pub const OPENAI_BUNDLED_MARKETPLACE_NAME: &str = "openai-bundled"; static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false); const FEATURED_PLUGIN_IDS_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 3); diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 16cc8d08c..b39f168a0 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -32,6 +32,7 @@ pub use installed_marketplaces::marketplace_install_root; pub use manager::ConfiguredMarketplace; pub use manager::ConfiguredMarketplaceListOutcome; pub use manager::ConfiguredMarketplacePlugin; +pub use manager::OPENAI_BUNDLED_MARKETPLACE_NAME; pub use manager::OPENAI_CURATED_MARKETPLACE_NAME; pub use manager::PluginDetail; pub use manager::PluginInstallError;