diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index eb69591059..3a523ee7ae 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -206,7 +206,6 @@ use crate::mentions::collect_tool_mentions_from_messages; use crate::network_policy_decision::execpolicy_network_rule_amendment; use crate::plugins::PluginCapabilitySummary; use crate::plugins::PluginsManager; -use crate::plugins::annotate_tools_with_plugin_sources; use crate::plugins::render_explicit_plugin_instructions; use crate::project_doc::get_user_instructions; use crate::protocol::AgentMessageContentDeltaEvent; @@ -1528,7 +1527,7 @@ impl Session { tool_approvals: Mutex::new(ApprovalStore::default()), execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, - plugins_manager, + plugins_manager: Arc::clone(&plugins_manager), mcp_manager, file_watcher, agent_control, @@ -1613,6 +1612,10 @@ impl Session { .map(|(name, _)| name.clone()) .collect(); required_mcp_servers.sort(); + let plugin_capability_index = plugins_manager + .plugins_for_config(config.as_ref()) + .capability_index() + .clone(); { let mut cancel_guard = sess.services.mcp_startup_cancellation_token.lock().await; cancel_guard.cancel(); @@ -1627,6 +1630,7 @@ impl Session { sandbox_state, config.codex_home.clone(), codex_apps_tools_cache_key(auth), + plugin_capability_index, ) .await; { @@ -3572,6 +3576,12 @@ impl Session { ) { let auth = self.services.auth_manager.auth().await; let config = self.get_config().await; + let plugin_capability_index = self + .services + .plugins_manager + .plugins_for_config(config.as_ref()) + .capability_index() + .clone(); let mcp_servers = with_codex_apps_mcp( mcp_servers, self.features.enabled(Feature::Apps), @@ -3599,6 +3609,7 @@ impl Session { sandbox_state, config.codex_home.clone(), codex_apps_tools_cache_key(auth.as_ref()), + plugin_capability_index, ) .await; { @@ -5699,10 +5710,6 @@ async fn built_tools( .services .plugins_manager .plugins_for_config(&turn_context.config); - // Annotate tool descriptions at runtime so prompt-visible tools and - // search_tool_bm25 share the same plugin provenance without mutating the - // cached MCP tool inventory. - mcp_tools = annotate_tools_with_plugin_sources(mcp_tools, loaded_plugins.capability_index()); let mut effective_explicitly_enabled_connectors = explicitly_enabled_connectors.clone(); effective_explicitly_enabled_connectors.extend(sess.get_connector_selection().await); diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 3ad48fa008..7d7f6a05f8 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -31,6 +31,7 @@ use crate::mcp::with_codex_apps_mcp; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::AppConnectorId; +use crate::plugins::PluginCapabilityIndex; use crate::token_data::TokenData; pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600); @@ -162,6 +163,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( sandbox_state, config.codex_home.clone(), codex_apps_tools_cache_key(auth.as_ref()), + PluginCapabilityIndex::default(), ) .await; diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index ac7dd2acaf..7ade2001df 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -217,7 +217,12 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent config.cli_auth_credentials_store_mode, ); let auth = auth_manager.auth().await; - let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone()))); + let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.clone())); + let plugin_capability_index = plugins_manager + .plugins_for_config(config) + .capability_index() + .clone(); + let mcp_manager = McpManager::new(plugins_manager); let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()); if mcp_servers.is_empty() { return McpListToolsResponseEvent { @@ -251,6 +256,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent sandbox_state, config.codex_home.clone(), codex_apps_tools_cache_key(auth.as_ref()), + plugin_capability_index, ) .await; diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 2d22351d1f..1fe7b15d2e 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -6,6 +6,7 @@ //! in a single aggregated map using the fully-qualified tool name //! `""` as the key. +use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; use std::env; @@ -79,6 +80,8 @@ use crate::codex::INITIAL_SUBMIT_ID; use crate::config::types::McpServerConfig; use crate::config::types::McpServerTransportConfig; use crate::connectors::is_connector_id_allowed; +use crate::plugins::AppConnectorId; +use crate::plugins::PluginCapabilityIndex; /// Delimiter used to separate the server name from the tool name in a fully /// qualified tool name. @@ -341,6 +344,7 @@ struct ManagedClient { tool_timeout: Option, server_supports_sandbox_state_capability: bool, codex_apps_tools_cache_context: Option, + plugin_capability_index: Arc, } impl ManagedClient { @@ -350,12 +354,17 @@ impl ManagedClient { && let CachedCodexAppsToolsLoad::Hit(tools) = load_cached_codex_apps_tools(cache_context) { + // Keep the disk cache raw because plugin provenance is session/config-scoped, + // while the codex_apps cache is only keyed per user account. emit_duration( MCP_TOOLS_LIST_DURATION_METRIC, total_start.elapsed(), &[("cache", "hit")], ); - return filter_tools(tools, &self.tool_filter); + return annotate_tools_with_plugin_sources( + filter_tools(tools, &self.tool_filter), + self.plugin_capability_index.as_ref(), + ); } if self.codex_apps_tools_cache_context.is_some() { @@ -394,6 +403,9 @@ struct AsyncManagedClient { } impl AsyncManagedClient { + // Keep this constructor flat so the startup inputs remain readable at the + // single call site instead of introducing a one-off params wrapper. + #[allow(clippy::too_many_arguments)] fn new( server_name: String, config: McpServerConfig, @@ -402,16 +414,23 @@ impl AsyncManagedClient { tx_event: Sender, elicitation_requests: ElicitationRequestManager, codex_apps_tools_cache_context: Option, + plugin_capability_index: Arc, ) -> Self { let tool_filter = ToolFilter::from_config(&config); let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot( &server_name, codex_apps_tools_cache_context.as_ref(), ) - .map(|tools| filter_tools(tools, &tool_filter)); + .map(|tools| { + annotate_tools_with_plugin_sources( + filter_tools(tools, &tool_filter), + plugin_capability_index.as_ref(), + ) + }); let startup_tool_filter = tool_filter; let startup_complete = Arc::new(AtomicBool::new(false)); let startup_complete_for_fut = Arc::clone(&startup_complete); + let startup_plugin_capability_index = Arc::clone(&plugin_capability_index); let fut = async move { let outcome = async { if let Err(error) = validate_mcp_server_name(&server_name) { @@ -432,6 +451,7 @@ impl AsyncManagedClient { tx_event, elicitation_requests, codex_apps_tools_cache_context, + plugin_capability_index: startup_plugin_capability_index, }, ) .or_cancel(&cancel_token) @@ -552,12 +572,14 @@ impl McpConnectionManager { initial_sandbox_state: SandboxState, codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, + plugin_capability_index: PluginCapabilityIndex, ) -> (Self, CancellationToken) { let cancel_token = CancellationToken::new(); let mut clients = HashMap::new(); let mut server_origins = HashMap::new(); let mut join_set = JoinSet::new(); let elicitation_requests = ElicitationRequestManager::new(approval_policy.value()); + let plugin_capability_index = Arc::new(plugin_capability_index); let mcp_servers = mcp_servers.clone(); for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) { if let Some(origin) = transport_origin(&cfg.transport) { @@ -588,6 +610,7 @@ impl McpConnectionManager { tx_event.clone(), elicitation_requests.clone(), codex_apps_tools_cache_context, + Arc::clone(&plugin_capability_index), ); clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); @@ -1135,6 +1158,63 @@ pub(crate) fn filter_mcp_tools_by_name( .collect() } +fn annotate_tools_with_plugin_sources( + mut tools: Vec, + capability_index: &PluginCapabilityIndex, +) -> Vec { + for tool in &mut tools { + let mut plugin_names = match tool.connector_id.as_ref() { + Some(connector_id) => capability_index + .plugins_for_connector_id(&AppConnectorId(connector_id.clone())) + .into_iter() + .map(|plugin| plugin.display_name.as_str()) + .collect::>(), + None => capability_index + .plugins_for_mcp_server_name(tool.server_name.as_str()) + .into_iter() + .map(|plugin| plugin.display_name.as_str()) + .collect::>(), + }; + + if plugin_names.is_empty() { + continue; + } + + plugin_names.sort_unstable(); + plugin_names.dedup(); + + let plugin_source_note = if plugin_names.len() == 1 { + format!("This tool is part of plugin `{}`.", plugin_names[0]) + } else { + format!( + "This tool is part of plugins {}.", + plugin_names + .into_iter() + .map(|plugin_name| format!("`{plugin_name}`")) + .collect::>() + .join(", ") + ) + }; + + let description = tool + .tool + .description + .as_deref() + .map(str::trim) + .unwrap_or(""); + let annotated_description = if description.is_empty() { + plugin_source_note + } else if matches!(description.chars().last(), Some('.' | '!' | '?')) { + format!("{description} {plugin_source_note}") + } else { + format!("{description}. {plugin_source_note}") + }; + tool.tool.description = Some(Cow::Owned(annotated_description)); + } + + tools +} + fn normalize_codex_apps_tool_title( server_name: &str, connector_name: Option<&str>, @@ -1233,6 +1313,7 @@ async fn start_server_task( tx_event, elicitation_requests, codex_apps_tools_cache_context, + plugin_capability_index, } = params; let elicitation = elicitation_capability_for_server(&server_name); let params = InitializeRequestParams { @@ -1285,7 +1366,10 @@ async fn start_server_task( &[("cache", "miss")], ); } - let tools = filter_tools(tools, &tool_filter); + let tools = annotate_tools_with_plugin_sources( + filter_tools(tools, &tool_filter), + plugin_capability_index.as_ref(), + ); let server_supports_sandbox_state_capability = initialize_result .capabilities @@ -1301,6 +1385,7 @@ async fn start_server_task( tool_filter, server_supports_sandbox_state_capability, codex_apps_tools_cache_context, + plugin_capability_index, }; Ok(managed) @@ -1313,6 +1398,7 @@ struct StartServerTaskParams { tx_event: Sender, elicitation_requests: ElicitationRequestManager, codex_apps_tools_cache_context: Option, + plugin_capability_index: Arc, } async fn make_rmcp_client( diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index fb290c9eb1..f9b75d392e 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -17,7 +17,6 @@ use crate::config_loader::ConfigLayerStack; use crate::features::Feature; use crate::features::FeatureOverrides; use crate::features::Features; -use crate::mcp_connection_manager::ToolInfo; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::MergeStrategy; use codex_utils_absolute_path::AbsolutePathBuf; @@ -25,7 +24,6 @@ use serde::Deserialize; use serde_json::Map as JsonMap; use serde_json::Value as JsonValue; use serde_json::json; -use std::borrow::Cow; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -97,7 +95,6 @@ impl PluginCapabilitySummary { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PluginCapabilityIndex { plugins: Vec, - plugin_index_by_config_name: HashMap, plugin_indexes_by_connector_id: HashMap>, plugin_indexes_by_mcp_server_name: HashMap>, } @@ -112,9 +109,6 @@ impl PluginCapabilityIndex { }; let plugin_index = capability_index.plugins.len(); - capability_index - .plugin_index_by_config_name - .insert(summary.config_name.clone(), plugin_index); for connector_id in &summary.app_connector_ids { capability_index @@ -142,12 +136,6 @@ impl PluginCapabilityIndex { &self.plugins } - pub fn plugin_for_config_name(&self, config_name: &str) -> Option<&PluginCapabilitySummary> { - self.plugin_index_by_config_name - .get(config_name) - .map(|plugin_index| &self.plugins[*plugin_index]) - } - pub fn plugins_for_connector_id( &self, connector_id: &AppConnectorId, @@ -170,66 +158,6 @@ impl PluginCapabilityIndex { } } -pub(crate) fn annotate_tools_with_plugin_sources( - mut tools: HashMap, - capability_index: &PluginCapabilityIndex, -) -> HashMap { - for tool in tools.values_mut() { - // Recover plugin provenance from the app connector or MCP server and - // append it to the shared description text used by both prompt tools - // and search_tool_bm25 indexing. - let mut plugin_names = match tool.connector_id.as_ref() { - Some(connector_id) => capability_index - .plugins_for_connector_id(&AppConnectorId(connector_id.clone())) - .into_iter() - .map(|plugin| plugin.display_name.as_str()) - .collect::>(), - None => capability_index - .plugins_for_mcp_server_name(tool.server_name.as_str()) - .into_iter() - .map(|plugin| plugin.display_name.as_str()) - .collect::>(), - }; - - if plugin_names.is_empty() { - continue; - } - - plugin_names.sort_unstable(); - plugin_names.dedup(); - - let plugin_source_note = if plugin_names.len() == 1 { - format!("This tool is part of plugin `{}`.", plugin_names[0]) - } else { - format!( - "This tool is part of plugins {}.", - plugin_names - .into_iter() - .map(|plugin_name| format!("`{plugin_name}`")) - .collect::>() - .join(", ") - ) - }; - - let description = tool - .tool - .description - .as_deref() - .map(str::trim) - .unwrap_or(""); - let annotated_description = if description.is_empty() { - plugin_source_note - } else if matches!(description.chars().last(), Some('.' | '!' | '?')) { - format!("{description} {plugin_source_note}") - } else { - format!("{description}. {plugin_source_note}") - }; - tool.tool.description = Some(Cow::Owned(annotated_description)); - } - - tools -} - #[derive(Debug, Clone, PartialEq)] pub struct PluginLoadOutcome { plugins: Vec, @@ -1112,8 +1040,11 @@ mod tests { assert_eq!( outcome .capability_index() - .plugin_for_config_name("empty@test"), - None + .plugins() + .iter() + .map(|plugin| plugin.config_name.as_str()) + .collect::>(), + vec!["skills@test", "alpha@test", "beta@test"] ); } diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 6f3a8a7e4c..a8e5183bbf 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -11,7 +11,6 @@ pub use manager::PluginInstallError; pub use manager::PluginInstallRequest; pub use manager::PluginLoadOutcome; pub use manager::PluginsManager; -pub(crate) use manager::annotate_tools_with_plugin_sources; pub(crate) use manager::plugin_namespace_for_skill_path; pub(crate) use manifest::load_plugin_manifest; pub(crate) use manifest::plugin_manifest_name; diff --git a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs index ef88d2fdd2..a7576d5042 100644 --- a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs +++ b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs @@ -13,7 +13,6 @@ use crate::connectors; use crate::function_tool::FunctionCallError; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp_connection_manager::ToolInfo; -use crate::plugins::annotate_tools_with_plugin_sources; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; @@ -126,15 +125,6 @@ impl ToolHandler for SearchToolBm25Handler { ); let mcp_tools = filter_codex_apps_mcp_tools(mcp_tools, &connectors); let mcp_tools = connectors::filter_codex_apps_tools_by_policy(mcp_tools, &turn.config); - let loaded_plugins = session - .services - .plugins_manager - .plugins_for_config(&turn.config); - // Reuse the same runtime annotation as prompt tool building so plugin - // names become searchable through normal description indexing. - let mcp_tools = - annotate_tools_with_plugin_sources(mcp_tools, loaded_plugins.capability_index()); - let mut entries: Vec = mcp_tools .into_iter() .map(|(name, info)| ToolEntry::new(name, info))