feat: Use remote installed plugin cache for skills and MCP (#20096)

- Fetches and caches remote /installed plugin state
- Lets skills/list load skills from remote-installed cached plugins
without requiring a local marketplace entry
- Routes plugin list/startup/install/uninstall changes through async
plugin cache invalidation and MCP refresh
This commit is contained in:
xl-openai
2026-04-29 12:09:49 -07:00
committed by GitHub
Unverified
parent 5cf0adba93
commit 73cd831952
9 changed files with 751 additions and 54 deletions
+39 -3
View File
@@ -5,6 +5,7 @@ use crate::manifest::load_plugin_manifest;
use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::list_marketplaces;
use crate::marketplace::load_marketplace;
use crate::remote::RemoteInstalledPlugin;
use crate::store::PluginStore;
use crate::store::plugin_version_for_source;
use codex_config::ConfigLayerStack;
@@ -107,13 +108,14 @@ struct PluginAppConfig {
pub async fn load_plugins_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
extra_plugins: HashMap<String, PluginConfig>,
store: &PluginStore,
restriction_product: Option<Product>,
) -> PluginLoadOutcome<McpServerConfig> {
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
let mut configured_plugins: Vec<_> = configured_plugins_from_stack(config_layer_stack)
.into_iter()
.collect();
let mut configured_plugins = configured_plugins_from_stack(config_layer_stack);
configured_plugins.extend(extra_plugins);
let mut configured_plugins: Vec<_> = configured_plugins.into_iter().collect();
configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut plugins = Vec::with_capacity(configured_plugins.len());
@@ -145,6 +147,40 @@ pub async fn load_plugins_from_layer_stack(
PluginLoadOutcome::from_plugins(plugins)
}
pub fn remote_installed_plugins_to_config(
plugins: &[RemoteInstalledPlugin],
store: &PluginStore,
) -> HashMap<String, PluginConfig> {
plugins
.iter()
.filter_map(|plugin| {
let plugin_id =
match PluginId::new(plugin.name.clone(), plugin.marketplace_name.clone()) {
Ok(plugin_id) => plugin_id,
Err(err) => {
warn!(
plugin = %plugin.name,
remote_id = %plugin.id,
error = %err,
"ignoring invalid remote installed plugin name"
);
return None;
}
};
// TODO(remote plugins): download or update missing local bundles during remote
// installed reconciliation. Until then, only publish remote installed state for
// bundles already present in the local plugin cache.
store.active_plugin_root(&plugin_id)?;
Some((
plugin_id.as_key(),
PluginConfig {
enabled: plugin.enabled,
},
))
})
.collect()
}
pub fn refresh_curated_plugin_cache(
codex_home: &Path,
plugin_version: &str,
+57
View File
@@ -39,6 +39,14 @@ pub struct RemoteMarketplace {
pub plugins: Vec<RemotePluginSummary>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RemoteInstalledPlugin {
pub marketplace_name: String,
pub id: String,
pub name: String,
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RemotePluginSummary {
pub id: String,
@@ -369,6 +377,39 @@ pub async fn fetch_remote_marketplaces(
Ok(marketplaces)
}
pub async fn fetch_remote_installed_plugins(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError> {
let auth = ensure_chatgpt_auth(auth)?;
let global = async {
let scope = RemotePluginScope::Global;
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
};
let workspace = async {
let scope = RemotePluginScope::Workspace;
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
};
let (global, workspace) = tokio::try_join!(global, workspace)?;
let mut installed_plugins = [global, workspace]
.into_iter()
.flat_map(|(scope, plugins)| {
plugins
.into_iter()
.map(move |plugin| remote_installed_plugin_to_info(scope, &plugin))
})
.collect::<Vec<_>>();
installed_plugins.sort_by(|left, right| {
left.marketplace_name
.cmp(&right.marketplace_name)
.then_with(|| left.id.cmp(&right.id))
});
Ok(installed_plugins)
}
pub async fn fetch_remote_plugin_detail(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
@@ -671,6 +712,22 @@ fn build_remote_plugin_summary(
}
}
fn remote_installed_plugin_to_info(
scope: RemotePluginScope,
installed_plugin: &RemotePluginInstalledItem,
) -> RemoteInstalledPlugin {
let plugin = &installed_plugin.plugin;
// Remote per-skill disabled state (`disabled_skill_names`) is intentionally
// not projected into skills/list yet; local skills.config remains the
// supported source for skill enablement.
RemoteInstalledPlugin {
marketplace_name: scope.marketplace_name().to_string(),
id: plugin.id.clone(),
name: plugin.name.clone(),
enabled: installed_plugin.enabled,
}
}
fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option<PluginInterface> {
let interface = &plugin.release.interface;
let display_name = non_empty_string(Some(&plugin.release.display_name));