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`
This commit is contained in:
Leo Shimonaka
2026-04-16 19:55:23 -07:00
committed by GitHub
parent 37161bc76e
commit dd00efe781
4 changed files with 154 additions and 34 deletions

View File

@@ -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::<DiscoverablePluginInfo>::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| {

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;