Files
codex/codex-rs/connectors/src/snapshot.rs
T
jif 4e0f863df3 Add a connector declaration snapshot (#29851)
## Why

Connector declarations currently enter Codex through broad plugin
capability summaries, then MCP setup, turn tooling, and `app/list` each
reconstruct the same information. That makes executor-selected
connectors difficult to add without coupling connector behavior to the
host plugin loader.

This PR introduces a small connector-owned value that later stack layers
can populate before thread startup.

## What changed

- Move the pure app-declaration parser into `codex-connectors`,
preserving declaration order and category cleanup while leaving
host-side validation and deduplication unchanged.
- Add an immutable `ConnectorSnapshot` with ordered connector IDs and
plugin display-name provenance.
- Adapt the existing local-plugin capability summaries into that
snapshot at current consumer boundaries.
- Use the snapshot for MCP tool provenance, turn connector inventory,
and `app/list`.
- Keep the crate API narrow: no test-only snapshot accessors are
exposed.

The externally visible behavior is unchanged. Connector tools still come
from the orchestrator-owned `/ps/mcp` server, and local plugin
enablement remains owned by the existing plugin loader.

## Stack scope

This is the foundation only. It does not read selected executor packages
or change thread startup. #29852 adds the executor-backed declaration
reader, and #29856 composes selected declarations into a thread
snapshot.
2026-06-24 23:24:01 +01:00

137 lines
4.7 KiB
Rust

use std::collections::HashMap;
use std::collections::HashSet;
use codex_plugin::AppConnectorId;
use codex_plugin::AppDeclaration;
use codex_plugin::PluginCapabilitySummary;
/// Connector declarations contributed by one plugin package.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginConnectorSource {
plugin_id: String,
plugin_display_name: String,
connector_ids: Vec<AppConnectorId>,
}
impl PluginConnectorSource {
/// Creates one plugin source from parsed app declarations.
pub fn new(
plugin_id: impl Into<String>,
plugin_display_name: impl Into<String>,
declarations: impl IntoIterator<Item = AppDeclaration>,
) -> Self {
Self::from_connector_ids(
plugin_id,
plugin_display_name,
declarations
.into_iter()
.map(|declaration| declaration.connector_id),
)
}
/// Creates one plugin source from connector IDs that were already parsed.
pub fn from_connector_ids(
plugin_id: impl Into<String>,
plugin_display_name: impl Into<String>,
connector_ids: impl IntoIterator<Item = AppConnectorId>,
) -> Self {
let mut seen_connector_ids = HashSet::new();
let connector_ids = connector_ids
.into_iter()
.filter(|connector_id| !connector_id.0.trim().is_empty())
.filter(|connector_id| seen_connector_ids.insert(connector_id.clone()))
.collect();
Self {
plugin_id: plugin_id.into(),
plugin_display_name: plugin_display_name.into(),
connector_ids,
}
}
/// Returns the package name shown in connector provenance.
pub fn plugin_display_name(&self) -> &str {
&self.plugin_display_name
}
/// Returns the connector IDs contributed by this package.
pub fn connector_ids(&self) -> &[AppConnectorId] {
&self.connector_ids
}
}
/// Immutable connector declarations and their plugin provenance.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ConnectorSnapshot {
sources: Vec<PluginConnectorSource>,
connector_ids: Vec<AppConnectorId>,
plugin_display_names_by_connector_id: HashMap<String, Vec<String>>,
}
impl ConnectorSnapshot {
/// Builds a connector snapshot from package-scoped declarations.
pub fn from_plugin_sources(sources: impl IntoIterator<Item = PluginConnectorSource>) -> Self {
let sources = sources
.into_iter()
.filter(|source| !source.connector_ids().is_empty())
.collect::<Vec<_>>();
let mut connector_ids = Vec::new();
let mut seen_connector_ids = HashSet::new();
let mut plugin_display_names_by_connector_id: HashMap<String, Vec<String>> = HashMap::new();
for source in &sources {
for connector_id in source.connector_ids() {
if seen_connector_ids.insert(connector_id.clone()) {
connector_ids.push(connector_id.clone());
}
plugin_display_names_by_connector_id
.entry(connector_id.0.clone())
.or_default()
.push(source.plugin_display_name().to_string());
}
}
for plugin_names in plugin_display_names_by_connector_id.values_mut() {
plugin_names.sort_unstable();
plugin_names.dedup();
}
Self {
sources,
connector_ids,
plugin_display_names_by_connector_id,
}
}
/// Adapts the current host plugin summaries to the connector-owned snapshot.
pub fn from_plugin_capability_summaries(summaries: &[PluginCapabilitySummary]) -> Self {
Self::from_plugin_sources(summaries.iter().map(|summary| {
PluginConnectorSource::from_connector_ids(
summary.config_name.clone(),
summary.display_name.clone(),
summary.app_connector_ids.clone(),
)
}))
}
/// Returns the connector IDs in source contribution order.
pub fn connector_ids(&self) -> &[AppConnectorId] {
&self.connector_ids
}
/// Returns the package display names associated with one connector.
pub fn plugin_display_names_for_connector_id(&self, connector_id: &str) -> &[String] {
self.plugin_display_names_by_connector_id
.get(connector_id)
.map(Vec::as_slice)
.unwrap_or_default()
}
/// Combines two snapshots while preserving source order and provenance.
pub fn merged_with(&self, other: &Self) -> Self {
Self::from_plugin_sources(self.sources.iter().chain(&other.sources).cloned())
}
}
#[cfg(test)]
#[path = "snapshot_tests.rs"]
mod tests;