[codex] Cache plugin metadata for tool suggestions (#27812)

## 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.
This commit is contained in:
Matthew Zeng authored and GitHub committed 2026-06-18 12:25:07 -07:00
1 parent 752ed90d78
commit a52a3b5197
8 files changed
+590 -30

No files matched your search

+34 -7
View File
@@ -51,6 +51,7 @@ use crate::startup_sync::sync_openai_plugins_repo;
use crate::store::PluginInstallResult as StorePluginInstallResult;
use crate::store::PluginStore;
use crate::store::PluginStoreError;
use crate::tool_suggest_metadata::ToolSuggestMetadataCache;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::AuthMode;
use codex_config::ConfigLayerStack;
@@ -354,6 +355,7 @@ pub struct PluginsManager {
// Keep the cache auth-independent so auth changes only need to resolve capabilities again.
loaded_plugins_cache: RwLock<LoadedPluginsCache>,
loaded_plugins_load_semaphore: Semaphore,
tool_suggest_metadata_cache: ToolSuggestMetadataCache,
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
global_remote_catalog_cache_refresh_state: RwLock<GlobalRemoteCatalogCacheRefreshState>,
@@ -410,6 +412,7 @@ impl PluginsManager {
non_curated_cache_refresh_state: RwLock::new(NonCuratedCacheRefreshState::default()),
loaded_plugins_cache: RwLock::new(LoadedPluginsCache::default()),
loaded_plugins_load_semaphore: Semaphore::new(/*permits*/ 1),
tool_suggest_metadata_cache: ToolSuggestMetadataCache::new(),
remote_installed_plugins_cache: RwLock::new(None),
remote_installed_plugins_cache_refresh_state: RwLock::new(
RemoteInstalledPluginsCacheRefreshState::default(),
@@ -551,6 +554,7 @@ impl PluginsManager {
}
fn clear_loaded_plugins_cache(&self) {
self.tool_suggest_metadata_cache.clear();
let mut cache = match self.loaded_plugins_cache.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
@@ -559,6 +563,17 @@ impl PluginsManager {
cache.entry = None;
}
fn clear_caches_after_marketplace_source_refresh(
&self,
installed_plugin_cache_refreshed: bool,
) {
if installed_plugin_cache_refreshed {
self.clear_cache();
} else {
self.tool_suggest_metadata_cache.clear();
}
}
/// Load plugins for a config layer stack without touching the plugins cache.
pub async fn plugins_for_layer_stack(
&self,
@@ -1444,6 +1459,19 @@ impl PluginsManager {
))
}
pub(crate) async fn tool_suggest_metadata_for_marketplace_plugin(
&self,
marketplace_name: &str,
plugin: &ConfiguredMarketplacePlugin,
skill_config_rules: &SkillConfigRules,
) -> Result<PluginCapabilitySummary, MarketplaceError> {
let fragment = self
.tool_suggest_metadata_cache
.metadata_for_plugin(marketplace_name, plugin, self.restriction_product)
.await?;
Ok(fragment.project(skill_config_rules, self.auth_mode()))
}
pub async fn read_plugin_for_config(
&self,
config: &PluginsConfigInput,
@@ -1795,9 +1823,7 @@ impl PluginsManager {
&outcome.upgraded_roots,
) {
Ok(cache_refreshed) => {
if cache_refreshed {
self.clear_cache();
}
self.clear_caches_after_marketplace_source_refresh(cache_refreshed);
}
Err(err) => {
self.clear_cache();
@@ -1977,9 +2003,8 @@ impl PluginsManager {
&configured_curated_plugin_ids,
) {
Ok(cache_refreshed) => {
if cache_refreshed {
manager.clear_cache();
}
manager
.clear_caches_after_marketplace_source_refresh(cache_refreshed);
}
Err(err) => {
manager.clear_cache();
@@ -2220,7 +2245,9 @@ impl PluginsManager {
}
}
fn remote_plugin_install_required_description(source: &MarketplacePluginSource) -> String {
pub(crate) fn remote_plugin_install_required_description(
source: &MarketplacePluginSource,
) -> String {
let source_description = match source {
MarketplacePluginSource::Git {
url,