Files
codex/codex-rs/plugin/src/lib.rs
T
felixxia-oai 51316ead4a [codex] Dedupe plugin MCPs by app declaration name (#27607)
## Context

This is the next step in the plugin auth-routing stack. The earlier PRs
make `PluginsManager` auth-aware and move the broad App/MCP surface
decision into that layer. This PR narrows the ChatGPT/SIWC behavior so
we only hide a plugin MCP server when it conflicts with an App
declaration of the same name.

In product terms: if a plugin exposes both an App route and MCP route
for `foo`, ChatGPT/SIWC sessions should use the App route for `foo`. If
the same plugin also exposes a separate MCP server like `foo2`, that MCP
server should remain available.

```json
// .app.json
{
  "apps": {
    "foo": {
      "id": "connector_abc"
    }
  }
}
```

```json
// .mcp.json
{
  "mcpServers": {
    "foo": {
      "url": "https://mcp.foo.com/mcp"
    },
    "foo2": {
      "url": "https://mcp.foo2.com/mcp"
    }
  }
}
```

## Stack

- PR1: #27652 seed plugin manager auth at construction.
- PR2: #27459 route plugin surfaces by auth mode.
- PR3: #27607 dedupe plugin MCP servers by App declaration name.
- PR4: #27602 preserve plugin Apps in connector listings.
- PR5: #27461 skip install-time plugin MCP OAuth for matching App
routes.

## Summary

- Preserve App declaration names in loaded plugin metadata.
- Keep public effective App outputs as deduped connector IDs for
existing callers.
- For ChatGPT/SIWC, suppress only plugin MCP servers whose names match
declared App names.

## Validation

```bash
cargo fmt --all
cargo test -p codex-core-plugins plugin_auth_projection
cargo test -p codex-core-plugins effective_apps
cargo test -p codex-core-plugins read_plugin_for_config_installed_git_source_reads_from_cache_without_cloning
cargo test -p codex-core explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins
cargo test -p codex-core explicit_plugin_mentions_keep_non_conflicting_mcp_for_chatgpt_auth
cargo test -p codex-app-server --test all plugin_install_filters_disallowed_apps_needing_auth
git diff --check
```

---------

Co-authored-by: Xin Lin <xl@openai.com>
2026-06-13 17:53:09 -07:00

101 lines
3.0 KiB
Rust

//! Shared plugin package models, source providers, identifiers, and telemetry summaries.
use std::collections::HashSet;
pub use codex_utils_plugins::mention_syntax;
pub use codex_utils_plugins::plugin_namespace_for_skill_path;
mod load_outcome;
pub mod manifest;
mod plugin_id;
mod provider;
use codex_config::HookEventsToml;
use codex_utils_absolute_path::AbsolutePathBuf;
pub use load_outcome::EffectiveSkillRoots;
pub use load_outcome::LoadedPlugin;
pub use load_outcome::PluginLoadOutcome;
pub use load_outcome::prompt_safe_plugin_description;
pub use plugin_id::PluginId;
pub use plugin_id::PluginIdError;
pub use plugin_id::validate_plugin_segment;
pub use provider::PluginProvider;
pub use provider::PluginResourceLocator;
pub use provider::ResolvedPlugin;
pub use provider::ResolvedPluginError;
pub use provider::ResolvedPluginLocation;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AppConnectorId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppDeclaration {
pub name: String,
pub connector_id: AppConnectorId,
pub category: Option<String>,
}
pub fn app_connector_ids_from_declarations<'a>(
app_declarations: impl IntoIterator<Item = &'a AppDeclaration>,
) -> Vec<AppConnectorId> {
let mut connector_ids = Vec::new();
let mut seen_connector_ids = HashSet::new();
for app in app_declarations {
if seen_connector_ids.insert(&app.connector_id) {
connector_ids.push(app.connector_id.clone());
}
}
connector_ids
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginCapabilitySummary {
pub config_name: String,
pub display_name: String,
pub description: Option<String>,
pub has_skills: bool,
pub mcp_server_names: Vec<String>,
pub app_connector_ids: Vec<AppConnectorId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginHookSource {
pub plugin_id: PluginId,
pub plugin_root: AbsolutePathBuf,
pub plugin_data_root: AbsolutePathBuf,
pub source_path: AbsolutePathBuf,
pub source_relative_path: String,
pub hooks: HookEventsToml,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginTelemetryMetadata {
pub plugin_id: PluginId,
/// Optional backend identifier for remote plugins, used when analytics
/// should report the remote id instead of the local plugin cache id.
pub remote_plugin_id: Option<String>,
pub capability_summary: Option<PluginCapabilitySummary>,
}
impl PluginTelemetryMetadata {
pub fn from_plugin_id(plugin_id: &PluginId) -> Self {
Self {
plugin_id: plugin_id.clone(),
remote_plugin_id: None,
capability_summary: None,
}
}
}
impl PluginCapabilitySummary {
pub fn telemetry_metadata(&self) -> Option<PluginTelemetryMetadata> {
PluginId::parse(&self.config_name)
.ok()
.map(|plugin_id| PluginTelemetryMetadata {
plugin_id,
remote_plugin_id: None,
capability_summary: Some(self.clone()),
})
}
}