[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
2026-06-18 12:25:07 -07:00
committed by GitHub
parent 752ed90d78
commit a52a3b5197
8 changed files with 590 additions and 30 deletions
+45 -11
View File
@@ -768,6 +768,29 @@ fn apply_plugin_mcp_server_policy(config: &mut McpServerConfig, policy: &PluginM
}
}
pub(crate) struct PluginSkillInventory {
skills: Vec<SkillMetadata>,
had_errors: bool,
}
impl PluginSkillInventory {
pub(crate) fn has_enabled_skills(&self, skill_config_rules: &SkillConfigRules) -> bool {
contains_enabled_skill(
&self.skills,
&resolve_disabled_skill_paths(&self.skills, skill_config_rules),
)
}
fn resolve(self, skill_config_rules: &SkillConfigRules) -> ResolvedPluginSkills {
let disabled_skill_paths = resolve_disabled_skill_paths(&self.skills, skill_config_rules);
ResolvedPluginSkills {
skills: self.skills,
disabled_skill_paths,
had_errors: self.had_errors,
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedPluginSkills {
pub skills: Vec<SkillMetadata>,
@@ -777,14 +800,19 @@ pub struct ResolvedPluginSkills {
impl ResolvedPluginSkills {
pub fn has_enabled_skills(&self) -> bool {
self.had_errors
|| self
.skills
.iter()
.any(|skill| !self.disabled_skill_paths.contains(&skill.path_to_skills_md))
self.had_errors || contains_enabled_skill(&self.skills, &self.disabled_skill_paths)
}
}
fn contains_enabled_skill(
skills: &[SkillMetadata],
disabled_skill_paths: &HashSet<AbsolutePathBuf>,
) -> bool {
skills
.iter()
.any(|skill| !disabled_skill_paths.contains(&skill.path_to_skills_md))
}
pub async fn load_plugin_skills(
plugin_root: &AbsolutePathBuf,
plugin_id: &PluginId,
@@ -792,6 +820,17 @@ pub async fn load_plugin_skills(
restriction_product: Option<Product>,
skill_config_rules: &SkillConfigRules,
) -> ResolvedPluginSkills {
load_plugin_skill_inventory(plugin_root, plugin_id, manifest, restriction_product)
.await
.resolve(skill_config_rules)
}
pub(crate) async fn load_plugin_skill_inventory(
plugin_root: &AbsolutePathBuf,
plugin_id: &PluginId,
manifest: &PluginManifest,
restriction_product: Option<Product>,
) -> PluginSkillInventory {
let roots = plugin_skill_roots(plugin_root, &manifest.paths)
.into_iter()
.map(|path| SkillRoot {
@@ -810,13 +849,8 @@ pub async fn load_plugin_skills(
.into_iter()
.filter(|skill| skill.matches_product_restriction_for_product(restriction_product))
.collect::<Vec<_>>();
let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules);
ResolvedPluginSkills {
skills,
disabled_skill_paths,
had_errors,
}
PluginSkillInventory { skills, had_errors }
}
fn plugin_skill_roots(