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
This commit is contained in:
Anthony Tafoya
2026-09-18 22:17:32 +00:00
committed by copyberry
parent cd2f9ca692
commit 46d87f3b1c
5 changed files with 131 additions and 9 deletions

View File

@@ -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;

View File

@@ -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<Arc<McpResourceClient>>,
}

View File

@@ -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<PluginCatalogEntry>,
pub warnings: Vec<String>,
}
/// 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<String>,
/// Serialized `.mcp.json` server declarations keyed by their authored names.
pub mcp_servers: BTreeMap<String, String>,
pub connector_ids: Vec<String>,
pub locations: Vec<PluginSourceLocation>,
}
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::<Vec<_>>(),
)
.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<String>,
},
Executor {
/// Executor environment that owns `root`.
environment_id: String,
/// Local `name@marketplace` key used for this installation in configuration.
plugin_id: String,
root: PathUri,
},
}

View File

@@ -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;

View File

@@ -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<Box<dyn Future<Output = Result<T, PluginProviderError>> + 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<ListQuery = ()>: 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<Output = Result<Option<ResolvedPlugin>, Self::Error>> + Send;
_root: &SelectedCapabilityRoot,
) -> impl Future<Output = Result<Option<ResolvedPlugin>, Self::Error>> + Send {
async { Ok(None) }
}
}
#[cfg(test)]