[codex] Ignore local curated plugins when remote catalog is active (#29765)

## Summary

- suppress configured `openai-curated` plugins when the remote plugin
feature is enabled and auth uses the Codex backend
- preserve `openai-api-curated` and non-Codex-backend behavior while
including remote catalog activation in the plugin load cache key
- add core plugin coverage and an app-server integration test for
runtime feature enablement

## Why

The Codex app enables remote plugins through process-local runtime
feature enablement, which can happen after app-server startup tasks have
already observed legacy local plugin state. The existing conflict logic
only preferred a remote plugin when the same plugin was already
installed remotely, so a configured legacy-only plugin could continue
exposing skills and other capabilities from `openai-curated`.

## Impact

When the remote catalog is active, legacy `openai-curated` plugins no
longer contribute skills, MCP servers, apps, or hooks. Remote installed
plugins continue to load normally, and `openai-api-curated` remains
unaffected. This does not change remote fetch, bundle sync, or uninstall
behavior.

## Validation

- `just test -p codex-core-plugins
remote_global_catalog_ignores_local_curated_plugins
remote_plugin_feature_keeps_local_curated_without_codex_backend`
- `just test -p codex-app-server
runtime_remote_plugin_enablement_excludes_local_curated_plugin_skills`
- `just fmt`
- `git diff --check`
This commit is contained in:
xl-openai
2026-06-23 19:51:31 -07:00
committed by GitHub
parent 2696e7199b
commit ff78e21215
5 changed files with 189 additions and 30 deletions
+18 -15
View File
@@ -118,14 +118,14 @@ pub(crate) async fn load_plugins_from_layer_stack(
store: &PluginStore,
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
restriction_product: Option<Product>,
prefer_remote_curated_conflicts: bool,
remote_global_catalog_active: bool,
) -> Vec<LoadedPlugin<McpServerConfig>> {
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
load_plugins_from_layer_stack_with_scope(
config_layer_stack,
extra_plugins,
store,
prefer_remote_curated_conflicts,
remote_global_catalog_active,
PluginLoadScope::AllCapabilities {
restriction_product,
skill_config_rules: &skill_config_rules,
@@ -139,14 +139,14 @@ async fn load_plugins_from_layer_stack_with_scope(
config_layer_stack: &ConfigLayerStack,
extra_plugins: HashMap<String, PluginConfig>,
store: &PluginStore,
prefer_remote_curated_conflicts: bool,
remote_global_catalog_active: bool,
scope: PluginLoadScope<'_>,
) -> Vec<LoadedPlugin<McpServerConfig>> {
let configured_plugins = merge_configured_plugins_with_remote_installed(
configured_plugins_from_stack(config_layer_stack),
extra_plugins,
store,
prefer_remote_curated_conflicts,
remote_global_catalog_active,
);
let mut configured_plugins: Vec<_> = configured_plugins.into_iter().collect();
configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
@@ -178,13 +178,13 @@ pub async fn load_plugin_hooks_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
extra_plugins: HashMap<String, PluginConfig>,
store: &PluginStore,
prefer_remote_curated_conflicts: bool,
remote_global_catalog_active: bool,
) -> PluginHookLoadOutcome {
let plugins = load_plugins_from_layer_stack_with_scope(
config_layer_stack,
extra_plugins,
store,
prefer_remote_curated_conflicts,
remote_global_catalog_active,
PluginLoadScope::HooksOnly,
)
.await;
@@ -206,8 +206,17 @@ fn merge_configured_plugins_with_remote_installed(
mut configured_plugins: HashMap<String, PluginConfig>,
extra_plugins: HashMap<String, PluginConfig>,
store: &PluginStore,
prefer_remote_curated_conflicts: bool,
remote_global_catalog_active: bool,
) -> HashMap<String, PluginConfig> {
if remote_global_catalog_active {
configured_plugins.retain(|plugin_key, _| match PluginId::parse(plugin_key) {
Ok(plugin_id) => plugin_id.marketplace_name != crate::OPENAI_CURATED_MARKETPLACE_NAME,
Err(_) => true,
});
configured_plugins.extend(extra_plugins);
return configured_plugins;
}
let mut local_curated_installed_plugin_keys = HashMap::<String, Vec<String>>::new();
for plugin_key in configured_plugins.keys() {
let Ok(plugin_id) = PluginId::parse(plugin_key) else {
@@ -234,14 +243,8 @@ fn merge_configured_plugins_with_remote_installed(
.as_ref()
.and_then(|plugin_name| local_curated_installed_plugin_keys.get(plugin_name));
if let Some(local_curated_plugin_keys) = local_curated_plugin_keys {
if prefer_remote_curated_conflicts {
for local_curated_plugin_key in local_curated_plugin_keys {
configured_plugins.remove(local_curated_plugin_key);
}
} else {
continue;
}
if local_curated_plugin_keys.is_some() {
continue;
}
configured_plugins.insert(plugin_key, plugin_config);