From daf76a57d2564be85b6e34c25a29380b3d4315b4 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Mon, 8 Jun 2026 14:46:59 -0700 Subject: [PATCH] [codex] Prune stale curated plugin caches (#26934) Curated plugin startup refresh now removes cached plugins whose names no longer appear in the raw openai-curated marketplace. This prevents users with the old standalone Google Sheets plugin selected locally from continuing to load its stale cache after the curated repo drops it. Existing config is left untouched, and plugins still present in the marketplace continue to refresh from local curated sources. Validation: - `just fmt` - `just test -p codex-core-plugins` - `git diff --check` --- codex-rs/core-plugins/src/loader.rs | 45 +++++++++++++--------- codex-rs/core-plugins/src/manager_tests.rs | 27 +++++++++++++ codex-rs/core-plugins/src/marketplace.rs | 11 ++++++ 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index d6414be6f..9dfa0bbc6 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -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::marketplace::load_raw_marketplace_plugin_names; use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; use crate::remote::RemoteInstalledPlugin; use crate::store::PluginStore; @@ -307,6 +308,10 @@ pub fn refresh_curated_plugin_cache( .join(".agents/plugins/marketplace.json"), ) .map_err(|_| "local curated marketplace is not available".to_string())?; + let marketplace_plugin_names = load_raw_marketplace_plugin_names(&curated_marketplace_path) + .map_err(|err| { + format!("failed to load curated marketplace plugin names for cache refresh: {err}") + })?; let curated_marketplace = load_marketplace(&curated_marketplace_path) .map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?; @@ -321,36 +326,40 @@ pub fn refresh_curated_plugin_cache( ); continue; } - let source_path = match plugin.source { - MarketplacePluginSource::Local { path } => path, - MarketplacePluginSource::Git { .. } => { - warn!( - plugin = plugin_name, - marketplace = OPENAI_CURATED_MARKETPLACE_NAME, - "skipping remote curated plugin source during cache refresh" - ); - continue; - } - }; - plugin_sources.insert(plugin_name, source_path); + if let MarketplacePluginSource::Local { path } = plugin.source { + plugin_sources.insert(plugin_name, path); + } } let mut cache_refreshed = false; for plugin_id in configured_curated_plugin_ids { - if store.active_plugin_version(plugin_id).as_deref() == Some(cache_plugin_version.as_str()) - { - continue; - } - - let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else { + if !marketplace_plugin_names.contains(&plugin_id.plugin_name) { warn!( plugin = plugin_id.plugin_name, marketplace = OPENAI_CURATED_MARKETPLACE_NAME, "configured curated plugin no longer exists in curated marketplace during cache refresh" ); + if store.plugin_base_root(plugin_id).as_path().exists() { + store.uninstall(plugin_id).map_err(|err| { + format!( + "failed to remove stale curated plugin cache for {}: {err}", + plugin_id.as_key() + ) + })?; + cache_refreshed = true; + } + continue; + } + + let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else { continue; }; + if store.active_plugin_version(plugin_id).as_deref() == Some(cache_plugin_version.as_str()) + { + continue; + } + store .install_with_version(source_path, plugin_id.clone(), cache_plugin_version.clone()) .map_err(|err| { diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index b1be37655..4fc8456ce 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -3039,6 +3039,33 @@ fn refresh_curated_plugin_cache_reinstalls_missing_configured_plugin_with_curren ); } +#[test] +fn refresh_curated_plugin_cache_removes_cache_for_plugin_removed_from_marketplace() { + let tmp = tempfile::tempdir().unwrap(); + let curated_root = curated_plugins_repo_path(tmp.path()); + write_openai_curated_marketplace(&curated_root, &[]); + let plugin_id = PluginId::new( + "google-sheets".to_string(), + OPENAI_CURATED_MARKETPLACE_NAME.to_string(), + ) + .unwrap(); + let plugin_cache_root = tmp + .path() + .join("plugins/cache/openai-curated/google-sheets"); + write_plugin( + &tmp.path().join("plugins/cache/openai-curated"), + &format!("google-sheets/{TEST_CURATED_PLUGIN_CACHE_VERSION}"), + "google-sheets", + ); + + assert!( + refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id]) + .expect("cache refresh should remove stale configured plugin") + ); + + assert!(!plugin_cache_root.exists()); +} + #[test] fn curated_plugin_ids_from_config_keys_reads_latest_codex_home_user_config() { let tmp = tempfile::tempdir().unwrap(); diff --git a/codex-rs/core-plugins/src/marketplace.rs b/codex-rs/core-plugins/src/marketplace.rs index 49925a673..049d88275 100644 --- a/codex-rs/core-plugins/src/marketplace.rs +++ b/codex-rs/core-plugins/src/marketplace.rs @@ -9,6 +9,7 @@ use codex_protocol::protocol::Product; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde_json::Value as JsonValue; +use std::collections::HashSet; use std::fs; use std::io; use std::path::Component; @@ -326,6 +327,16 @@ pub fn load_marketplace(path: &AbsolutePathBuf) -> Result Result, MarketplaceError> { + Ok(load_raw_marketplace_manifest(path)? + .plugins + .into_iter() + .map(|plugin| plugin.name) + .collect()) +} + #[doc(hidden)] pub fn list_marketplaces_with_home( additional_roots: &[AbsolutePathBuf],