Codex Handbook
core-plugins/src/manager.rs 2164 lines
use super::LoadedPlugin;use super::PluginLoadOutcome;use crate::app_mcp_routing::apply_app_mcp_routing_policy;use crate::installed_marketplaces::installed_marketplace_roots_from_layer_stack;use crate::is_openai_curated_marketplace_name;use crate::loader::PluginHookLoadOutcome;use crate::loader::configured_curated_plugin_ids_from_codex_home;use crate::loader::curated_plugin_cache_version;use crate::loader::installed_plugin_telemetry_metadata;use crate::loader::load_plugin_apps;use crate::loader::load_plugin_hooks;use crate::loader::load_plugin_hooks_from_layer_stack;use crate::loader::load_plugin_mcp_servers;use crate::loader::load_plugin_skills;use crate::loader::load_plugins_from_layer_stack;use crate::loader::log_plugin_load_errors;use crate::loader::materialize_marketplace_plugin_source;use crate::loader::plugin_telemetry_metadata_from_root;use crate::loader::refresh_curated_plugin_cache;use crate::loader::refresh_non_curated_plugin_cache;use crate::loader::refresh_non_curated_plugin_cache_force_reinstall;use crate::loader::remote_installed_plugins_to_config;use crate::manifest::PluginManifestInterface;use crate::manifest::load_plugin_manifest;use crate::marketplace::MarketplaceError;use crate::marketplace::MarketplaceInterface;use crate::marketplace::MarketplaceListError;use crate::marketplace::MarketplaceListOutcome;use crate::marketplace::MarketplacePluginAuthPolicy;use crate::marketplace::MarketplacePluginPolicy;use crate::marketplace::MarketplacePluginSource;use crate::marketplace::ResolvedMarketplacePlugin;use crate::marketplace::find_installable_marketplace_plugin;use crate::marketplace::find_marketplace_plugin;use crate::marketplace::list_marketplaces;use crate::marketplace::plugin_interface_with_marketplace_category;use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeError;use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;use crate::marketplace_upgrade::configured_git_marketplace_names;use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;use crate::remote::RecommendedPluginsMode;use crate::remote::RemoteInstalledPlugin;use crate::remote::RemotePluginCatalogError;use crate::remote::RemotePluginServiceConfig;use crate::remote_legacy::RemotePluginFetchError;use crate::remote_legacy::RemotePluginMutationError;use crate::startup_sync::curated_plugins_api_marketplace_path;use crate::startup_sync::curated_plugins_repo_path;use crate::startup_sync::read_curated_plugins_sha;use crate::startup_sync::sync_openai_plugins_repo;use crate::store::PluginInstallResult as StorePluginInstallResult;use crate::store::PluginStore;use crate::store::PluginStoreError;use codex_analytics::AnalyticsEventsClient;use codex_app_server_protocol::AuthMode;use codex_config::ConfigLayerStack;use codex_config::clear_user_plugin;use codex_config::set_user_plugin_enabled;use codex_config::types::PluginConfig;use codex_core_skills::SkillMetadata;use codex_core_skills::config_rules::SkillConfigRules;use codex_core_skills::config_rules::skill_config_rules_from_stack;use codex_hooks::plugin_hook_declarations;use codex_login::AuthManager;use codex_login::CodexAuth;use codex_plugin::AppConnectorId;use codex_plugin::PluginCapabilitySummary;use codex_plugin::PluginId;use codex_plugin::PluginIdError;use codex_plugin::app_connector_ids_from_declarations;use codex_plugin::prompt_safe_plugin_description;use codex_protocol::protocol::HookEventName;use codex_protocol::protocol::Product;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_plugins::PluginSkillRoot;use std::collections::HashMap;use std::collections::HashSet;use std::path::PathBuf;use std::sync::Arc;use std::sync::RwLock;use std::sync::atomic::AtomicBool;use std::sync::atomic::Ordering;use std::time::Instant;use tokio::sync::OnceCell;use tokio::sync::Semaphore;use tracing::instrument;use tracing::warn;static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false);const FEATURED_PLUGIN_IDS_CACHE_TTL: std::time::Duration =    std::time::Duration::from_secs(60 * 60 * 3);#[derive(Debug, Clone)]pub struct PluginsConfigInput {    pub config_layer_stack: ConfigLayerStack,    pub plugins_enabled: bool,    pub remote_plugin_enabled: bool,    pub chatgpt_base_url: String,}impl PluginsConfigInput {    pub fn new(        config_layer_stack: ConfigLayerStack,        plugins_enabled: bool,        remote_plugin_enabled: bool,        chatgpt_base_url: String,    ) -> Self {        Self {            config_layer_stack,            plugins_enabled,            remote_plugin_enabled,            chatgpt_base_url,        }    }}#[derive(Clone, PartialEq, Eq)]struct FeaturedPluginIdsCacheKey {    chatgpt_base_url: String,    account_id: Option<String>,    chatgpt_user_id: Option<String>,    is_workspace_account: bool,}#[derive(Clone, Hash, PartialEq, Eq)]struct RecommendedPluginsCacheKey {    chatgpt_base_url: String,}#[derive(Clone)]struct CachedFeaturedPluginIds {    key: FeaturedPluginIdsCacheKey,    expires_at: Instant,    featured_plugin_ids: Vec<String>,}struct RemoteInstalledPluginsCacheRefreshRequest {    service_config: RemotePluginServiceConfig,    auth: Option<CodexAuth>,    notify: RemoteInstalledPluginsCacheRefreshNotify,    // App-server attaches side effects such as skills metadata invalidation and MCP refreshes when    // remote installed state changes.    on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,}#[derive(Clone, Copy)]enum RemoteInstalledPluginsCacheRefreshNotify {    IfCacheChanged,    // Remote mutations may change local bundles or active MCP state even when the installed set is    // unchanged. Notify after `/installed` succeeds so MCP refreshes are ordered after the remote    // installed cache.    AfterSuccessfulRefresh,}#[derive(Default)]struct RemoteInstalledPluginsCacheRefreshState {    requested: Option<RemoteInstalledPluginsCacheRefreshRequest>,    in_flight: bool,}struct GlobalRemoteCatalogCacheRefreshRequest {    service_config: RemotePluginServiceConfig,    auth: Option<CodexAuth>,}#[derive(Default)]struct GlobalRemoteCatalogCacheRefreshState {    requested: Option<GlobalRemoteCatalogCacheRefreshRequest>,    in_flight: bool,}#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub struct PluginListBackgroundTaskOptions {    pub refresh_global_remote_catalog_cache: bool,}#[derive(Clone, PartialEq, Eq)]struct NonCuratedCacheRefreshRequest {    roots: Vec<AbsolutePathBuf>,    mode: NonCuratedCacheRefreshMode,}#[derive(Clone, Copy, PartialEq, Eq)]enum NonCuratedCacheRefreshMode {    IfVersionChanged,    ForceReinstall,}#[derive(Default)]struct NonCuratedCacheRefreshState {    requested: Option<NonCuratedCacheRefreshRequest>,    last_refreshed: Option<NonCuratedCacheRefreshRequest>,    in_flight: bool,}#[derive(Default)]struct ConfiguredMarketplaceUpgradeState {    in_flight: bool,}fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig {    RemotePluginServiceConfig {        chatgpt_base_url: config.chatgpt_base_url.clone(),    }}fn featured_plugin_ids_cache_key(    config: &PluginsConfigInput,    auth: Option<&CodexAuth>,) -> FeaturedPluginIdsCacheKey {    FeaturedPluginIdsCacheKey {        chatgpt_base_url: config.chatgpt_base_url.clone(),        account_id: auth.and_then(CodexAuth::get_account_id),        chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id),        is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account),    }}fn recommended_plugins_cache_key(config: &PluginsConfigInput) -> RecommendedPluginsCacheKey {    RecommendedPluginsCacheKey {        chatgpt_base_url: config.chatgpt_base_url.clone(),    }}#[derive(Debug, Clone, PartialEq, Eq)]pub struct PluginInstallRequest {    pub plugin_name: String,    pub marketplace_path: AbsolutePathBuf,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct PluginReadRequest {    pub plugin_name: String,    pub marketplace_path: AbsolutePathBuf,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct PluginInstallOutcome {    pub plugin_id: PluginId,    pub plugin_version: String,    pub installed_path: AbsolutePathBuf,    pub auth_policy: MarketplacePluginAuthPolicy,}#[derive(Debug, Clone, PartialEq)]pub struct PluginReadOutcome {    pub marketplace_name: String,    pub marketplace_path: Option<AbsolutePathBuf>,    pub plugin: PluginDetail,}#[derive(Debug, Clone, PartialEq)]pub struct PluginDetail {    pub id: String,    pub name: String,    pub local_version: Option<String>,    pub description: Option<String>,    pub source: MarketplacePluginSource,    pub policy: MarketplacePluginPolicy,    pub interface: Option<PluginManifestInterface>,    pub keywords: Vec<String>,    pub installed: bool,    pub enabled: bool,    pub skills: Vec<SkillMetadata>,    pub disabled_skill_paths: HashSet<AbsolutePathBuf>,    pub hooks: Vec<PluginHookSummary>,    pub apps: Vec<AppConnectorId>,    pub app_category_by_id: HashMap<String, String>,    pub mcp_server_names: Vec<String>,    pub details_unavailable_reason: Option<PluginDetailsUnavailableReason>,}#[derive(Debug, Clone, PartialEq)]pub struct PluginHookSummary {    pub key: String,    pub event_name: HookEventName,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PluginDetailsUnavailableReason {    InstallRequiredForRemoteSource,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct ConfiguredMarketplace {    pub name: String,    pub path: AbsolutePathBuf,    pub interface: Option<MarketplaceInterface>,    pub plugins: Vec<ConfiguredMarketplacePlugin>,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct ConfiguredMarketplacePlugin {    pub id: String,    pub name: String,    pub local_version: Option<String>,    pub installed_version: Option<String>,    pub source: MarketplacePluginSource,    pub policy: MarketplacePluginPolicy,    pub interface: Option<PluginManifestInterface>,    pub keywords: Vec<String>,    pub installed: bool,    pub enabled: bool,}#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct ConfiguredMarketplaceListOutcome {    pub marketplaces: Vec<ConfiguredMarketplace>,    pub errors: Vec<MarketplaceListError>,}impl From<PluginDetail> for PluginCapabilitySummary {    fn from(value: PluginDetail) -> Self {        let has_skills = value.skills.iter().any(|skill| {            !value                .disabled_skill_paths                .contains(&skill.path_to_skills_md)        });        Self {            config_name: value.id,            display_name: value.name,            description: prompt_safe_plugin_description(value.description.as_deref()),            has_skills,            mcp_server_names: value.mcp_server_names,            app_connector_ids: value.apps,        }    }}pub struct PluginsManager {    codex_home: PathBuf,    store: PluginStore,    featured_plugin_ids_cache: RwLock<Option<CachedFeaturedPluginIds>>,    recommended_plugins_cache: RwLock<HashMap<RecommendedPluginsCacheKey, RecommendedPluginsMode>>,    recommended_plugins_refreshes:        RwLock<HashMap<RecommendedPluginsCacheKey, Arc<OnceCell<RecommendedPluginsMode>>>>,    configured_marketplace_upgrade_state: RwLock<ConfiguredMarketplaceUpgradeState>,    non_curated_cache_refresh_state: RwLock<NonCuratedCacheRefreshState>,    // Keep the cache auth-independent so auth changes only need to resolve capabilities again.    loaded_plugins_cache: RwLock<LoadedPluginsCache>,    loaded_plugins_load_semaphore: Semaphore,    remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,    remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,    global_remote_catalog_cache_refresh_state: RwLock<GlobalRemoteCatalogCacheRefreshState>,    restriction_product: Option<Product>,    auth_mode: RwLock<Option<AuthMode>>,    analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,}#[derive(Clone)]struct LoadedPluginsCacheEntry {    key: PluginLoadCacheKey,    plugins: Vec<LoadedPlugin>,}#[derive(Default)]struct LoadedPluginsCache {    generation: u64,    entry: Option<LoadedPluginsCacheEntry>,}#[derive(Clone, PartialEq, Eq)]struct PluginLoadCacheKey {    configured_plugins: HashMap<String, PluginConfig>,    skill_config_rules: SkillConfigRules,    remote_plugin_enabled: bool,}impl PluginsManager {    pub fn new(codex_home: PathBuf) -> Self {        Self::new_with_options(codex_home, Some(Product::Codex), /*auth_mode*/ None)    }    pub fn new_with_options(        codex_home: PathBuf,        restriction_product: Option<Product>,        auth_mode: Option<AuthMode>,    ) -> Self {        // Product restrictions are enforced at marketplace admission time for a given CODEX_HOME:        // listing, install, and curated refresh all consult this restriction context before new        // plugins enter local config or cache. After admission, runtime plugin loading trusts the        // contents of that CODEX_HOME and does not re-filter configured plugins by product, so        // already-admitted plugins may continue exposing MCP servers/tools from shared local state.        //        // This assumes a single CODEX_HOME is only used by one product.        Self {            codex_home: codex_home.clone(),            store: PluginStore::new(codex_home),            featured_plugin_ids_cache: RwLock::new(None),            recommended_plugins_cache: RwLock::new(HashMap::new()),            recommended_plugins_refreshes: RwLock::new(HashMap::new()),            configured_marketplace_upgrade_state: RwLock::new(                ConfiguredMarketplaceUpgradeState::default(),            ),            non_curated_cache_refresh_state: RwLock::new(NonCuratedCacheRefreshState::default()),            loaded_plugins_cache: RwLock::new(LoadedPluginsCache::default()),            loaded_plugins_load_semaphore: Semaphore::new(/*permits*/ 1),            remote_installed_plugins_cache: RwLock::new(None),            remote_installed_plugins_cache_refresh_state: RwLock::new(                RemoteInstalledPluginsCacheRefreshState::default(),            ),            global_remote_catalog_cache_refresh_state: RwLock::new(                GlobalRemoteCatalogCacheRefreshState::default(),            ),            restriction_product,            auth_mode: RwLock::new(auth_mode),            analytics_events_client: RwLock::new(None),        }    }    pub fn set_auth_mode(&self, auth_mode: Option<AuthMode>) -> bool {        let mut stored_auth_mode = match self.auth_mode.write() {            Ok(auth_mode_guard) => auth_mode_guard,            Err(err) => err.into_inner(),        };        if *stored_auth_mode == auth_mode {            return false;        }        *stored_auth_mode = auth_mode;        true    }    pub fn auth_mode(&self) -> Option<AuthMode> {        match self.auth_mode.read() {            Ok(auth_mode_guard) => *auth_mode_guard,            Err(err) => *err.into_inner(),        }    }    pub fn set_analytics_events_client(&self, analytics_events_client: AnalyticsEventsClient) {        let mut stored_client = match self.analytics_events_client.write() {            Ok(client_guard) => client_guard,            Err(err) => err.into_inner(),        };        *stored_client = Some(analytics_events_client);    }    fn restriction_product_matches(&self, products: Option<&[Product]>) -> bool {        match products {            None => true,            Some([]) => false,            Some(products) => self                .restriction_product                .is_some_and(|product| product.matches_product_restriction(products)),        }    }    pub async fn plugins_for_config(&self, config: &PluginsConfigInput) -> PluginLoadOutcome {        self.plugins_for_config_with_force_reload(config, /*force_reload*/ false)            .await    }    #[instrument(        level = "trace",        skip_all,        fields(force_reload, plugins_enabled = config.plugins_enabled)    )]    pub(crate) async fn plugins_for_config_with_force_reload(        &self,        config: &PluginsConfigInput,        force_reload: bool,    ) -> PluginLoadOutcome {        if !config.plugins_enabled {            return PluginLoadOutcome::default();        }        let cache_key = PluginLoadCacheKey {            configured_plugins: configured_plugins_from_stack(&config.config_layer_stack),            skill_config_rules: skill_config_rules_from_stack(&config.config_layer_stack),            remote_plugin_enabled: config.remote_plugin_enabled,        };        if !force_reload && let Some(plugins) = self.cached_loaded_plugins(&cache_key) {            return self.resolve_loaded_plugins_for_auth(plugins);        }        let Ok(_load_permit) = self.loaded_plugins_load_semaphore.acquire().await else {            warn!("plugin load semaphore closed");            return PluginLoadOutcome::default();        };        if !force_reload && let Some(plugins) = self.cached_loaded_plugins(&cache_key) {            return self.resolve_loaded_plugins_for_auth(plugins);        }        let cache_generation = self.loaded_plugins_cache_generation();        let plugins = load_plugins_from_layer_stack(            &config.config_layer_stack,            self.remote_installed_plugin_configs(),            &self.store,            self.restriction_product,            config.remote_plugin_enabled,        )        .await;        log_plugin_load_errors(&plugins);        self.cache_loaded_plugins_if_current(cache_generation, cache_key, plugins.clone());        self.resolve_loaded_plugins_for_auth(plugins)    }    fn resolve_loaded_plugins_for_auth(&self, mut plugins: Vec<LoadedPlugin>) -> PluginLoadOutcome {        let auth_mode = self.auth_mode();        for plugin in &mut plugins {            let plugin_active = plugin.is_active();            apply_app_mcp_routing_policy(                &mut plugin.apps,                &mut plugin.mcp_servers,                auth_mode,                plugin_active,            );        }        PluginLoadOutcome::from_plugins(plugins)    }    pub fn clear_cache(&self) {        self.clear_loaded_plugins_cache();        let mut featured_plugin_ids_cache = match self.featured_plugin_ids_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        *featured_plugin_ids_cache = None;    }    pub fn clear_recommended_plugins_cache(&self) {        let mut refreshes = match self.recommended_plugins_refreshes.write() {            Ok(refreshes) => refreshes,            Err(err) => err.into_inner(),        };        refreshes.clear();        let mut cache = match self.recommended_plugins_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        cache.clear();    }    fn clear_loaded_plugins_cache(&self) {        let mut cache = match self.loaded_plugins_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        cache.generation = cache.generation.wrapping_add(1);        cache.entry = None;    }    /// Load plugins for a config layer stack without touching the plugins cache.    pub async fn plugins_for_layer_stack(        &self,        config_layer_stack: &ConfigLayerStack,        config: &PluginsConfigInput,    ) -> PluginLoadOutcome {        if !config.plugins_enabled {            return PluginLoadOutcome::default();        }        let plugins = load_plugins_from_layer_stack(            config_layer_stack,            self.remote_installed_plugin_configs(),            &self.store,            self.restriction_product,            config.remote_plugin_enabled,        )        .await;        self.resolve_loaded_plugins_for_auth(plugins)    }    /// Resolve plugin hooks for a config layer stack without loading other plugin capabilities.    pub async fn plugin_hooks_for_layer_stack(        &self,        config_layer_stack: &ConfigLayerStack,        config: &PluginsConfigInput,    ) -> PluginHookLoadOutcome {        if !config.plugins_enabled {            return PluginHookLoadOutcome::default();        }        load_plugin_hooks_from_layer_stack(            config_layer_stack,            self.remote_installed_plugin_configs(),            &self.store,            config.remote_plugin_enabled,        )        .await    }    /// Resolve plugin skill roots for a config layer stack without touching the plugins cache.    pub async fn effective_skill_roots_for_layer_stack(        &self,        config_layer_stack: &ConfigLayerStack,        config: &PluginsConfigInput,    ) -> Vec<PluginSkillRoot> {        self.plugins_for_layer_stack(config_layer_stack, config)            .await            .effective_plugin_skill_roots()    }    fn cached_loaded_plugins(&self, key: &PluginLoadCacheKey) -> Option<Vec<LoadedPlugin>> {        match self.loaded_plugins_cache.read() {            Ok(cache) => cache                .entry                .as_ref()                .filter(|cached| cached.key == *key)                .map(|cached| cached.plugins.clone()),            Err(err) => err                .into_inner()                .entry                .as_ref()                .filter(|cached| cached.key == *key)                .map(|cached| cached.plugins.clone()),        }    }    fn loaded_plugins_cache_generation(&self) -> u64 {        match self.loaded_plugins_cache.read() {            Ok(cache) => cache.generation,            Err(err) => err.into_inner().generation,        }    }    fn cache_loaded_plugins_if_current(        &self,        generation: u64,        key: PluginLoadCacheKey,        plugins: Vec<LoadedPlugin>,    ) {        let mut cache = match self.loaded_plugins_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        if cache.generation == generation {            cache.entry = Some(LoadedPluginsCacheEntry { key, plugins });        }    }    fn remote_installed_plugin_configs(&self) -> HashMap<String, PluginConfig> {        let cache = match self.remote_installed_plugins_cache.read() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        let Some(plugins) = cache.as_ref() else {            return HashMap::new();        };        remote_installed_plugins_to_config(plugins, &self.store)    }    pub fn build_remote_installed_plugin_marketplaces_from_cache(        &self,        visible_marketplaces: &[&str],    ) -> Option<Vec<crate::remote::RemoteMarketplace>> {        let cache = match self.remote_installed_plugins_cache.read() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        let plugins = cache.as_ref()?;        Some(            crate::remote::group_remote_installed_plugins_by_marketplaces(                plugins,                visible_marketplaces,            ),        )    }    pub fn cached_global_remote_discoverable_plugins_for_config(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,    ) -> Vec<crate::remote::RemoteDiscoverablePlugin> {        if !config.plugins_enabled || !config.remote_plugin_enabled {            return Vec::new();        }        let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {            return Vec::new();        };        let Some(account_id) = auth.get_account_id() else {            return Vec::new();        };        if account_id.is_empty() {            return Vec::new();        }        crate::remote::cached_global_remote_discoverable_plugins(            self.codex_home.as_path(),            &remote_plugin_service_config(config),            auth,        )    }    pub async fn build_and_cache_remote_installed_plugin_marketplaces(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,        visible_marketplaces: &[&str],        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {        let plugins = crate::remote::fetch_remote_installed_plugins(            &remote_plugin_service_config(config),            auth,        )        .await?;        let marketplaces = crate::remote::group_remote_installed_plugins_by_marketplaces(            &plugins,            visible_marketplaces,        );        let changed = self.write_remote_installed_plugins_cache(plugins);        if changed && let Some(on_effective_plugins_changed) = on_effective_plugins_changed {            on_effective_plugins_changed();        }        Ok(marketplaces)    }    fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool {        let mut cache = match self.remote_installed_plugins_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        if cache.as_ref().is_some_and(|cache| cache.eq(&plugins)) {            return false;        }        *cache = Some(plugins);        drop(cache);        self.clear_loaded_plugins_cache();        true    }    pub fn clear_remote_installed_plugins_cache(&self) -> bool {        let mut cache = match self.remote_installed_plugins_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        if cache.is_none() {            return false;        }        *cache = None;        drop(cache);        self.clear_loaded_plugins_cache();        true    }    pub fn maybe_start_remote_plugin_caches_refresh(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        self.maybe_start_remote_installed_plugins_cache_refresh_with_notify(            config,            auth.clone(),            RemoteInstalledPluginsCacheRefreshNotify::IfCacheChanged,            on_effective_plugins_changed,        );        let manager = Arc::clone(self);        let config = config.clone();        tokio::spawn(async move {            manager                .recommended_plugins_mode_for_config(&config, auth.as_ref())                .await;        });    }    pub fn maybe_start_remote_installed_plugins_cache_refresh_after_mutation(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        self.maybe_start_remote_installed_plugins_cache_refresh_with_notify(            config,            auth,            RemoteInstalledPluginsCacheRefreshNotify::AfterSuccessfulRefresh,            on_effective_plugins_changed,        );    }    fn maybe_start_remote_installed_plugins_cache_refresh_with_notify(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,        notify: RemoteInstalledPluginsCacheRefreshNotify,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        if !config.plugins_enabled {            return;        }        self.schedule_remote_installed_plugins_cache_refresh(            RemoteInstalledPluginsCacheRefreshRequest {                service_config: remote_plugin_service_config(config),                auth,                notify,                on_effective_plugins_changed,            },        );    }    pub fn maybe_start_remote_installed_plugin_bundle_sync(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        if !config.plugins_enabled {            return;        }        let manager = Arc::clone(self);        let config_for_refresh = config.clone();        let auth_for_refresh = auth.clone();        let on_local_cache_changed = Arc::new(move || {            manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(                &config_for_refresh,                auth_for_refresh.clone(),                on_effective_plugins_changed.clone(),            );        });        crate::remote::maybe_start_remote_installed_plugin_bundle_sync(            self.codex_home.clone(),            remote_plugin_service_config(config),            auth,            Some(on_local_cache_changed),        );    }    fn maybe_start_global_remote_catalog_cache_refresh(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,    ) {        if !config.plugins_enabled || !config.remote_plugin_enabled {            return;        }        self.schedule_global_remote_catalog_cache_refresh(GlobalRemoteCatalogCacheRefreshRequest {            service_config: remote_plugin_service_config(config),            auth,        });    }    pub fn maybe_start_plugin_list_background_tasks_for_config(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth: Option<CodexAuth>,        roots: &[AbsolutePathBuf],        options: PluginListBackgroundTaskOptions,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        self.maybe_start_non_curated_plugin_cache_refresh(roots);        if options.refresh_global_remote_catalog_cache {            self.maybe_start_global_remote_catalog_cache_refresh(config, auth.clone());        }        self.maybe_start_remote_plugin_caches_refresh(            config,            auth.clone(),            on_effective_plugins_changed.clone(),        );        self.maybe_start_remote_installed_plugin_bundle_sync(            config,            auth,            on_effective_plugins_changed,        );    }    fn cached_featured_plugin_ids(        &self,        cache_key: &FeaturedPluginIdsCacheKey,    ) -> Option<Vec<String>> {        {            let cache = match self.featured_plugin_ids_cache.read() {                Ok(cache) => cache,                Err(err) => err.into_inner(),            };            let now = Instant::now();            if let Some(cached) = cache.as_ref()                && now < cached.expires_at                && cached.key == *cache_key            {                return Some(cached.featured_plugin_ids.clone());            }        }        let mut cache = match self.featured_plugin_ids_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        let now = Instant::now();        if cache            .as_ref()            .is_some_and(|cached| now >= cached.expires_at || cached.key != *cache_key)        {            *cache = None;        }        None    }    fn write_featured_plugin_ids_cache(        &self,        cache_key: FeaturedPluginIdsCacheKey,        featured_plugin_ids: &[String],    ) {        let mut cache = match self.featured_plugin_ids_cache.write() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        *cache = Some(CachedFeaturedPluginIds {            key: cache_key,            expires_at: Instant::now() + FEATURED_PLUGIN_IDS_CACHE_TTL,            featured_plugin_ids: featured_plugin_ids.to_vec(),        });    }    pub async fn featured_plugin_ids_for_config(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,    ) -> Result<Vec<String>, RemotePluginFetchError> {        if !config.plugins_enabled {            return Ok(Vec::new());        }        let cache_key = featured_plugin_ids_cache_key(config, auth);        if let Some(featured_plugin_ids) = self.cached_featured_plugin_ids(&cache_key) {            return Ok(featured_plugin_ids);        }        let featured_plugin_ids = crate::remote_legacy::fetch_remote_featured_plugin_ids(            &remote_plugin_service_config(config),            auth,            self.restriction_product,        )        .await?;        self.write_featured_plugin_ids_cache(cache_key, &featured_plugin_ids);        Ok(featured_plugin_ids)    }    pub async fn recommended_plugins_mode_for_config(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,    ) -> RecommendedPluginsMode {        if !config.plugins_enabled            || !config.remote_plugin_enabled            || !auth.is_some_and(CodexAuth::uses_codex_backend)        {            return RecommendedPluginsMode::Legacy;        }        let cache_key = recommended_plugins_cache_key(config);        if let Some(cached) = self.cached_recommended_plugins_mode(&cache_key) {            return cached;        }        let refresh = {            let mut refreshes = match self.recommended_plugins_refreshes.write() {                Ok(refreshes) => refreshes,                Err(err) => err.into_inner(),            };            if let Some(cached) = self.cached_recommended_plugins_mode(&cache_key) {                return cached;            }            refreshes                .entry(cache_key.clone())                .or_insert_with(|| Arc::new(OnceCell::new()))                .clone()        };        let mode = refresh            .get_or_init(|| async {                match crate::remote::fetch_recommended_plugins(                    &remote_plugin_service_config(config),                    auth,                )                .await                {                    Ok(mode) => {                        let mut cache = match self.recommended_plugins_cache.write() {                            Ok(cache) => cache,                            Err(err) => err.into_inner(),                        };                        cache.insert(cache_key.clone(), mode.clone());                        mode                    }                    Err(err) => {                        warn!(error = %err, "failed to load recommended plugins");                        RecommendedPluginsMode::Legacy                    }                }            })            .await            .clone();        let mut refreshes = match self.recommended_plugins_refreshes.write() {            Ok(refreshes) => refreshes,            Err(err) => err.into_inner(),        };        if refreshes            .get(&cache_key)            .is_some_and(|current| Arc::ptr_eq(current, &refresh))        {            refreshes.remove(&cache_key);        }        mode    }    fn cached_recommended_plugins_mode(        &self,        cache_key: &RecommendedPluginsCacheKey,    ) -> Option<RecommendedPluginsMode> {        let cache = match self.recommended_plugins_cache.read() {            Ok(cache) => cache,            Err(err) => err.into_inner(),        };        cache.get(cache_key).cloned()    }    pub async fn install_plugin(        &self,        request: PluginInstallRequest,    ) -> Result<PluginInstallOutcome, PluginInstallError> {        let resolved = find_installable_marketplace_plugin(            &request.marketplace_path,            &request.plugin_name,            self.restriction_product,        )?;        self.install_resolved_plugin(resolved).await    }    pub async fn install_plugin_with_remote_sync(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,        request: PluginInstallRequest,    ) -> Result<PluginInstallOutcome, PluginInstallError> {        let resolved = find_installable_marketplace_plugin(            &request.marketplace_path,            &request.plugin_name,            self.restriction_product,        )?;        let plugin_id = resolved.plugin_id.as_key();        // This only forwards the backend mutation before the local install flow.        crate::remote_legacy::enable_remote_plugin(            &remote_plugin_service_config(config),            auth,            &plugin_id,        )        .await        .map_err(PluginInstallError::from)?;        self.install_resolved_plugin(resolved).await    }    async fn install_resolved_plugin(        &self,        resolved: ResolvedMarketplacePlugin,    ) -> Result<PluginInstallOutcome, PluginInstallError> {        let auth_policy = resolved.policy.authentication;        let plugin_version =            if is_openai_curated_marketplace_name(&resolved.plugin_id.marketplace_name) {                let curated_plugin_version = read_curated_plugins_sha(self.codex_home.as_path())                    .ok_or_else(|| {                        PluginStoreError::Invalid(                            "local curated marketplace sha is not available".to_string(),                        )                    })?;                Some(curated_plugin_cache_version(&curated_plugin_version))            } else {                None            };        let store = self.store.clone();        let codex_home = self.codex_home.clone();        let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {            let materialized =                materialize_marketplace_plugin_source(codex_home.as_path(), &resolved.source)                    .map_err(PluginStoreError::Invalid)?;            let source_path = materialized.path;            if let Some(plugin_version) = plugin_version {                store.install_with_version(source_path, resolved.plugin_id, plugin_version)            } else {                store.install(source_path, resolved.plugin_id)            }        })        .await        .map_err(PluginInstallError::join)??;        set_user_plugin_enabled(            &self.codex_home,            result.plugin_id.as_key(),            /*enabled*/ true,        )        .await        .map_err(anyhow::Error::from)?;        let analytics_events_client = match self.analytics_events_client.read() {            Ok(client) => client.clone(),            Err(err) => err.into_inner().clone(),        };        if let Some(analytics_events_client) = analytics_events_client {            analytics_events_client.track_plugin_installed(                plugin_telemetry_metadata_from_root(&result.plugin_id, &result.installed_path)                    .await,            );        }        Ok(PluginInstallOutcome {            plugin_id: result.plugin_id,            plugin_version: result.plugin_version,            installed_path: result.installed_path,            auth_policy,        })    }    pub async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError> {        let plugin_id = PluginId::parse(&plugin_id)?;        self.uninstall_plugin_id(plugin_id).await    }    pub async fn uninstall_plugin_with_remote_sync(        &self,        config: &PluginsConfigInput,        auth: Option<&CodexAuth>,        plugin_id: String,    ) -> Result<(), PluginUninstallError> {        // TODO: Remove this legacy remote-sync path once remote plugins have        // their own manager and installed-state API.        let plugin_id = PluginId::parse(&plugin_id)?;        let plugin_key = plugin_id.as_key();        // This only forwards the backend mutation before the local uninstall flow.        crate::remote_legacy::uninstall_remote_plugin(            &remote_plugin_service_config(config),            auth,            &plugin_key,        )        .await        .map_err(PluginUninstallError::from)?;        self.uninstall_plugin_id(plugin_id).await    }    async fn uninstall_plugin_id(&self, plugin_id: PluginId) -> Result<(), PluginUninstallError> {        let plugin_telemetry = if self.store.active_plugin_root(&plugin_id).is_some() {            Some(installed_plugin_telemetry_metadata(self.codex_home.as_path(), &plugin_id).await)        } else {            None        };        let store = self.store.clone();        let plugin_id_for_store = plugin_id.clone();        tokio::task::spawn_blocking(move || store.uninstall(&plugin_id_for_store))            .await            .map_err(PluginUninstallError::join)??;        clear_user_plugin(&self.codex_home, plugin_id.as_key())            .await            .map_err(anyhow::Error::from)?;        let analytics_events_client = match self.analytics_events_client.read() {            Ok(client) => client.clone(),            Err(err) => err.into_inner().clone(),        };        if let Some(plugin_telemetry) = plugin_telemetry            && let Some(analytics_events_client) = analytics_events_client        {            analytics_events_client.track_plugin_uninstalled(plugin_telemetry);        }        Ok(())    }    pub fn list_marketplaces_for_config(        &self,        config: &PluginsConfigInput,        additional_roots: &[AbsolutePathBuf],        include_openai_curated: bool,    ) -> Result<ConfiguredMarketplaceListOutcome, MarketplaceError> {        if !config.plugins_enabled {            return Ok(ConfiguredMarketplaceListOutcome::default());        }        let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);        let marketplace_roots =            self.marketplace_roots(config, additional_roots, include_openai_curated);        let marketplace_outcome = list_marketplaces(&marketplace_roots)?;        let mut seen_plugin_keys = HashSet::new();        let marketplaces = marketplace_outcome            .marketplaces            .into_iter()            .filter_map(|marketplace| {                let marketplace_name = marketplace.name.clone();                let plugins = marketplace                    .plugins                    .into_iter()                    .filter_map(|plugin| {                        let plugin_key = format!("{}@{marketplace_name}", plugin.name);                        if !seen_plugin_keys.insert(plugin_key.clone()) {                            return None;                        }                        if !self.restriction_product_matches(plugin.policy.products.as_deref()) {                            return None;                        }                        let plugin_id =                            PluginId::new(plugin.name.clone(), marketplace_name.clone()).ok();                        let installed = installed_plugins.contains(&plugin_key);                        let installed_version = installed.then_some(()).and_then(|_| {                            plugin_id                                .as_ref()                                .and_then(|plugin_id| self.store.active_plugin_version(plugin_id))                        });                        let enabled = enabled_plugins.contains(&plugin_key);                        let mut interface = plugin.interface;                        let mut local_version = plugin.local_version;                        if installed                            && matches!(&plugin.source, MarketplacePluginSource::Git { .. })                            && let Some(plugin_id) = plugin_id.as_ref()                            && let Some(plugin_root) = self.store.active_plugin_root(plugin_id)                            && let Some(manifest) = load_plugin_manifest(plugin_root.as_path())                        {                            local_version = manifest.version.clone();                            let marketplace_category = interface                                .as_ref()                                .and_then(|interface| interface.category.clone());                            interface = plugin_interface_with_marketplace_category(                                manifest.interface,                                marketplace_category,                            );                        }                        Some(ConfiguredMarketplacePlugin {                            // Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate                            // plugin entries from duplicate marketplace files intentionally                            // resolve to the first discovered source.                            id: plugin_key,                            installed_version,                            installed,                            enabled,                            name: plugin.name,                            local_version,                            source: plugin.source,                            policy: plugin.policy,                            keywords: plugin.keywords,                            interface,                        })                    })                    .collect::<Vec<_>>();                (!plugins.is_empty()).then_some(ConfiguredMarketplace {                    name: marketplace.name,                    path: marketplace.path,                    interface: marketplace.interface,                    plugins,                })            })            .collect();        Ok(ConfiguredMarketplaceListOutcome {            marketplaces,            errors: marketplace_outcome.errors,        })    }    pub fn discover_marketplaces_for_config(        &self,        config: &PluginsConfigInput,        additional_roots: &[AbsolutePathBuf],    ) -> Result<MarketplaceListOutcome, MarketplaceError> {        if !config.plugins_enabled {            return Ok(MarketplaceListOutcome::default());        }        list_marketplaces(&self.marketplace_roots(            config,            additional_roots,            /*include_openai_curated*/ true,        ))    }    pub async fn read_plugin_for_config(        &self,        config: &PluginsConfigInput,        request: &PluginReadRequest,    ) -> Result<PluginReadOutcome, MarketplaceError> {        if !config.plugins_enabled {            return Err(MarketplaceError::PluginsDisabled);        }        let plugin = find_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;        if !self.restriction_product_matches(plugin.policy.products.as_deref()) {            return Err(MarketplaceError::PluginNotFound {                plugin_name: plugin.plugin_id.plugin_name,                marketplace_name: plugin.plugin_id.marketplace_name,            });        }        let marketplace_name = plugin.plugin_id.marketplace_name.clone();        let plugin_key = plugin.plugin_id.as_key();        let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);        let installed = installed_plugins.contains(&plugin_key);        let installed_version = if installed {            self.store.active_plugin_version(&plugin.plugin_id)        } else {            None        };        let plugin = self            .read_plugin_detail_for_marketplace_plugin(                config,                &marketplace_name,                ConfiguredMarketplacePlugin {                    id: plugin_key.clone(),                    name: plugin.plugin_id.plugin_name,                    local_version: plugin                        .manifest                        .as_ref()                        .and_then(|manifest| manifest.version.clone()),                    installed_version,                    source: plugin.source,                    policy: plugin.policy,                    interface: plugin.interface,                    keywords: plugin                        .manifest                        .as_ref()                        .map(|manifest| manifest.keywords.clone())                        .unwrap_or_default(),                    installed,                    enabled: enabled_plugins.contains(&plugin_key),                },            )            .await?;        Ok(PluginReadOutcome {            marketplace_name,            marketplace_path: Some(request.marketplace_path.clone()),            plugin,        })    }    #[instrument(level = "trace", skip_all)]    pub async fn read_plugin_detail_for_marketplace_plugin(        &self,        config: &PluginsConfigInput,        marketplace_name: &str,        plugin: ConfiguredMarketplacePlugin,    ) -> Result<PluginDetail, MarketplaceError> {        if !self.restriction_product_matches(plugin.policy.products.as_deref()) {            return Err(MarketplaceError::PluginNotFound {                plugin_name: plugin.name,                marketplace_name: marketplace_name.to_string(),            });        }        let plugin_id =            PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| {                match err {                    PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),                }            })?;        let plugin_key = plugin_id.as_key();        if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && !plugin.installed {            let description = remote_plugin_install_required_description(&plugin.source);            return Ok(PluginDetail {                id: plugin_key,                name: plugin.name,                local_version: None,                description: Some(description),                source: plugin.source,                policy: plugin.policy,                interface: plugin.interface,                keywords: plugin.keywords,                installed: plugin.installed,                enabled: plugin.enabled,                skills: Vec::new(),                disabled_skill_paths: HashSet::new(),                hooks: Vec::new(),                apps: Vec::new(),                app_category_by_id: HashMap::new(),                mcp_server_names: Vec::new(),                details_unavailable_reason: Some(                    PluginDetailsUnavailableReason::InstallRequiredForRemoteSource,                ),            });        }        let source_path =            if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && plugin.installed {                self.store.active_plugin_root(&plugin_id).ok_or_else(|| {                    MarketplaceError::InvalidPlugin(format!(                        "installed plugin cache entry is missing for {plugin_key}"                    ))                })?            } else {                let codex_home = self.codex_home.clone();                let source = plugin.source.clone();                let materialized = tokio::task::spawn_blocking(move || {                    materialize_marketplace_plugin_source(codex_home.as_path(), &source)                })                .await                .map_err(|err| {                    MarketplaceError::InvalidPlugin(format!(                        "failed to materialize plugin source: {err}"                    ))                })?                .map_err(MarketplaceError::InvalidPlugin)?;                materialized.path.clone()            };        if !source_path.as_path().is_dir() {            return Err(MarketplaceError::InvalidPlugin(                "path does not exist or is not a directory".to_string(),            ));        }        let manifest = load_plugin_manifest(source_path.as_path()).ok_or_else(|| {            MarketplaceError::InvalidPlugin("missing or invalid plugin.json".to_string())        })?;        let description = manifest.description.clone();        let marketplace_category = plugin            .interface            .as_ref()            .and_then(|interface| interface.category.clone());        let interface = plugin_interface_with_marketplace_category(            manifest.interface.clone(),            marketplace_category,        );        let resolved_skills = load_plugin_skills(            &source_path,            &plugin_id,            &manifest.paths,            self.restriction_product,            &codex_core_skills::config_rules::skill_config_rules_from_stack(                &config.config_layer_stack,            ),        )        .await;        let plugin_data_root = self.store.plugin_data_root(&plugin_id);        let (hook_sources, _hook_load_warnings) =            load_plugin_hooks(&source_path, &plugin_id, &plugin_data_root, &manifest.paths);        let hooks = plugin_hook_declarations(&hook_sources)            .into_iter()            .map(|hook| PluginHookSummary {                key: hook.key,                event_name: hook.event_name,            })            .collect();        let auth_mode = self.auth_mode();        let mut app_declarations = load_plugin_apps(source_path.as_path()).await;        let mut mcp_servers = load_plugin_mcp_servers(source_path.as_path(), auth_mode).await;        if auth_mode.is_some() {            apply_app_mcp_routing_policy(                &mut app_declarations,                &mut mcp_servers,                auth_mode,                /*plugin_active*/ true,            );        }        let apps = app_connector_ids_from_declarations(&app_declarations);        let mut seen_app_connector_ids = HashSet::new();        let mut app_category_by_id = HashMap::new();        for app in &app_declarations {            if seen_app_connector_ids.insert(app.connector_id.0.as_str())                && let Some(category) = &app.category            {                app_category_by_id.insert(app.connector_id.0.clone(), category.clone());            }        }        let mut mcp_server_names = mcp_servers.into_keys().collect::<Vec<_>>();        mcp_server_names.sort_unstable();        mcp_server_names.dedup();        Ok(PluginDetail {            id: plugin.id,            name: plugin.name,            local_version: manifest.version.clone(),            description,            source: plugin.source,            policy: plugin.policy,            interface,            keywords: manifest.keywords,            installed: plugin.installed,            enabled: plugin.enabled,            skills: resolved_skills.skills,            disabled_skill_paths: resolved_skills.disabled_skill_paths,            hooks,            apps,            app_category_by_id,            mcp_server_names,            details_unavailable_reason: None,        })    }    pub fn maybe_start_plugin_startup_tasks_for_config(        self: &Arc<Self>,        config: &PluginsConfigInput,        auth_manager: Arc<AuthManager>,        on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,    ) {        if config.plugins_enabled {            self.start_curated_repo_sync();            let should_spawn_marketplace_auto_upgrade = {                let mut state = match self.configured_marketplace_upgrade_state.write() {                    Ok(state) => state,                    Err(err) => err.into_inner(),                };                if state.in_flight {                    false                } else {                    state.in_flight = true;                    true                }            };            if should_spawn_marketplace_auto_upgrade {                let manager = Arc::clone(self);                let config = config.clone();                if let Err(err) = std::thread::Builder::new()                    .name("plugins-marketplace-auto-upgrade".to_string())                    .spawn(move || {                        let outcome = manager.upgrade_configured_marketplaces_for_config(                            &config, /*marketplace_name*/ None,                        );                        match outcome {                            Ok(outcome) => {                                for error in outcome.errors {                                    warn!(                                        marketplace = error.marketplace_name,                                        error = %error.message,                                        "failed to auto-upgrade configured marketplace"                                    );                                }                            }                            Err(err) => {                                warn!("failed to auto-upgrade configured marketplaces: {err}");                            }                        }                        let mut state = match manager.configured_marketplace_upgrade_state.write() {                            Ok(state) => state,                            Err(err) => err.into_inner(),                        };                        state.in_flight = false;                    })                {                    let mut state = match self.configured_marketplace_upgrade_state.write() {                        Ok(state) => state,                        Err(err) => err.into_inner(),                    };                    state.in_flight = false;                    warn!("failed to start configured marketplace auto-upgrade task: {err}");                }            }            let config_for_remote_sync = config.clone();            let manager = Arc::clone(self);            let auth_manager_for_remote_sync = auth_manager.clone();            let on_effective_plugins_changed = on_effective_plugins_changed.clone();            tokio::spawn(async move {                let auth = auth_manager_for_remote_sync.auth().await;                manager.maybe_start_remote_plugin_caches_refresh(                    &config_for_remote_sync,                    auth.clone(),                    on_effective_plugins_changed.clone(),                );                manager.maybe_start_remote_installed_plugin_bundle_sync(                    &config_for_remote_sync,                    auth.clone(),                    on_effective_plugins_changed,                );                if config_for_remote_sync.remote_plugin_enabled {                    match crate::remote::fetch_and_cache_global_remote_plugin_catalog(                        manager.codex_home.as_path(),                        &remote_plugin_service_config(&config_for_remote_sync),                        auth.as_ref(),                    )                    .await                    {                        Ok(()) => {}                        Err(                            RemotePluginCatalogError::AuthRequired                            | RemotePluginCatalogError::UnsupportedAuthMode,                        ) => {}                        Err(err) => {                            warn!(                                error = %err,                                "failed to warm remote plugin catalog cache"                            );                        }                    }                }            });            let config_for_featured_plugins = config.clone();            let manager = Arc::clone(self);            tokio::spawn(async move {                let auth = auth_manager.auth().await;                if let Err(err) = manager                    .featured_plugin_ids_for_config(&config_for_featured_plugins, auth.as_ref())                    .await                {                    warn!(                        error = %err,                        "failed to warm featured plugin ids cache"                    );                }            });        }    }    pub fn upgrade_configured_marketplaces_for_config(        &self,        config: &PluginsConfigInput,        marketplace_name: Option<&str>,    ) -> Result<ConfiguredMarketplaceUpgradeOutcome, String> {        if let Some(marketplace_name) = marketplace_name            && !configured_git_marketplace_names(&config.config_layer_stack)                .iter()                .any(|name| name == marketplace_name)        {            return Err(format!(                "marketplace `{marketplace_name}` is not configured as a Git marketplace"            ));        }        let mut outcome = upgrade_configured_git_marketplaces(            self.codex_home.as_path(),            &config.config_layer_stack,            marketplace_name,        );        if !outcome.upgraded_roots.is_empty() {            match refresh_non_curated_plugin_cache_force_reinstall(                self.codex_home.as_path(),                &outcome.upgraded_roots,            ) {                Ok(cache_refreshed) => {                    if cache_refreshed {                        self.clear_cache();                    }                }                Err(err) => {                    self.clear_cache();                    outcome.errors.push(ConfiguredMarketplaceUpgradeError {                        marketplace_name: marketplace_name                            .unwrap_or("all configured marketplaces")                            .to_string(),                        message: format!(                            "failed to refresh installed plugin cache after marketplace upgrade: {err}"                        ),                    });                }            }        }        Ok(outcome)    }    pub fn maybe_start_non_curated_plugin_cache_refresh(        self: &Arc<Self>,        roots: &[AbsolutePathBuf],    ) {        self.schedule_non_curated_plugin_cache_refresh(            roots,            NonCuratedCacheRefreshMode::IfVersionChanged,        );    }    fn schedule_remote_installed_plugins_cache_refresh(        self: &Arc<Self>,        mut request: RemoteInstalledPluginsCacheRefreshRequest,    ) {        let should_spawn = {            let mut state = match self.remote_installed_plugins_cache_refresh_state.write() {                Ok(state) => state,                Err(err) => err.into_inner(),            };            if let Some(existing_request) = state.requested.as_ref() {                if matches!(                    existing_request.notify,                    RemoteInstalledPluginsCacheRefreshNotify::AfterSuccessfulRefresh                ) {                    request.notify =                        RemoteInstalledPluginsCacheRefreshNotify::AfterSuccessfulRefresh;                }                if request.on_effective_plugins_changed.is_none() {                    request.on_effective_plugins_changed =                        existing_request.on_effective_plugins_changed.clone();                }            }            state.requested = Some(request);            if state.in_flight {                false            } else {                state.in_flight = true;                true            }        };        if !should_spawn {            return;        }        let manager = Arc::clone(self);        tokio::spawn(async move {            manager                .run_remote_installed_plugins_cache_refresh_loop()                .await;        });    }    fn schedule_global_remote_catalog_cache_refresh(        self: &Arc<Self>,        request: GlobalRemoteCatalogCacheRefreshRequest,    ) {        let should_spawn = {            let mut state = match self.global_remote_catalog_cache_refresh_state.write() {                Ok(state) => state,                Err(err) => err.into_inner(),            };            state.requested = Some(request);            if state.in_flight {                false            } else {                state.in_flight = true;                true            }        };        if !should_spawn {            return;        }        let manager = Arc::clone(self);        tokio::spawn(async move {            manager.run_global_remote_catalog_cache_refresh_loop().await;        });    }    fn schedule_non_curated_plugin_cache_refresh(        self: &Arc<Self>,        roots: &[AbsolutePathBuf],        mode: NonCuratedCacheRefreshMode,    ) {        let mut roots = roots.to_vec();        roots.sort_unstable();        roots.dedup();        if roots.is_empty() {            return;        }        let request = NonCuratedCacheRefreshRequest { roots, mode };        let should_spawn = {            let mut state = match self.non_curated_cache_refresh_state.write() {                Ok(state) => state,                Err(err) => err.into_inner(),            };            // Collapse repeated plugin/list requests onto one worker and only queue another pass            // when the requested roots set actually changes. Forced reinstall requests are not            // deduped against the last completed pass because the same marketplace root path can            // point at newly activated files after an auto-upgrade.            if state.requested.as_ref() == Some(&request)                || (mode == NonCuratedCacheRefreshMode::IfVersionChanged                    && !state.in_flight                    && state.last_refreshed.as_ref() == Some(&request))            {                return;            }            if mode == NonCuratedCacheRefreshMode::IfVersionChanged                && state.requested.as_ref().is_some_and(|requested| {                    requested.mode == NonCuratedCacheRefreshMode::ForceReinstall                        && requested.roots == request.roots                })            {                return;            }            state.requested = Some(request);            if state.in_flight {                false            } else {                state.in_flight = true;                true            }        };        if !should_spawn {            return;        }        let manager = Arc::clone(self);        if let Err(err) = std::thread::Builder::new()            .name("plugins-non-curated-cache-refresh".to_string())            .spawn(move || manager.run_non_curated_plugin_cache_refresh_loop())        {            let mut state = match self.non_curated_cache_refresh_state.write() {                Ok(state) => state,                Err(err) => err.into_inner(),            };            state.in_flight = false;            state.requested = None;            warn!("failed to start non-curated plugin cache refresh task: {err}");        }    }    fn start_curated_repo_sync(self: &Arc<Self>) {        if CURATED_REPO_SYNC_STARTED.swap(true, Ordering::SeqCst) {            return;        }        let manager = Arc::clone(self);        let codex_home = self.codex_home.clone();        if let Err(err) = std::thread::Builder::new()            .name("plugins-curated-repo-sync".to_string())            .spawn(                move || match sync_openai_plugins_repo(codex_home.as_path()) {                    Ok(curated_plugin_version) => {                        let configured_curated_plugin_ids =                            configured_curated_plugin_ids_from_codex_home(codex_home.as_path());                        match refresh_curated_plugin_cache(                            codex_home.as_path(),                            &curated_plugin_version,                            &configured_curated_plugin_ids,                        ) {                            Ok(cache_refreshed) => {                                if cache_refreshed {                                    manager.clear_cache();                                }                            }                            Err(err) => {                                manager.clear_cache();                                CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);                                warn!("failed to refresh curated plugin cache after sync: {err}");                            }                        }                    }                    Err(err) => {                        CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);                        warn!("failed to sync curated plugins repo: {err}");                    }                },            )        {            CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);            warn!("failed to start curated plugins repo sync task: {err}");        }    }    async fn run_remote_installed_plugins_cache_refresh_loop(self: Arc<Self>) {        loop {            let request = {                let mut state = match self.remote_installed_plugins_cache_refresh_state.write() {                    Ok(state) => state,                    Err(err) => err.into_inner(),                };                match state.requested.take() {                    Some(request) => request,                    None => {                        state.in_flight = false;                        return;                    }                }            };            let installed_plugins = crate::remote::fetch_remote_installed_plugins(                &request.service_config,                request.auth.as_ref(),            )            .await;            match installed_plugins {                Ok(installed_plugins) => {                    // TODO(remote plugins): reconcile missing or stale local bundles before                    // publishing remote installed state as effective local plugin config.                    let changed = self.write_remote_installed_plugins_cache(installed_plugins);                    let should_notify = changed                        || matches!(                            request.notify,                            RemoteInstalledPluginsCacheRefreshNotify::AfterSuccessfulRefresh                        );                    if should_notify                        && let Some(on_effective_plugins_changed) =                            request.on_effective_plugins_changed                    {                        on_effective_plugins_changed();                    }                }                Err(                    RemotePluginCatalogError::AuthRequired                    | RemotePluginCatalogError::UnsupportedAuthMode,                ) => {                    let changed = self.clear_remote_installed_plugins_cache();                    if changed                        && let Some(on_effective_plugins_changed) =                            request.on_effective_plugins_changed                    {                        on_effective_plugins_changed();                    }                }                Err(err) => {                    warn!(                        error = %err,                        "failed to refresh remote installed plugins cache"                    );                }            }        }    }    async fn run_global_remote_catalog_cache_refresh_loop(self: Arc<Self>) {        loop {            let request = {                let mut state = match self.global_remote_catalog_cache_refresh_state.write() {                    Ok(state) => state,                    Err(err) => err.into_inner(),                };                match state.requested.take() {                    Some(request) => request,                    None => {                        state.in_flight = false;                        return;                    }                }            };            match crate::remote::fetch_and_cache_global_remote_plugin_catalog(                self.codex_home.as_path(),                &request.service_config,                request.auth.as_ref(),            )            .await            {                Ok(()) => {}                Err(                    RemotePluginCatalogError::AuthRequired                    | RemotePluginCatalogError::UnsupportedAuthMode,                ) => {}                Err(err) => {                    warn!(                        error = %err,                        "failed to refresh cached global remote plugin catalog"                    );                }            }        }    }    fn run_non_curated_plugin_cache_refresh_loop(self: Arc<Self>) {        loop {            let request = {                let state = match self.non_curated_cache_refresh_state.read() {                    Ok(state) => state,                    Err(err) => err.into_inner(),                };                state.requested.clone()            };            let Some(request) = request else {                let mut state = match self.non_curated_cache_refresh_state.write() {                    Ok(state) => state,                    Err(err) => err.into_inner(),                };                state.in_flight = false;                return;            };            let refresh_result = match request.mode {                NonCuratedCacheRefreshMode::IfVersionChanged => {                    refresh_non_curated_plugin_cache(self.codex_home.as_path(), &request.roots)                }                NonCuratedCacheRefreshMode::ForceReinstall => {                    refresh_non_curated_plugin_cache_force_reinstall(                        self.codex_home.as_path(),                        &request.roots,                    )                }            };            let refreshed = match refresh_result {                Ok(cache_refreshed) => {                    if cache_refreshed {                        self.clear_cache();                    }                    true                }                Err(err) => {                    self.clear_cache();                    warn!("failed to refresh non-curated plugin cache: {err}");                    false                }            };            let mut state = match self.non_curated_cache_refresh_state.write() {                Ok(state) => state,                Err(err) => err.into_inner(),            };            if refreshed {                state.last_refreshed = Some(request.clone());            }            if state.requested.as_ref() == Some(&request) {                state.requested = None;                state.in_flight = false;                return;            }        }    }    fn configured_plugin_states(        &self,        config: &PluginsConfigInput,    ) -> (HashSet<String>, HashSet<String>) {        let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);        let installed_plugins = configured_plugins            .keys()            .filter(|plugin_key| {                PluginId::parse(plugin_key)                    .ok()                    .is_some_and(|plugin_id| self.store.is_installed(&plugin_id))            })            .cloned()            .collect::<HashSet<_>>();        let enabled_plugins = configured_plugins            .into_iter()            .filter_map(|(plugin_key, plugin)| plugin.enabled.then_some(plugin_key))            .collect::<HashSet<_>>();        (installed_plugins, enabled_plugins)    }    fn marketplace_roots(        &self,        config: &PluginsConfigInput,        additional_roots: &[AbsolutePathBuf],        include_openai_curated: bool,    ) -> Vec<AbsolutePathBuf> {        // Treat the curated catalog as an extra marketplace root so plugin listing can surface it        // without requiring every caller to know where it is stored.        let mut roots = additional_roots.to_vec();        roots.extend(installed_marketplace_roots_from_layer_stack(            &config.config_layer_stack,            self.codex_home.as_path(),        ));        let curated_marketplace_path = if include_openai_curated {            if matches!(                self.auth_mode(),                Some(AuthMode::ApiKey | AuthMode::BedrockApiKey)            ) {                let api_marketplace_path =                    curated_plugins_api_marketplace_path(self.codex_home.as_path());                api_marketplace_path                    .is_file()                    .then_some(api_marketplace_path)            } else {                let curated_repo_root = curated_plugins_repo_path(self.codex_home.as_path());                curated_repo_root.is_dir().then_some(curated_repo_root)            }        } else {            None        };        if let Some(curated_marketplace_path) = curated_marketplace_path            && let Ok(curated_marketplace_path) =                AbsolutePathBuf::try_from(curated_marketplace_path)        {            roots.push(curated_marketplace_path);        }        roots.sort_unstable();        roots.dedup();        roots    }}fn remote_plugin_install_required_description(source: &MarketplacePluginSource) -> String {    let source_description = match source {        MarketplacePluginSource::Git {            url,            path,            ref_name,            sha,        } => {            let mut parts = vec![url.clone()];            if let Some(path) = path {                parts.push(format!("path `{path}`"));            }            if let Some(ref_name) = ref_name {                parts.push(format!("ref `{ref_name}`"));            }            if let Some(sha) = sha {                parts.push(format!("sha `{sha}`"));            }            parts.join(", ")        }        MarketplacePluginSource::Local { path } => path.as_path().display().to_string(),    };    format!(        "This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {source_description}."    )}#[derive(Debug, thiserror::Error)]pub enum PluginInstallError {    #[error("{0}")]    Marketplace(#[from] MarketplaceError),    #[error("{0}")]    Remote(#[from] RemotePluginMutationError),    #[error("{0}")]    Store(#[from] PluginStoreError),    #[error("{0}")]    Config(#[from] anyhow::Error),    #[error("failed to join plugin install task: {0}")]    Join(#[from] tokio::task::JoinError),}impl PluginInstallError {    fn join(source: tokio::task::JoinError) -> Self {        Self::Join(source)    }    pub fn is_invalid_request(&self) -> bool {        matches!(            self,            Self::Marketplace(                MarketplaceError::MarketplaceNotFound { .. }                    | MarketplaceError::InvalidMarketplaceFile { .. }                    | MarketplaceError::PluginNotFound { .. }                    | MarketplaceError::PluginNotAvailable { .. }                    | MarketplaceError::InvalidPlugin(_)            ) | Self::Store(PluginStoreError::Invalid(_))        )    }}#[derive(Debug, thiserror::Error)]pub enum PluginUninstallError {    #[error("{0}")]    InvalidPluginId(#[from] PluginIdError),    #[error("{0}")]    Remote(#[from] RemotePluginMutationError),    #[error("{0}")]    Store(#[from] PluginStoreError),    #[error("{0}")]    Config(#[from] anyhow::Error),    #[error("failed to join plugin uninstall task: {0}")]    Join(#[from] tokio::task::JoinError),}impl PluginUninstallError {    fn join(source: tokio::task::JoinError) -> Self {        Self::Join(source)    }    pub fn is_invalid_request(&self) -> bool {        matches!(self, Self::InvalidPluginId(_))    }}pub(crate) fn configured_plugins_from_stack(    config_layer_stack: &ConfigLayerStack,) -> HashMap<String, PluginConfig> {    // Plugin entries remain persisted user config only.    let Some(user_config) = config_layer_stack.effective_user_config() else {        return HashMap::new();    };    configured_plugins_from_user_config_value(&user_config)}fn configured_plugins_from_user_config_value(    user_config: &toml::Value,) -> HashMap<String, PluginConfig> {    let Some(plugins_value) = user_config.get("plugins") else {        return HashMap::new();    };    match plugins_value.clone().try_into() {        Ok(plugins) => plugins,        Err(err) => {            warn!("invalid plugins config: {err}");            HashMap::new()        }    }}#[cfg(test)]#[path = "manager_tests.rs"]mod tests;