Prefer remote Sites over the bundled plugin (#42100)

## Why

When the remote Sites plugin is installed and its local bundle is available, exposing the bundled copy as well creates duplicate catalog entries and can load the wrong skills.

## What changed

- Persist an account- and backend-scoped exclusion for `sites@openai-bundled` once the remote replacement is installed and loadable.
- Apply the exclusion to plugin catalogs, search, direct reads and installs, runtime loading, hooks, and skill caches while preserving the remote plugin's server-owned enabled state.
- Restore the bundled fallback when the remote plugin is absent or its local files are unavailable, and throttle repeated availability checks for 60 seconds.

## Testing

- Added manager tests for persistence across restarts, account/backend isolation, fallback restoration, concurrent checks, and refresh throttling.
- Added app-server and agent-turn tests confirming that catalog, search, and skill loading prefer remote Sites.

GitOrigin-RevId: bc1154f79d3107910fee9fa27389a0ddb48ce6cf
This commit is contained in:
Jiwon Kim
2026-09-01 16:58:54 +00:00
committed by copyberry
parent a17ee5705c
commit b6ab99ed87
16 changed files with 864 additions and 27 deletions
+10 -1
View File
@@ -53,6 +53,7 @@ use codex_utils_plugins::SkillDiscoveryMode;
use codex_utils_plugins::find_plugin_manifest_path;
use codex_utils_plugins::migrated_command_skills_root;
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
@@ -128,6 +129,8 @@ pub(crate) fn log_plugin_load_errors(plugins: &[LoadedPlugin<McpServerConfig>])
}
/// Load configured plugins without applying auth-dependent runtime policies.
// TODO(sites-migration): Remove the exclusion parameter and lint allowance when bundled Sites is retired.
#[allow(clippy::too_many_arguments)]
#[instrument(level = "trace", skip_all)]
pub(crate) async fn load_plugins_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
@@ -137,6 +140,7 @@ pub(crate) async fn load_plugins_from_layer_stack(
restriction_product: Option<Product>,
remote_global_catalog_active: bool,
skill_root_loader: &dyn SkillRootLoader<PluginSkillRoot>,
excluded_plugin_ids: &BTreeSet<String>,
) -> Vec<LoadedPlugin<McpServerConfig>> {
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
let RemoteInstalledPluginsSnapshot {
@@ -146,6 +150,7 @@ pub(crate) async fn load_plugins_from_layer_stack(
load_plugins_from_layer_stack_with_scope(
config_layer_stack,
extra_plugins,
excluded_plugin_ids,
store,
remote_global_catalog_active,
PluginLoadScope::AllCapabilities {
@@ -162,16 +167,18 @@ pub(crate) async fn load_plugins_from_layer_stack(
async fn load_plugins_from_layer_stack_with_scope(
config_layer_stack: &ConfigLayerStack,
extra_plugins: HashMap<String, PluginConfig>,
excluded_plugin_ids: &BTreeSet<String>,
store: &PluginStore,
remote_global_catalog_active: bool,
scope: PluginLoadScope<'_>,
) -> Vec<LoadedPlugin<McpServerConfig>> {
let configured_plugins = merge_configured_plugins_with_remote_installed(
let mut configured_plugins = merge_configured_plugins_with_remote_installed(
configured_plugins_from_stack(config_layer_stack, store.codex_home().as_path()),
extra_plugins,
store,
remote_global_catalog_active,
);
configured_plugins.retain(|id, _| !excluded_plugin_ids.contains(id));
let mut configured_plugins: Vec<_> = configured_plugins.into_iter().collect();
configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
@@ -201,6 +208,7 @@ async fn load_plugins_from_layer_stack_with_scope(
pub async fn load_plugin_hooks_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
extra_plugins: HashMap<String, PluginConfig>,
excluded_plugin_ids: &BTreeSet<String>,
store: &PluginStore,
target_curated_marketplace: TargetCuratedMarketplace,
remote_global_catalog_active: bool,
@@ -208,6 +216,7 @@ pub async fn load_plugin_hooks_from_layer_stack(
let mut plugins = load_plugins_from_layer_stack_with_scope(
config_layer_stack,
extra_plugins,
excluded_plugin_ids,
store,
remote_global_catalog_active,
PluginLoadScope::HooksOnly,