mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
a52a3b5197
## Why `built_tools` runs for every sampling request, and local plugin discovery was repeatedly rereading plugin manifests, skills, MCP configuration, and app declarations to build the same tool-suggest metadata. That source-derived metadata is stable until the existing plugin manager reloads its cache. Runtime eligibility still needs to reflect the current install, disable, policy, app-overlap, and authentication state. ## What changed - Add a bounded, in-memory tool-suggest metadata cache owned by `PluginsManager`. - Key cached metadata by plugin identity and source, while applying authentication routing each time the metadata is projected. - Invalidate the metadata alongside the existing loaded-plugin cache, including its normal configuration, marketplace refresh, and remote-installed-plugin invalidation paths. - Guard against an in-flight load repopulating stale metadata after invalidation. - Keep marketplace membership and all runtime eligibility filtering live rather than introducing a separate catalog or revision model. ## Impact Repeated sampling requests reuse already-loaded plugin capability metadata while retaining the existing plugin-manager lifecycle as the single freshness boundary. ## Validation - `just test -p codex-core-plugins` — 252 passed - Added focused coverage for cache invalidation and authentication reprojection.
230 lines
8.9 KiB
Rust
230 lines
8.9 KiB
Rust
use anyhow::Context;
|
|
use codex_app_server_protocol::PluginAvailability;
|
|
use codex_app_server_protocol::PluginInstallPolicy;
|
|
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
|
use codex_login::CodexAuth;
|
|
use codex_plugin::PluginId;
|
|
use std::collections::HashSet;
|
|
use tracing::warn;
|
|
|
|
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
|
|
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
|
|
use crate::PluginsConfigInput;
|
|
use crate::PluginsManager;
|
|
use crate::marketplace::MarketplacePluginInstallPolicy;
|
|
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
|
|
|
const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
|
|
"github@openai-curated",
|
|
"notion@openai-curated",
|
|
"slack@openai-curated",
|
|
"gmail@openai-curated",
|
|
"google-calendar@openai-curated",
|
|
"google-drive@openai-curated",
|
|
"openai-developers@openai-curated",
|
|
"canva@openai-curated",
|
|
"teams@openai-curated",
|
|
"sharepoint@openai-curated",
|
|
"outlook-email@openai-curated",
|
|
"outlook-calendar@openai-curated",
|
|
"linear@openai-curated",
|
|
"figma@openai-curated",
|
|
"github@openai-curated-remote",
|
|
"notion@openai-curated-remote",
|
|
"slack@openai-curated-remote",
|
|
"gmail@openai-curated-remote",
|
|
"google-calendar@openai-curated-remote",
|
|
"google-drive@openai-curated-remote",
|
|
"openai-developers@openai-curated-remote",
|
|
"canva@openai-curated-remote",
|
|
"teams@openai-curated-remote",
|
|
"sharepoint@openai-curated-remote",
|
|
"outlook-email@openai-curated-remote",
|
|
"outlook-calendar@openai-curated-remote",
|
|
"linear@openai-curated-remote",
|
|
"figma@openai-curated-remote",
|
|
"chrome@openai-bundled",
|
|
"computer-use@openai-bundled",
|
|
];
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ToolSuggestPluginDiscoveryInput {
|
|
pub plugins: PluginsConfigInput,
|
|
pub configured_plugin_ids: HashSet<String>,
|
|
pub disabled_plugin_ids: HashSet<String>,
|
|
pub loaded_plugin_app_connector_ids: HashSet<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ToolSuggestDiscoverablePlugin {
|
|
pub id: String,
|
|
pub remote_plugin_id: Option<String>,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub has_skills: bool,
|
|
pub mcp_server_names: Vec<String>,
|
|
pub app_connector_ids: Vec<String>,
|
|
}
|
|
|
|
impl PluginsManager {
|
|
pub async fn list_tool_suggest_discoverable_plugins(
|
|
&self,
|
|
input: &ToolSuggestPluginDiscoveryInput,
|
|
auth: Option<&CodexAuth>,
|
|
) -> anyhow::Result<Vec<ToolSuggestDiscoverablePlugin>> {
|
|
if !input.plugins.plugins_enabled {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let use_remote_global_catalog =
|
|
input.plugins.remote_plugin_enabled && auth.is_some_and(CodexAuth::uses_codex_backend);
|
|
let marketplaces = self
|
|
.list_marketplaces_for_config(
|
|
&input.plugins,
|
|
&[],
|
|
/*include_openai_curated*/ !use_remote_global_catalog,
|
|
)
|
|
.context("failed to list plugin marketplaces for tool suggestions")?
|
|
.marketplaces;
|
|
let remote_installed_marketplaces = if use_remote_global_catalog {
|
|
self.build_remote_installed_plugin_marketplaces_from_cache(&[
|
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
|
])
|
|
} else {
|
|
None
|
|
};
|
|
let skill_config_rules = skill_config_rules_from_stack(&input.plugins.config_layer_stack);
|
|
|
|
let mut discoverable_plugins = Vec::<ToolSuggestDiscoverablePlugin>::new();
|
|
for marketplace in marketplaces {
|
|
let marketplace_name = marketplace.name;
|
|
|
|
for plugin in marketplace.plugins {
|
|
let is_configured_plugin = input.configured_plugin_ids.contains(plugin.id.as_str());
|
|
let is_fallback_plugin = is_tool_suggest_fallback_plugin(&plugin.id);
|
|
if plugin.installed
|
|
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|
|
|| input.disabled_plugin_ids.contains(plugin.id.as_str())
|
|
|| (!is_configured_plugin && !is_fallback_plugin)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let plugin_id = plugin.id.clone();
|
|
match self
|
|
.tool_suggest_metadata_for_marketplace_plugin(
|
|
&marketplace_name,
|
|
&plugin,
|
|
&skill_config_rules,
|
|
)
|
|
.await
|
|
{
|
|
Ok(plugin) => {
|
|
discoverable_plugins.push(ToolSuggestDiscoverablePlugin {
|
|
id: plugin.config_name,
|
|
remote_plugin_id: None,
|
|
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:#}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(remote_installed_marketplaces) = remote_installed_marketplaces.as_ref() {
|
|
let mut installed_app_connector_ids = self
|
|
.plugins_for_config(&input.plugins)
|
|
.await
|
|
.capability_summaries()
|
|
.iter()
|
|
.flat_map(|plugin| plugin.app_connector_ids.iter())
|
|
.map(|connector_id| connector_id.0.clone())
|
|
.collect::<HashSet<_>>();
|
|
installed_app_connector_ids
|
|
.extend(input.loaded_plugin_app_connector_ids.iter().cloned());
|
|
let installed_remote_plugin_ids = remote_installed_marketplaces
|
|
.iter()
|
|
.flat_map(|marketplace| marketplace.plugins.iter())
|
|
.map(|plugin| plugin.remote_plugin_id.clone())
|
|
.collect::<HashSet<_>>();
|
|
for plugin in
|
|
self.cached_global_remote_discoverable_plugins_for_config(&input.plugins, auth)
|
|
{
|
|
let is_configured_plugin = input
|
|
.configured_plugin_ids
|
|
.contains(plugin.config_id.as_str())
|
|
|| input
|
|
.configured_plugin_ids
|
|
.contains(plugin.remote_plugin_id.as_str());
|
|
let is_fallback_plugin = is_tool_suggest_fallback_plugin(&plugin.config_id);
|
|
let matches_installed_app = plugin
|
|
.app_ids
|
|
.iter()
|
|
.any(|app_id| installed_app_connector_ids.contains(app_id.as_str()));
|
|
let is_disabled = input
|
|
.disabled_plugin_ids
|
|
.contains(plugin.config_id.as_str())
|
|
|| input
|
|
.disabled_plugin_ids
|
|
.contains(plugin.remote_plugin_id.as_str());
|
|
if installed_remote_plugin_ids.contains(&plugin.remote_plugin_id)
|
|
|| plugin.install_policy == PluginInstallPolicy::NotAvailable
|
|
|| plugin.availability == PluginAvailability::DisabledByAdmin
|
|
|| is_disabled
|
|
|| (!is_configured_plugin && !is_fallback_plugin && !matches_installed_app)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
discoverable_plugins.push(ToolSuggestDiscoverablePlugin {
|
|
id: plugin.config_id,
|
|
remote_plugin_id: Some(plugin.remote_plugin_id),
|
|
name: plugin.name,
|
|
description: plugin.description,
|
|
has_skills: plugin.has_skills,
|
|
mcp_server_names: Vec::new(),
|
|
app_connector_ids: plugin.app_ids,
|
|
});
|
|
}
|
|
}
|
|
discoverable_plugins.sort_by(|left, right| {
|
|
left.name
|
|
.cmp(&right.name)
|
|
.then_with(|| left.id.cmp(&right.id))
|
|
});
|
|
Ok(discoverable_plugins)
|
|
}
|
|
}
|
|
|
|
fn is_tool_suggest_fallback_plugin(plugin_id: &str) -> bool {
|
|
if TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin_id) {
|
|
return true;
|
|
}
|
|
|
|
let Ok(plugin_id) = PluginId::parse(plugin_id) else {
|
|
return false;
|
|
};
|
|
if plugin_id.marketplace_name != OPENAI_API_CURATED_MARKETPLACE_NAME {
|
|
return false;
|
|
}
|
|
|
|
let default_curated_plugin_id = format!(
|
|
"{}@{}",
|
|
plugin_id.plugin_name, OPENAI_CURATED_MARKETPLACE_NAME
|
|
);
|
|
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&default_curated_plugin_id.as_str())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "discoverable_tests.rs"]
|
|
mod tests;
|