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.
This commit is contained in:
jif
2026-06-24 23:24:01 +01:00
committed by GitHub
Unverified
parent bb05c1f30f
commit 4e0f863df3
15 changed files with 390 additions and 110 deletions
+4 -1
View File
@@ -2584,6 +2584,8 @@ version = "0.0.0"
dependencies = [
"anyhow",
"codex-config",
"codex-plugin",
"indexmap 2.14.0",
"pretty_assertions",
"serde",
"serde_json",
@@ -2756,6 +2758,7 @@ dependencies = [
"codex-analytics",
"codex-app-server-protocol",
"codex-config",
"codex-connectors",
"codex-core-skills",
"codex-exec-server",
"codex-git-utils",
@@ -2773,7 +2776,6 @@ dependencies = [
"codex-utils-plugins",
"dirs",
"flate2",
"indexmap 2.14.0",
"libc",
"pretty_assertions",
"regex",
@@ -3312,6 +3314,7 @@ dependencies = [
"codex-api",
"codex-async-utils",
"codex-config",
"codex-connectors",
"codex-exec-server",
"codex-login",
"codex-model-provider",
@@ -183,10 +183,14 @@ impl AppsRequestProcessor {
None => 0,
};
let plugin_apps = plugins_manager
let loaded_plugins = plugins_manager
.plugins_for_config(&config.plugins_config_input())
.await
.effective_apps();
.await;
let connector_snapshot =
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
);
let plugin_apps = connector_snapshot.connector_ids().to_vec();
let (mut accessible_connectors, mut all_connectors) = tokio::join!(
connectors::list_cached_accessible_connectors_from_mcp_tools(&config),
connectors::list_cached_all_connectors(&config, &plugin_apps)
+2 -1
View File
@@ -19,11 +19,11 @@ async-channel = { workspace = true }
codex-async-utils = { workspace = true }
codex-api = { workspace = true }
codex-config = { workspace = true }
codex-connectors = { workspace = true }
codex-exec-server = { workspace = true }
codex-login = { workspace = true }
codex-model-provider = { workspace = true }
codex-otel = { workspace = true }
codex-plugin = { workspace = true }
codex-protocol = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-utils-path-uri = { workspace = true }
@@ -41,6 +41,7 @@ tracing = { workspace = true }
url = { workspace = true }
[dev-dependencies]
codex-plugin = { workspace = true }
pretty_assertions = { workspace = true }
rmcp = { workspace = true, default-features = false, features = ["base64", "macros", "schemars", "server"] }
tempfile = { workspace = true }
+13 -11
View File
@@ -24,8 +24,8 @@ use codex_config::McpServerTransportConfig;
use codex_config::types::AppToolApproval;
use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_connectors::ConnectorSnapshot;
use codex_login::CodexAuth;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate;
@@ -142,9 +142,9 @@ pub struct McpConfig {
pub client_elicitation_capability: ElicitationCapability,
/// Resolved MCP registrations keyed by logical server name.
pub mcp_server_catalog: ResolvedMcpCatalog,
/// Plugin metadata used to attribute connector tools to plugin display names.
/// Plugin declarations used to attribute connector tools to plugin display names.
/// MCP registrations retain their own package attribution in the catalog.
pub plugin_capability_summaries: Vec<PluginCapabilitySummary>,
pub connector_snapshot: ConnectorSnapshot,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
@@ -182,14 +182,16 @@ impl ToolPluginProvenance {
fn from_config(config: &McpConfig) -> Self {
let mut tool_plugin_provenance = Self::default();
for plugin in &config.plugin_capability_summaries {
for connector_id in &plugin.app_connector_ids {
tool_plugin_provenance
.plugin_display_names_by_connector_id
.entry(connector_id.0.clone())
.or_default()
.push(plugin.display_name.clone());
}
for connector_id in config.connector_snapshot.connector_ids() {
tool_plugin_provenance
.plugin_display_names_by_connector_id
.insert(
connector_id.0.clone(),
config
.connector_snapshot
.plugin_display_names_for_connector_id(&connector_id.0)
.to_vec(),
);
}
for (server_name, attribution) in config
+30 -26
View File
@@ -34,7 +34,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
prefix_mcp_tool_names: true,
client_elicitation_capability: ElicitationCapability::default(),
mcp_server_catalog: ResolvedMcpCatalog::default(),
plugin_capability_summaries: Vec::new(),
connector_snapshot: codex_connectors::ConnectorSnapshot::default(),
}
}
@@ -133,25 +133,26 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
codex_apps_mcp_server_config("https://alpha.example", /*apps_mcp_product_sku*/ None),
));
config.mcp_server_catalog = catalog.build();
config.plugin_capability_summaries = vec![
PluginCapabilitySummary {
config_name: "alpha@test".to_string(),
display_name: "alpha-plugin".to_string(),
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
mcp_server_names: vec!["alpha".to_string()],
..PluginCapabilitySummary::default()
},
PluginCapabilitySummary {
config_name: "beta@test".to_string(),
display_name: "beta-plugin".to_string(),
app_connector_ids: vec![
AppConnectorId("connector_example".to_string()),
AppConnectorId("connector_gmail".to_string()),
],
mcp_server_names: vec!["beta".to_string()],
..PluginCapabilitySummary::default()
},
];
config.connector_snapshot =
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(&[
PluginCapabilitySummary {
config_name: "alpha@test".to_string(),
display_name: "alpha-plugin".to_string(),
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
mcp_server_names: vec!["alpha".to_string()],
..PluginCapabilitySummary::default()
},
PluginCapabilitySummary {
config_name: "beta@test".to_string(),
display_name: "beta-plugin".to_string(),
app_connector_ids: vec![
AppConnectorId("connector_example".to_string()),
AppConnectorId("connector_gmail".to_string()),
],
mcp_server_names: vec!["beta".to_string()],
..PluginCapabilitySummary::default()
},
]);
let provenance = tool_plugin_provenance(&config);
assert_eq!(
@@ -199,12 +200,15 @@ fn selected_mcp_attribution_does_not_join_an_unrelated_local_summary() {
codex_apps_mcp_server_config("https://github.example", /*apps_mcp_product_sku*/ None),
));
config.mcp_server_catalog = catalog.build();
config.plugin_capability_summaries = vec![PluginCapabilitySummary {
config_name: "shared-plugin-id".to_string(),
display_name: "Local GitHub".to_string(),
mcp_server_names: vec!["github".to_string()],
..PluginCapabilitySummary::default()
}];
config.connector_snapshot =
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(&[
PluginCapabilitySummary {
config_name: "shared-plugin-id".to_string(),
display_name: "Local GitHub".to_string(),
mcp_server_names: vec!["github".to_string()],
..PluginCapabilitySummary::default()
},
]);
let provenance = tool_plugin_provenance(&config);
+2
View File
@@ -10,6 +10,8 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
codex-config = { workspace = true }
codex-plugin = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha1 = { workspace = true }
+6
View File
@@ -15,6 +15,8 @@ mod directory_cache;
pub mod filter;
pub mod merge;
pub mod metadata;
mod plugin_config;
mod snapshot;
pub use app_info::AppBranding;
pub use app_info::AppInfo;
@@ -27,6 +29,10 @@ pub use app_tool_policy::AppToolPolicyInput;
pub use app_tool_policy::app_is_enabled;
pub use app_tool_policy::apps_config_from_layer_stack;
pub use directory_cache::ConnectorDirectoryCacheContext;
pub use plugin_config::parse_plugin_app_config;
pub use plugin_config::parse_plugin_app_config_value;
pub use snapshot::ConnectorSnapshot;
pub use snapshot::PluginConnectorSource;
pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600);
+50
View File
@@ -0,0 +1,50 @@
use codex_plugin::AppConnectorId;
use codex_plugin::AppDeclaration;
use indexmap::IndexMap;
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginAppFile {
#[serde(default)]
apps: IndexMap<String, PluginAppConfig>,
}
#[derive(Debug, Default, Deserialize)]
struct PluginAppConfig {
id: String,
category: Option<String>,
}
/// Parses connector declarations from a plugin app configuration file.
pub fn parse_plugin_app_config(contents: &str) -> serde_json::Result<Vec<AppDeclaration>> {
serde_json::from_str(contents).map(app_declarations_from_file)
}
/// Parses connector declarations from an already-decoded plugin app configuration.
pub fn parse_plugin_app_config_value(value: Value) -> serde_json::Result<Vec<AppDeclaration>> {
serde_json::from_value(value).map(app_declarations_from_file)
}
fn app_declarations_from_file(parsed: PluginAppFile) -> Vec<AppDeclaration> {
parsed
.apps
.into_iter()
.map(|(name, app)| AppDeclaration {
name,
connector_id: AppConnectorId(app.id),
category: cleaned_category(app.category),
})
.collect()
}
fn cleaned_category(category: Option<String>) -> Option<String> {
category
.map(|category| category.trim().to_string())
.filter(|category| !category.is_empty())
}
#[cfg(test)]
#[path = "plugin_config_tests.rs"]
mod tests;
@@ -0,0 +1,53 @@
use codex_plugin::AppConnectorId;
use codex_plugin::AppDeclaration;
use pretty_assertions::assert_eq;
use super::parse_plugin_app_config;
#[test]
fn parses_plugin_app_config_in_order_without_validating_connector_ids() {
let parsed = parse_plugin_app_config(
r#"{
"apps": {
"calendar": {
"id": "connector_calendar",
"category": " productivity "
},
"drive": {
"id": "connector_calendar",
"category": " "
},
"blank": {
"id": " "
}
}
}"#,
)
.expect("plugin app config should parse");
assert_eq!(
parsed,
vec![
AppDeclaration {
name: "calendar".to_string(),
connector_id: AppConnectorId("connector_calendar".to_string()),
category: Some("productivity".to_string()),
},
AppDeclaration {
name: "drive".to_string(),
connector_id: AppConnectorId("connector_calendar".to_string()),
category: None,
},
AppDeclaration {
name: "blank".to_string(),
connector_id: AppConnectorId(" ".to_string()),
category: None,
},
]
);
}
#[test]
fn rejects_invalid_plugin_app_config() {
assert!(parse_plugin_app_config("not json").is_err());
}
+136
View File
@@ -0,0 +1,136 @@
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;
+47
View File
@@ -0,0 +1,47 @@
use codex_plugin::AppConnectorId;
use pretty_assertions::assert_eq;
use super::ConnectorSnapshot;
use super::PluginConnectorSource;
#[test]
fn snapshot_merges_sources_in_order_and_dedupes_provenance() {
let host_source = source("host", "Zulu", &["calendar", "calendar"]);
let host = ConnectorSnapshot::from_plugin_sources([
source("skills", "Skills only", &[]),
host_source.clone(),
]);
let selected = ConnectorSnapshot::from_plugin_sources([
source("selected-a", "Alpha", &["drive", "calendar"]),
source("selected-b", "Alpha", &["calendar"]),
]);
let merged = host.merged_with(&selected);
assert_eq!(host.sources, vec![host_source]);
assert_eq!(
merged.connector_ids(),
&[
AppConnectorId("calendar".to_string()),
AppConnectorId("drive".to_string()),
]
);
assert_eq!(
merged.plugin_display_names_for_connector_id("calendar"),
&["Alpha".to_string(), "Zulu".to_string()]
);
assert_eq!(
merged.plugin_display_names_for_connector_id("missing"),
&[] as &[String]
);
}
fn source(id: &str, display_name: &str, connector_ids: &[&str]) -> PluginConnectorSource {
PluginConnectorSource::from_connector_ids(
id,
display_name,
connector_ids
.iter()
.map(|id| AppConnectorId((*id).to_string())),
)
}
+1 -1
View File
@@ -17,6 +17,7 @@ anyhow = { workspace = true }
codex-analytics = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-config = { workspace = true }
codex-connectors = { workspace = true }
codex-core-skills = { workspace = true }
codex-exec-server = { workspace = true }
codex-git-utils = { workspace = true }
@@ -35,7 +36,6 @@ codex-utils-plugins = { workspace = true }
chrono = { workspace = true }
dirs = { workspace = true }
flate2 = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
reqwest = { workspace = true }
regex = { workspace = true }
semver = { workspace = true }
+17 -54
View File
@@ -20,6 +20,8 @@ use codex_config::HooksFile;
use codex_config::types::McpServerConfig;
use codex_config::types::PluginConfig;
use codex_config::types::PluginMcpServerConfig;
use codex_connectors::parse_plugin_app_config;
use codex_connectors::parse_plugin_app_config_value;
use codex_core_skills::PluginSkillSnapshots;
use codex_core_skills::SkillMetadata;
use codex_core_skills::config_rules::SkillConfigRules;
@@ -29,7 +31,6 @@ use codex_core_skills::loader::SkillRoot;
use codex_core_skills::loader::load_skills_from_roots;
use codex_exec_server::LOCAL_FS;
use codex_mcp::parse_plugin_mcp_config;
use codex_plugin::AppConnectorId;
use codex_plugin::AppDeclaration;
use codex_plugin::LoadedPlugin;
use codex_plugin::PluginCapabilitySummary;
@@ -42,8 +43,6 @@ use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_plugins::find_plugin_manifest_path;
use indexmap::IndexMap;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -96,19 +95,6 @@ pub(crate) fn log_plugin_load_errors(plugins: &[LoadedPlugin<McpServerConfig>])
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginAppFile {
#[serde(default)]
apps: IndexMap<String, PluginAppConfig>,
}
#[derive(Debug, Default, Deserialize)]
struct PluginAppConfig {
id: String,
category: Option<String>,
}
/// Load configured plugins without applying auth-dependent runtime policies.
#[instrument(level = "trace", skip_all)]
pub(crate) async fn load_plugins_from_layer_stack(
@@ -965,10 +951,10 @@ pub(crate) async fn load_plugin_apps_from_manifest(
}
pub fn plugin_app_declarations_from_value(value: &JsonValue) -> Vec<AppDeclaration> {
let Ok(parsed) = serde_json::from_value::<PluginAppFile>(value.clone()) else {
let Ok(mut apps) = parse_plugin_app_config_value(value.clone()) else {
return Vec::new();
};
let mut apps = app_declarations_from_file(parsed, /*plugin_root*/ None);
apps.retain(|app| !app.connector_id.0.trim().is_empty());
let mut seen_connector_ids = HashSet::new();
apps.retain(|app| seen_connector_ids.insert(app.connector_id.0.clone()));
apps
@@ -1116,8 +1102,8 @@ async fn load_apps_from_paths(
let Ok(contents) = tokio::fs::read_to_string(app_config_path.as_path()).await else {
continue;
};
let parsed = match serde_json::from_str::<PluginAppFile>(&contents) {
Ok(parsed) => parsed,
let declarations = match parse_plugin_app_config(&contents) {
Ok(declarations) => declarations,
Err(err) => {
warn!(
path = %app_config_path.display(),
@@ -1127,44 +1113,21 @@ async fn load_apps_from_paths(
}
};
app_declarations.extend(app_declarations_from_file(parsed, Some(plugin_root)));
app_declarations.extend(declarations.into_iter().filter(|app| {
if app.connector_id.0.trim().is_empty() {
warn!(
plugin = %plugin_root.display(),
"plugin app config is missing an app id"
);
false
} else {
true
}
}));
}
app_declarations
}
fn app_declarations_from_file(
parsed: PluginAppFile,
plugin_root: Option<&Path>,
) -> Vec<AppDeclaration> {
parsed
.apps
.into_iter()
.filter_map(|(name, app)| {
if app.id.trim().is_empty() {
if let Some(plugin_root) = plugin_root {
warn!(
plugin = %plugin_root.display(),
"plugin app config is missing an app id"
);
}
None
} else {
Some(AppDeclaration {
name,
connector_id: AppConnectorId(app.id),
category: cleaned_app_category(app.category),
})
}
})
.collect()
}
fn cleaned_app_category(category: Option<String>) -> Option<String> {
category
.map(|category| category.trim().to_string())
.filter(|category| !category.is_empty())
}
pub async fn plugin_capability_summary_from_root(
plugin_id: &PluginId,
plugin_root: &AbsolutePathBuf,
+4 -1
View File
@@ -1578,7 +1578,10 @@ impl Config {
ElicitationCapability::default()
},
mcp_server_catalog: catalog.build(),
plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(),
connector_snapshot:
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
),
}
}
+18 -12
View File
@@ -532,6 +532,9 @@ async fn build_skills_and_plugins(
// enabled plugins, then converted into turn-scoped guidance below.
let mentioned_plugins =
collect_explicit_plugin_mentions(&user_input, loaded_plugins.capability_summaries());
let connector_snapshot = codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
);
let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() {
// Plugin mentions need raw MCP/app inventory even when app tools
// are normally hidden so we can describe the plugin's currently
@@ -553,10 +556,10 @@ async fn build_skills_and_plugins(
};
let available_connectors = if turn_context.apps_enabled() {
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
loaded_plugins
.effective_apps()
.into_iter()
.map(|connector_id| connector_id.0),
connector_snapshot
.connector_ids()
.iter()
.map(|connector_id| connector_id.0.clone()),
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
);
connectors::with_app_enabled_state(connectors, &turn_context.config)
@@ -1177,6 +1180,9 @@ pub(crate) async fn built_tools(
.plugins_for_config(&turn_context.config.plugins_config_input())
.instrument(trace_span!("built_tools.load_plugins"))
.await;
let connector_snapshot = codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
);
let apps_enabled = turn_context.apps_enabled();
let accessible_connectors =
@@ -1187,10 +1193,10 @@ pub(crate) async fn built_tools(
});
let connectors = if apps_enabled {
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
loaded_plugins
.effective_apps()
.into_iter()
.map(|connector_id| connector_id.0),
connector_snapshot
.connector_ids()
.iter()
.map(|connector_id| connector_id.0.clone()),
accessible_connectors.clone().unwrap_or_default(),
);
Some(connectors::with_app_enabled_state(
@@ -1228,10 +1234,10 @@ pub(crate) async fn built_tools(
presentation: ToolSuggestPresentation::RecommendationContext,
})
} else {
let loaded_plugin_app_connector_ids = loaded_plugins
.effective_apps()
.into_iter()
.map(|connector_id| connector_id.0)
let loaded_plugin_app_connector_ids = connector_snapshot
.connector_ids()
.iter()
.map(|connector_id| connector_id.0.clone())
.collect::<Vec<_>>();
async {
if apps_enabled && tool_suggest_is_enabled {