From 46d87f3b1c34c8355d17b04d07eff54dfc40cc62 Mon Sep 17 00:00:00 2001 From: Anthony Tafoya Date: Fri, 18 Sep 2026 22:17:32 +0000 Subject: [PATCH] Add shared plugin catalog discovery APIs (#46558) ## What changed - Add `PluginCatalog` types for discovery snapshots, warnings, plugin metadata, stable identities, and cloud or executor source locations. Keep MCP server declaration values out of debug output. - Extend `PluginProvider` with a generic list query, asynchronous catalog listing, and shared error and future types. Provide empty defaults for listing and root resolution so providers can implement their supported operation. - Add `PluginListQuery` in the MCP extension with thread and turn identifiers and an optional MCP resource client. GitOrigin-RevId: 0bf2a1713c25d912c41daab788b88729d4871fcf --- codex-rs/ext/mcp/src/lib.rs | 5 +++ codex-rs/ext/mcp/src/provider.rs | 16 +++++++ codex-rs/plugin/src/catalog.rs | 76 ++++++++++++++++++++++++++++++++ codex-rs/plugin/src/lib.rs | 7 +++ codex-rs/plugin/src/provider.rs | 36 +++++++++++---- 5 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 codex-rs/ext/mcp/src/provider.rs create mode 100644 codex-rs/plugin/src/catalog.rs diff --git a/codex-rs/ext/mcp/src/lib.rs b/codex-rs/ext/mcp/src/lib.rs index 802c4a7080..22ee8fe20b 100644 --- a/codex-rs/ext/mcp/src/lib.rs +++ b/codex-rs/ext/mcp/src/lib.rs @@ -11,8 +11,13 @@ use codex_mcp::hosted_plugin_runtime_mcp_server_config; #[path = "event_stream_tests.rs"] mod event_stream_tests; mod executor_plugin; +mod provider; mod stream_manager; +pub use provider::PluginListQuery; +pub use provider::PluginProvider; +pub use provider::PluginProviderError; +pub use provider::PluginProviderFuture; pub use stream_manager::McpEventStreamManager; pub use stream_manager::McpEventStreamUpdate; diff --git a/codex-rs/ext/mcp/src/provider.rs b/codex-rs/ext/mcp/src/provider.rs new file mode 100644 index 0000000000..1fbe5d8915 --- /dev/null +++ b/codex-rs/ext/mcp/src/provider.rs @@ -0,0 +1,16 @@ +//! MCP context passed to plugin catalog providers. + +use codex_mcp::McpResourceClient; +use std::sync::Arc; + +pub use codex_plugin::PluginProvider; +pub use codex_plugin::PluginProviderError; +pub use codex_plugin::PluginProviderFuture; + +/// Turn identifiers and MCP client used to request one catalog snapshot. +#[derive(Clone, Debug)] +pub struct PluginListQuery { + pub thread_id: String, + pub turn_id: String, + pub mcp_resources: Option>, +} diff --git a/codex-rs/plugin/src/catalog.rs b/codex-rs/plugin/src/catalog.rs new file mode 100644 index 0000000000..7e53d955fc --- /dev/null +++ b/codex-rs/plugin/src/catalog.rs @@ -0,0 +1,76 @@ +//! Source-neutral plugin discovery types. + +use codex_utils_path_uri::PathUri; +use std::collections::BTreeMap; + +/// One source's complete discovery snapshot; no entries means no plugins were found. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PluginCatalog { + pub entries: Vec, + pub warnings: Vec, +} + +/// Manifest metadata and every location that can supply the same logical plugin. +#[derive(Clone, PartialEq, Eq)] +pub struct PluginCatalogEntry { + pub id: PluginIdentity, + pub display_name: String, + pub version: Option, + /// Serialized `.mcp.json` server declarations keyed by their authored names. + pub mcp_servers: BTreeMap, + pub connector_ids: Vec, + pub locations: Vec, +} + +impl std::fmt::Debug for PluginCatalogEntry { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("PluginCatalogEntry") + .field("id", &self.id) + .field("display_name", &self.display_name) + .field("version", &self.version) + // Declaration values can contain secrets, so only log their keys. + .field( + "mcp_server_names", + &self.mcp_servers.keys().collect::>(), + ) + .field("connector_ids", &self.connector_ids) + .field("locations", &self.locations) + .finish() + } +} + +/// Stable key used to merge discoveries of the same logical plugin. +/// Identity is independent of whether the plugin is supplied by the cloud or an executor. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PluginIdentity { + /// Stable ID assigned by plugin-service and shared by its materialized installations. + Remote { remote_plugin_id: String }, + /// Local `name@marketplace` config key for a plugin without a remote ID. + Local { plugin_id: String }, +} + +impl PluginIdentity { + pub fn as_str(&self) -> &str { + match self { + Self::Remote { remote_plugin_id } => remote_plugin_id, + Self::Local { plugin_id } => plugin_id, + } + } +} + +/// A source that can supply a plugin; cloud URIs are opaque, not local paths. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PluginSourceLocation { + Cloud { + resource_uri: String, + bundle_uri: Option, + }, + Executor { + /// Executor environment that owns `root`. + environment_id: String, + /// Local `name@marketplace` key used for this installation in configuration. + plugin_id: String, + root: PathUri, + }, +} diff --git a/codex-rs/plugin/src/lib.rs b/codex-rs/plugin/src/lib.rs index 0ba9375897..d981aef2b0 100644 --- a/codex-rs/plugin/src/lib.rs +++ b/codex-rs/plugin/src/lib.rs @@ -5,12 +5,17 @@ use std::collections::HashSet; pub use codex_utils_plugins::mention_syntax; mod bundled_hooks; +mod catalog; mod load_outcome; pub mod manifest; mod plugin_id; mod provider; pub use bundled_hooks::is_allowlisted_bundled_cleanup_hook; +pub use catalog::PluginCatalog; +pub use catalog::PluginCatalogEntry; +pub use catalog::PluginIdentity; +pub use catalog::PluginSourceLocation; use codex_config::HookEventsToml; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; @@ -21,6 +26,8 @@ pub use plugin_id::PluginId; pub use plugin_id::PluginIdError; pub use plugin_id::validate_plugin_segment; pub use provider::PluginProvider; +pub use provider::PluginProviderError; +pub use provider::PluginProviderFuture; pub use provider::PluginResourceLocator; pub use provider::ResolvedPlugin; pub use provider::ResolvedPluginError; diff --git a/codex-rs/plugin/src/provider.rs b/codex-rs/plugin/src/provider.rs index 6dd81db8a7..2cb325dec6 100644 --- a/codex-rs/plugin/src/provider.rs +++ b/codex-rs/plugin/src/provider.rs @@ -1,8 +1,12 @@ +//! Shared APIs for catalog discovery and root-at-a-time plugin resolution. + +use crate::PluginCatalog; use crate::manifest::PluginManifest; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_utils_path_uri::PathUri; use std::error::Error as StdError; use std::future::Future; +use std::pin::Pin; use thiserror::Error; /// A plugin resource paired with the environment that owns its filesystem. @@ -104,20 +108,34 @@ fn environment_resource( }) } -/// Resolves source-owned package roots into inert plugin descriptors. +/// Error returned when a provider cannot produce a complete catalog snapshot. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[error("{0}")] +pub struct PluginProviderError(pub String); + +pub type PluginProviderFuture<'a, T> = + Pin> + Send + 'a>>; + +/// Discovers plugin metadata through batch listing or single-root resolution. /// -/// Implementations must perform all filesystem access through the authority -/// named by the selected root. `None` means the root contains no plugin -/// manifest and may be handled as another standalone capability. -pub trait PluginProvider: Send + Sync { - /// Source-specific resolution failure. +/// Cloud providers implement `list`; executor providers continue implementing `resolve`. +/// Each contributor invokes its provider's supported operation; the other is a no-op. +pub trait PluginProvider: Send + Sync { + /// Error returned by root-at-a-time resolution. type Error: StdError + Send + Sync + 'static; - /// Resolves one selected root without activating any of its components. + /// Returns a complete snapshot. + fn list(&self, _query: ListQuery) -> PluginProviderFuture<'_, PluginCatalog> { + Box::pin(async { Ok(PluginCatalog::default()) }) + } + + /// Resolves one selected root using the filesystem authority named by that root. fn resolve( &self, - root: &SelectedCapabilityRoot, - ) -> impl Future, Self::Error>> + Send; + _root: &SelectedCapabilityRoot, + ) -> impl Future, Self::Error>> + Send { + async { Ok(None) } + } } #[cfg(test)]