mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
Generated
+4
-1
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user