update tool descs with plugins at tool-build time

This commit is contained in:
Sayan Sisodiya
2026-03-04 19:51:41 -08:00
parent e0b157279f
commit 974d553983
7 changed files with 116 additions and 95 deletions
+13 -6
View File
@@ -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);
+2
View File
@@ -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;
+7 -1
View File
@@ -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;
+89 -3
View File
@@ -6,6 +6,7 @@
//! in a single aggregated map using the fully-qualified tool name
//! `"<server><MCP_TOOL_NAME_DELIMITER><tool>"` 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<Duration>,
server_supports_sandbox_state_capability: bool,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
plugin_capability_index: Arc<PluginCapabilityIndex>,
}
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<Event>,
elicitation_requests: ElicitationRequestManager,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
plugin_capability_index: Arc<PluginCapabilityIndex>,
) -> 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<ToolInfo>,
capability_index: &PluginCapabilityIndex,
) -> Vec<ToolInfo> {
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::<Vec<_>>(),
None => capability_index
.plugins_for_mcp_server_name(tool.server_name.as_str())
.into_iter()
.map(|plugin| plugin.display_name.as_str())
.collect::<Vec<_>>(),
};
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::<Vec<_>>()
.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<Event>,
elicitation_requests: ElicitationRequestManager,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
plugin_capability_index: Arc<PluginCapabilityIndex>,
}
async fn make_rmcp_client(
+5 -74
View File
@@ -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<PluginCapabilitySummary>,
plugin_index_by_config_name: HashMap<String, usize>,
plugin_indexes_by_connector_id: HashMap<AppConnectorId, Vec<usize>>,
plugin_indexes_by_mcp_server_name: HashMap<String, Vec<usize>>,
}
@@ -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<String, ToolInfo>,
capability_index: &PluginCapabilityIndex,
) -> HashMap<String, ToolInfo> {
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::<Vec<_>>(),
None => capability_index
.plugins_for_mcp_server_name(tool.server_name.as_str())
.into_iter()
.map(|plugin| plugin.display_name.as_str())
.collect::<Vec<_>>(),
};
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::<Vec<_>>()
.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<LoadedPlugin>,
@@ -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<_>>(),
vec!["skills@test", "alpha@test", "beta@test"]
);
}
-1
View File
@@ -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;
@@ -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<ToolEntry> = mcp_tools
.into_iter()
.map(|(name, info)| ToolEntry::new(name, info))