mirror of
https://github.com/openai/codex.git
synced 2026-05-09 22:02:32 +00:00
Compare commits
6 Commits
owen/sqlit
...
pakrym/rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0a9253ef | ||
|
|
f459c8360d | ||
|
|
d33d9a912a | ||
|
|
0c1c82874c | ||
|
|
496d9d0d8d | ||
|
|
a101766ff4 |
@@ -5,7 +5,6 @@
|
||||
//! connector allow-list filtering, and the normalization that turns app
|
||||
//! connector/tool metadata into model-visible MCP callable names.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -38,16 +37,6 @@ pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCac
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_non_codex_apps_mcp_tools_only(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> HashMap<String, ToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CodexAppsToolsCacheContext {
|
||||
pub(crate) codex_home: PathBuf,
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::qualify_tools;
|
||||
use crate::tools::normalize_tools_for_model;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
@@ -337,10 +337,9 @@ impl McpConnectionManager {
|
||||
failures
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all tools. Each key is the
|
||||
/// fully-qualified name for the tool.
|
||||
/// Returns all tools with model-visible names normalized.
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
|
||||
pub async fn list_all_tools(&self) -> Vec<ToolInfo> {
|
||||
let mut tools = Vec::new();
|
||||
for managed_client in self.clients.values() {
|
||||
let Some(server_tools) = managed_client.listed_tools().await else {
|
||||
@@ -348,15 +347,15 @@ impl McpConnectionManager {
|
||||
};
|
||||
tools.extend(server_tools);
|
||||
}
|
||||
qualify_tools(tools)
|
||||
normalize_tools_for_model(tools)
|
||||
}
|
||||
|
||||
/// Force-refresh codex apps tools by bypassing the in-process cache.
|
||||
///
|
||||
/// On success, the refreshed tools replace the cache contents and the
|
||||
/// latest filtered tool map is returned directly to the caller. On
|
||||
/// latest filtered tools are returned directly to the caller. On
|
||||
/// failure, the existing cache remains unchanged.
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<String, ToolInfo>> {
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<Vec<ToolInfo>> {
|
||||
let managed_client = self
|
||||
.clients
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME)
|
||||
@@ -399,7 +398,7 @@ impl McpConnectionManager {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
tool
|
||||
});
|
||||
Ok(qualify_tools(tools))
|
||||
Ok(normalize_tools_for_model(tools))
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all resources. Each key is the
|
||||
@@ -638,7 +637,7 @@ impl McpConnectionManager {
|
||||
pub async fn resolve_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
|
||||
let all_tools = self.list_all_tools().await;
|
||||
all_tools
|
||||
.into_values()
|
||||
.into_iter()
|
||||
.find(|tool| tool.canonical_tool_name() == *tool_name)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::rmcp_client::elicitation_capability_for_server;
|
||||
use crate::tools::ToolFilter;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::qualify_tools;
|
||||
use crate::tools::normalize_tools_for_model;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::Constrained;
|
||||
use codex_protocol::ToolName;
|
||||
@@ -85,6 +85,26 @@ fn create_codex_apps_tools_cache_context(
|
||||
}
|
||||
}
|
||||
|
||||
fn model_tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
|
||||
tools
|
||||
.iter()
|
||||
.map(ToolInfo::canonical_tool_name)
|
||||
.collect::<HashSet<_>>()
|
||||
}
|
||||
|
||||
fn model_tool_name_len(name: &ToolName) -> usize {
|
||||
name.namespace.as_deref().map_or(0, str::len) + name.name.len()
|
||||
}
|
||||
|
||||
fn is_code_mode_compatible_tool_name(name: &ToolName) -> bool {
|
||||
name.namespace
|
||||
.as_deref()
|
||||
.into_iter()
|
||||
.chain(std::iter::once(name.name.as_str()))
|
||||
.flat_map(str::chars)
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_openai_file_fields_treat_names_literally() {
|
||||
let meta = serde_json::json!({
|
||||
@@ -272,35 +292,41 @@ async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fiel
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_short_non_duplicated_names() {
|
||||
fn test_normalize_tools_short_non_duplicated_names() {
|
||||
let tools = vec![
|
||||
create_test_tool("server1", "tool1"),
|
||||
create_test_tool("server1", "tool2"),
|
||||
];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
assert!(qualified_tools.contains_key("mcp__server1__tool1"));
|
||||
assert!(qualified_tools.contains_key("mcp__server1__tool2"));
|
||||
assert_eq!(
|
||||
model_tool_names(&model_tools),
|
||||
HashSet::from([
|
||||
ToolName::namespaced("mcp__server1__", "tool1"),
|
||||
ToolName::namespaced("mcp__server1__", "tool2")
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_duplicated_names_skipped() {
|
||||
fn test_normalize_tools_duplicated_names_skipped() {
|
||||
let tools = vec![
|
||||
create_test_tool("server1", "duplicate_tool"),
|
||||
create_test_tool("server1", "duplicate_tool"),
|
||||
];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
// Only the first tool should remain, the second is skipped
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
assert!(qualified_tools.contains_key("mcp__server1__duplicate_tool"));
|
||||
assert_eq!(
|
||||
model_tool_names(&model_tools),
|
||||
HashSet::from([ToolName::namespaced("mcp__server1__", "duplicate_tool")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_long_names_same_server() {
|
||||
fn test_normalize_tools_long_names_same_server() {
|
||||
let server_name = "my_server";
|
||||
|
||||
let tools = vec![
|
||||
@@ -314,116 +340,121 @@ fn test_qualify_tools_long_names_same_server() {
|
||||
),
|
||||
];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
assert_eq!(model_tools.len(), 2);
|
||||
|
||||
let mut keys: Vec<_> = qualified_tools.keys().cloned().collect();
|
||||
keys.sort();
|
||||
let names = model_tool_names(&model_tools);
|
||||
|
||||
assert!(keys.iter().all(|key| key.len() == 64));
|
||||
assert!(keys.iter().all(|key| key.starts_with("mcp__my_server__")));
|
||||
assert!(names.iter().all(|name| model_tool_name_len(name) == 64));
|
||||
assert!(
|
||||
keys.iter()
|
||||
.all(|key| key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
|
||||
"qualified names must be code-mode compatible: {keys:?}"
|
||||
names
|
||||
.iter()
|
||||
.all(|name| name.namespace.as_deref() == Some("mcp__my_server__"))
|
||||
);
|
||||
assert!(
|
||||
names.iter().all(is_code_mode_compatible_tool_name),
|
||||
"model-visible names must be code-mode compatible: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_sanitizes_invalid_characters() {
|
||||
fn test_normalize_tools_sanitizes_invalid_characters() {
|
||||
let tools = vec![create_test_tool("server.one", "tool.two-three")];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
let (qualified_name, tool) = qualified_tools.into_iter().next().expect("one tool");
|
||||
assert_eq!(qualified_name, "mcp__server_one__tool_two_three");
|
||||
assert_eq!(model_tools.len(), 1);
|
||||
let tool = model_tools.into_iter().next().expect("one tool");
|
||||
let model_name = tool.canonical_tool_name();
|
||||
assert_eq!(
|
||||
format!("{}{}", tool.callable_namespace, tool.callable_name),
|
||||
qualified_name
|
||||
model_name,
|
||||
ToolName::namespaced("mcp__server_one__", "tool_two_three")
|
||||
);
|
||||
|
||||
// The key and callable parts are sanitized for model-visible tool calls, but
|
||||
// the raw MCP name is preserved for the actual MCP call.
|
||||
assert_eq!(
|
||||
ToolName::namespaced(tool.callable_namespace.clone(), tool.callable_name.clone()),
|
||||
model_name
|
||||
);
|
||||
// The callable parts are sanitized for model-visible tool calls, but the raw
|
||||
// MCP name is preserved for the actual MCP call.
|
||||
assert_eq!(tool.server_name, "server.one");
|
||||
assert_eq!(tool.callable_namespace, "mcp__server_one__");
|
||||
assert_eq!(tool.callable_name, "tool_two_three");
|
||||
assert_eq!(tool.tool.name, "tool.two-three");
|
||||
|
||||
assert!(
|
||||
qualified_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_'),
|
||||
"qualified name must be code-mode compatible: {qualified_name:?}"
|
||||
is_code_mode_compatible_tool_name(&model_name),
|
||||
"model-visible name must be code-mode compatible: {model_name:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_keeps_hyphenated_mcp_tools_callable() {
|
||||
fn test_normalize_tools_keeps_hyphenated_mcp_tools_callable() {
|
||||
let tools = vec![create_test_tool("music-studio", "get-strudel-guide")];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 1);
|
||||
let (qualified_name, tool) = qualified_tools.into_iter().next().expect("one tool");
|
||||
assert_eq!(qualified_name, "mcp__music_studio__get_strudel_guide");
|
||||
assert_eq!(model_tools.len(), 1);
|
||||
let tool = model_tools.into_iter().next().expect("one tool");
|
||||
assert_eq!(
|
||||
tool.canonical_tool_name(),
|
||||
ToolName::namespaced("mcp__music_studio__", "get_strudel_guide")
|
||||
);
|
||||
assert_eq!(tool.callable_namespace, "mcp__music_studio__");
|
||||
assert_eq!(tool.callable_name, "get_strudel_guide");
|
||||
assert_eq!(tool.tool.name, "get-strudel-guide");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_disambiguates_sanitized_namespace_collisions() {
|
||||
fn test_normalize_tools_disambiguates_sanitized_namespace_collisions() {
|
||||
let tools = vec![
|
||||
create_test_tool("basic-server", "lookup"),
|
||||
create_test_tool("basic_server", "query"),
|
||||
];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
let mut namespaces = qualified_tools
|
||||
.values()
|
||||
assert_eq!(model_tools.len(), 2);
|
||||
let mut namespaces = model_tools
|
||||
.iter()
|
||||
.map(|tool| tool.callable_namespace.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
namespaces.sort();
|
||||
namespaces.dedup();
|
||||
assert_eq!(namespaces.len(), 2);
|
||||
|
||||
let raw_servers = qualified_tools
|
||||
.values()
|
||||
let raw_servers = model_tools
|
||||
.iter()
|
||||
.map(|tool| tool.server_name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(raw_servers, HashSet::from(["basic-server", "basic_server"]));
|
||||
let model_names = model_tool_names(&model_tools);
|
||||
assert!(
|
||||
qualified_tools
|
||||
.keys()
|
||||
.all(|key| key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
|
||||
"qualified names must be code-mode compatible: {qualified_tools:?}"
|
||||
model_names.iter().all(is_code_mode_compatible_tool_name),
|
||||
"model-visible names must be code-mode compatible: {model_names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_disambiguates_sanitized_tool_name_collisions() {
|
||||
fn test_normalize_tools_disambiguates_sanitized_tool_name_collisions() {
|
||||
let tools = vec![
|
||||
create_test_tool("server", "tool-name"),
|
||||
create_test_tool("server", "tool_name"),
|
||||
];
|
||||
|
||||
let qualified_tools = qualify_tools(tools);
|
||||
let model_tools = normalize_tools_for_model(tools);
|
||||
|
||||
assert_eq!(qualified_tools.len(), 2);
|
||||
let raw_tool_names = qualified_tools
|
||||
.values()
|
||||
assert_eq!(model_tools.len(), 2);
|
||||
let raw_tool_names = model_tools
|
||||
.iter()
|
||||
.map(|tool| tool.tool.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
raw_tool_names,
|
||||
HashSet::from(["tool-name".to_string(), "tool_name".to_string()])
|
||||
);
|
||||
let callable_tool_names = qualified_tools
|
||||
.values()
|
||||
let callable_tool_names = model_tools
|
||||
.iter()
|
||||
.map(|tool| tool.callable_name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(callable_tool_names.len(), 2);
|
||||
@@ -672,7 +703,11 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
.get("mcp__codex_apps__calendar_create_event")
|
||||
.iter()
|
||||
.find(|tool| {
|
||||
tool.canonical_tool_name()
|
||||
== ToolName::namespaced("mcp__codex_apps__", "calendar_create_event")
|
||||
})
|
||||
.expect("tool from startup cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
@@ -798,7 +833,11 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
.get("mcp__codex_apps__calendar_create_event")
|
||||
.iter()
|
||||
.find(|tool| {
|
||||
tool.canonical_tool_name()
|
||||
== ToolName::namespaced("mcp__codex_apps__", "calendar_create_event")
|
||||
})
|
||||
.expect("tool from startup cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
|
||||
@@ -48,7 +48,6 @@ pub use mcp::oauth_login_support;
|
||||
pub use mcp::resolve_oauth_scopes;
|
||||
pub use mcp::should_retry_without_scopes;
|
||||
|
||||
pub use codex_apps::filter_non_codex_apps_mcp_tools_only;
|
||||
pub use mcp::McpPermissionPromptAutoApproveContext;
|
||||
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
||||
pub use mcp::qualified_mcp_tool_name_prefix;
|
||||
|
||||
@@ -564,7 +564,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
);
|
||||
|
||||
let mut tools_by_server = HashMap::<String, HashMap<String, Tool>>::new();
|
||||
for (_qualified_name, tool_info) in tools {
|
||||
for tool_info in tools {
|
||||
let raw_tool_name = tool_info.tool.name.to_string();
|
||||
let Some(tool) = protocol_tool_from_rmcp_tool(&raw_tool_name, &tool_info.tool) else {
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! MCP tool metadata, filtering, schema shaping, and name qualification.
|
||||
//! MCP tool metadata, filtering, schema shaping, and name normalization.
|
||||
//!
|
||||
//! Raw MCP tool identities must be preserved for protocol calls, while
|
||||
//! model-visible tool names must be sanitized, deduplicated, and kept within API
|
||||
@@ -130,12 +130,12 @@ pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<Too
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a qualified-name lookup for MCP tools.
|
||||
/// Returns MCP tools with model-visible names normalized.
|
||||
///
|
||||
/// Raw MCP server/tool names are kept on each [`ToolInfo`] for protocol calls, while
|
||||
/// `callable_namespace` / `callable_name` are sanitized and, when necessary, hashed so
|
||||
/// every model-visible `mcp__namespace__tool` name is unique and <= 64 bytes.
|
||||
pub(crate) fn qualify_tools<I>(tools: I) -> HashMap<String, ToolInfo>
|
||||
/// every model-visible name is unique and <= 64 bytes.
|
||||
pub(crate) fn normalize_tools_for_model<I>(tools: I) -> Vec<ToolInfo>
|
||||
where
|
||||
I: IntoIterator<Item = ToolInfo>,
|
||||
{
|
||||
@@ -213,9 +213,9 @@ where
|
||||
candidates.sort_by(|left, right| left.raw_tool_identity.cmp(&right.raw_tool_identity));
|
||||
|
||||
let mut used_names = HashSet::new();
|
||||
let mut qualified_tools = HashMap::new();
|
||||
let mut model_tools = Vec::new();
|
||||
for mut candidate in candidates {
|
||||
let (callable_namespace, callable_name, qualified_name) = unique_callable_parts(
|
||||
let (callable_namespace, callable_name) = unique_callable_parts(
|
||||
&candidate.callable_namespace,
|
||||
&candidate.callable_name,
|
||||
&candidate.raw_tool_identity,
|
||||
@@ -223,9 +223,9 @@ where
|
||||
);
|
||||
candidate.tool.callable_namespace = callable_namespace;
|
||||
candidate.tool.callable_name = callable_name;
|
||||
qualified_tools.insert(qualified_name, candidate.tool);
|
||||
model_tools.push(candidate.tool);
|
||||
}
|
||||
qualified_tools
|
||||
model_tools
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -345,10 +345,10 @@ fn unique_callable_parts(
|
||||
tool_name: &str,
|
||||
raw_identity: &str,
|
||||
used_names: &mut HashSet<String>,
|
||||
) -> (String, String, String) {
|
||||
let qualified_name = format!("{namespace}{tool_name}");
|
||||
if qualified_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(qualified_name.clone()) {
|
||||
return (namespace.to_string(), tool_name.to_string(), qualified_name);
|
||||
) -> (String, String) {
|
||||
let model_name = format!("{namespace}{tool_name}");
|
||||
if model_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) {
|
||||
return (namespace.to_string(), tool_name.to_string());
|
||||
}
|
||||
|
||||
let mut attempt = 0_u32;
|
||||
@@ -360,9 +360,9 @@ fn unique_callable_parts(
|
||||
};
|
||||
let (namespace, tool_name) =
|
||||
fit_callable_parts_with_hash(namespace, tool_name, &hash_input);
|
||||
let qualified_name = format!("{namespace}{tool_name}");
|
||||
if used_names.insert(qualified_name.clone()) {
|
||||
return (namespace, tool_name, qualified_name);
|
||||
let model_name = format!("{namespace}{tool_name}");
|
||||
if used_names.insert(model_name) {
|
||||
return (namespace, tool_name);
|
||||
}
|
||||
attempt = attempt.saturating_add(1);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
mcp_tools: &[ToolInfo],
|
||||
) {
|
||||
if !config.features.enabled(Feature::Apps) {
|
||||
return;
|
||||
@@ -516,12 +516,10 @@ async fn chatgpt_get_request_with_auth_provider<T: DeserializeOwned>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
|
||||
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
||||
// can be derived here instead of requiring a separate enrichment pass.
|
||||
let tools = mcp_tools.values().filter_map(|tool| {
|
||||
let tools = mcp_tools.iter().filter_map(|tool| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -172,39 +172,30 @@ fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
|
||||
#[test]
|
||||
fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
/*connector_name*/ None,
|
||||
&["sample", "sample"],
|
||||
),
|
||||
let tools = vec![
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
/*connector_name*/ None,
|
||||
&["sample", "sample"],
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["beta", "sample"],
|
||||
),
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["beta", "sample"],
|
||||
),
|
||||
(
|
||||
"mcp__sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
namespace_description: None,
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: plugin_names(&["ignored"]),
|
||||
},
|
||||
),
|
||||
]);
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
namespace_description: None,
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: plugin_names(&["ignored"]),
|
||||
},
|
||||
];
|
||||
|
||||
let connectors = accessible_connectors_from_mcp_tools(&tools);
|
||||
|
||||
@@ -238,26 +229,20 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
.expect("config should load");
|
||||
let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);
|
||||
let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["calendar-plugin"],
|
||||
),
|
||||
let tools = vec![
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["calendar-plugin"],
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__openai_hidden".to_string(),
|
||||
codex_app_tool(
|
||||
"openai_hidden",
|
||||
"connector_openai_hidden",
|
||||
Some("Hidden"),
|
||||
&[],
|
||||
),
|
||||
codex_app_tool(
|
||||
"openai_hidden",
|
||||
"connector_openai_hidden",
|
||||
Some("Hidden"),
|
||||
&[],
|
||||
),
|
||||
]);
|
||||
];
|
||||
|
||||
let cached = with_accessible_connectors_cache_cleared(|| {
|
||||
refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools);
|
||||
@@ -315,29 +300,26 @@ fn merge_connectors_unions_and_dedupes_plugin_display_names() {
|
||||
|
||||
#[test]
|
||||
fn accessible_connectors_from_mcp_tools_preserves_description() {
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: Some("Plan events".to_string()),
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event".into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
let mcp_tools = vec![ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: Some("Plan events".to_string()),
|
||||
tool: Tool {
|
||||
name: "calendar_create_event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event".into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
)]);
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
accessible_connectors_from_mcp_tools(&mcp_tools),
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
call_id: String,
|
||||
server: String,
|
||||
tool_name: String,
|
||||
hook_tool_name: String,
|
||||
hook_tool_name: HookToolName,
|
||||
arguments: String,
|
||||
) -> HandledMcpToolCall {
|
||||
// Parse the `arguments` as JSON. An empty string is OK, but invalid JSON
|
||||
@@ -220,7 +220,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context,
|
||||
&call_id,
|
||||
&invocation,
|
||||
&hook_tool_name,
|
||||
hook_tool_name,
|
||||
metadata.as_ref(),
|
||||
approval_mode,
|
||||
)
|
||||
@@ -1144,7 +1144,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
turn_context: &Arc<TurnContext>,
|
||||
call_id: &str,
|
||||
invocation: &McpInvocation,
|
||||
hook_tool_name: &str,
|
||||
hook_tool_name: HookToolName,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
@@ -1204,7 +1204,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
turn_context,
|
||||
call_id,
|
||||
PermissionRequestPayload {
|
||||
tool_name: HookToolName::new(hook_tool_name),
|
||||
tool_name: hook_tool_name,
|
||||
tool_input: invocation
|
||||
.arguments
|
||||
.clone()
|
||||
@@ -1477,7 +1477,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let tool_info = tools
|
||||
.into_values()
|
||||
.into_iter()
|
||||
.find(|tool_info| tool_info.server_name == server && tool_info.tool.name == tool_name)?;
|
||||
let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(
|
||||
@@ -1572,7 +1572,7 @@ async fn lookup_mcp_app_usage_metadata(
|
||||
.list_all_tools()
|
||||
.await;
|
||||
|
||||
tools.into_values().find_map(|tool_info| {
|
||||
tools.into_iter().find_map(|tool_info| {
|
||||
if tool_info.server_name == server && tool_info.tool.name == tool_name {
|
||||
Some(McpAppUsageMetadata {
|
||||
connector_id: tool_info.connector_id,
|
||||
|
||||
@@ -55,6 +55,14 @@ fn annotations(
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_tool_name(name: &str) -> HookToolName {
|
||||
HookToolName::new(name)
|
||||
}
|
||||
|
||||
fn hook_tool_name_with_legacy_alias(name: &str, legacy_name: &str) -> HookToolName {
|
||||
HookToolName::new_with_aliases(name, vec![legacy_name.to_string()])
|
||||
}
|
||||
|
||||
fn approval_metadata(
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
@@ -2186,7 +2194,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
|
||||
&turn_context,
|
||||
"call-1",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("read_only_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2259,7 +2267,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
|
||||
&turn_context,
|
||||
"call-guardian",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("read_only_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2315,7 +2323,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
&turn_context,
|
||||
"call-mcp-hook",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
hook_tool_name_with_legacy_alias("create_entities", "mcp__memory__create_entities"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2336,7 +2344,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
||||
"transcript_path": null,
|
||||
"model": turn_context.model_info.slug,
|
||||
"permission_mode": "default",
|
||||
"tool_name": "mcp__memory__create_entities",
|
||||
"tool_name": "create_entities",
|
||||
"hook_event_name": "PermissionRequest",
|
||||
"tool_input": {
|
||||
"entities": [{
|
||||
@@ -2375,7 +2383,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
||||
&turn_context,
|
||||
"call-mcp-hook-no-metadata",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
hook_tool_name_with_legacy_alias("create_entities", "mcp__memory__create_entities"),
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2396,7 +2404,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
||||
"transcript_path": null,
|
||||
"model": turn_context.model_info.slug,
|
||||
"permission_mode": "default",
|
||||
"tool_name": "mcp__memory__create_entities",
|
||||
"tool_name": "create_entities",
|
||||
"hook_event_name": "PermissionRequest",
|
||||
"tool_input": { "entities": [] }
|
||||
})]
|
||||
@@ -2452,7 +2460,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
||||
&turn_context,
|
||||
"call-mcp-remembered",
|
||||
&invocation,
|
||||
"mcp__memory__create_entities",
|
||||
hook_tool_name_with_legacy_alias("create_entities", "mcp__memory__create_entities"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2532,7 +2540,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
|
||||
&turn_context,
|
||||
"call-guardian-deny",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
)
|
||||
@@ -2589,7 +2597,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
&turn_context,
|
||||
"call-prompt",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("read_only_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Prompt,
|
||||
)
|
||||
@@ -2664,7 +2672,7 @@ async fn approve_mode_skips_arc_interrupt_for_model() {
|
||||
&turn_context,
|
||||
"call-2",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2731,7 +2739,7 @@ async fn custom_approve_mode_skips_arc_interrupt_for_model() {
|
||||
&turn_context,
|
||||
"call-2-custom",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2798,7 +2806,7 @@ async fn approve_mode_skips_arc_interrupt_without_annotations() {
|
||||
&turn_context,
|
||||
"call-3",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
@@ -2875,7 +2883,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
|
||||
&turn_context,
|
||||
"call-2",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
approval_mode,
|
||||
)
|
||||
@@ -2978,7 +2986,7 @@ async fn approve_mode_skips_arc_and_guardian_in_every_permission_mode() {
|
||||
&turn_context,
|
||||
"call-3",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
hook_tool_name("dangerous_tool"),
|
||||
Some(&metadata),
|
||||
AppToolApproval::Approve,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo as McpToolInfo;
|
||||
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
|
||||
use codex_tools::ToolsConfig;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -13,12 +11,12 @@ use crate::connectors;
|
||||
pub(crate) const DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD: usize = 100;
|
||||
|
||||
pub(crate) struct McpToolExposure {
|
||||
pub(crate) direct_tools: HashMap<String, McpToolInfo>,
|
||||
pub(crate) deferred_tools: Option<HashMap<String, McpToolInfo>>,
|
||||
pub(crate) direct_tools: Vec<McpToolInfo>,
|
||||
pub(crate) deferred_tools: Option<Vec<McpToolInfo>>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_mcp_tool_exposure(
|
||||
all_mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
all_mcp_tools: &[McpToolInfo],
|
||||
connectors: Option<&[connectors::AppInfo]>,
|
||||
explicitly_enabled_connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
@@ -48,9 +46,11 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
|
||||
let direct_tools =
|
||||
filter_codex_apps_mcp_tools(all_mcp_tools, explicitly_enabled_connectors, config);
|
||||
for direct_tool_name in direct_tools.keys() {
|
||||
deferred_tools.remove(direct_tool_name);
|
||||
}
|
||||
let direct_tool_names = direct_tools
|
||||
.iter()
|
||||
.map(McpToolInfo::canonical_tool_name)
|
||||
.collect::<HashSet<_>>();
|
||||
deferred_tools.retain(|tool| !direct_tool_names.contains(&tool.canonical_tool_name()));
|
||||
|
||||
McpToolExposure {
|
||||
direct_tools,
|
||||
@@ -58,11 +58,19 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_non_codex_apps_mcp_tools_only(mcp_tools: &[McpToolInfo]) -> Vec<McpToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
mcp_tools: &[McpToolInfo],
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, McpToolInfo> {
|
||||
) -> Vec<McpToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
@@ -70,7 +78,7 @@ fn filter_codex_apps_mcp_tools(
|
||||
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
.filter(|tool| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return false;
|
||||
}
|
||||
@@ -79,7 +87,7 @@ fn filter_codex_apps_mcp_tools(
|
||||
};
|
||||
allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool)
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_connectors::metadata::sanitize_name;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
@@ -11,6 +10,7 @@ use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::ToolsConfigParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -42,22 +42,15 @@ fn make_connector(id: &str, name: &str) -> AppInfo {
|
||||
fn make_mcp_tool(
|
||||
server_name: &str,
|
||||
tool_name: &str,
|
||||
callable_namespace: &str,
|
||||
callable_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
) -> ToolInfo {
|
||||
let tool_namespace = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connector_name
|
||||
.map(sanitize_name)
|
||||
.map(|connector_name| format!("mcp__{server_name}__{connector_name}"))
|
||||
.unwrap_or_else(|| server_name.to_string())
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
};
|
||||
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
callable_name: callable_name.to_string(),
|
||||
callable_namespace: callable_namespace.to_string(),
|
||||
namespace_description: None,
|
||||
tool: Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
@@ -76,20 +69,29 @@ fn make_mcp_tool(
|
||||
}
|
||||
}
|
||||
|
||||
fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
|
||||
fn numbered_mcp_tools(count: usize) -> Vec<ToolInfo> {
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let tool_name = format!("tool_{index}");
|
||||
(
|
||||
format!("mcp__rmcp__{tool_name}"),
|
||||
make_mcp_tool(
|
||||
"rmcp", &tool_name, /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
make_mcp_tool(
|
||||
"rmcp",
|
||||
&tool_name,
|
||||
"mcp__rmcp__",
|
||||
&tool_name,
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
|
||||
tools
|
||||
.iter()
|
||||
.map(codex_mcp::ToolInfo::canonical_tool_name)
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
|
||||
let config = test_config().await;
|
||||
let model_info =
|
||||
@@ -124,11 +126,7 @@ async fn directly_exposes_small_effective_tool_sets() {
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut direct_tool_names: Vec<_> = exposure.direct_tools.keys().cloned().collect();
|
||||
direct_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(direct_tool_names, expected_tool_names);
|
||||
assert_eq!(tool_names(&exposure.direct_tools), tool_names(&mcp_tools));
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
}
|
||||
|
||||
@@ -151,11 +149,7 @@ async fn searches_large_effective_tool_sets() {
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("large tool sets should be discoverable through tool_search");
|
||||
let mut deferred_tool_names: Vec<_> = deferred_tools.keys().cloned().collect();
|
||||
deferred_tool_names.sort();
|
||||
let mut expected_tool_names: Vec<_> = mcp_tools.keys().cloned().collect();
|
||||
expected_tool_names.sort();
|
||||
assert_eq!(deferred_tool_names, expected_tool_names);
|
||||
assert_eq!(tool_names(deferred_tools), tool_names(&mcp_tools));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -163,15 +157,14 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
let config = test_config().await;
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
|
||||
mcp_tools.extend([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
)]);
|
||||
mcp_tools.push(make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
));
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
@@ -182,28 +175,32 @@ async fn directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut tool_names: Vec<String> = exposure.direct_tools.into_keys().collect();
|
||||
tool_names.sort();
|
||||
let direct_tool_names = tool_names(&exposure.direct_tools);
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
vec!["mcp__codex_apps__calendar_create_event".to_string()]
|
||||
direct_tool_names,
|
||||
HashSet::from([ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
)])
|
||||
);
|
||||
assert_eq!(
|
||||
exposure.deferred_tools.as_ref().map(HashMap::len),
|
||||
exposure.deferred_tools.as_ref().map(Vec::len),
|
||||
Some(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1)
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("large tool sets should be discoverable through tool_search");
|
||||
let deferred_tool_names = tool_names(deferred_tools);
|
||||
assert!(
|
||||
tool_names
|
||||
.iter()
|
||||
.all(|direct_tool_name| !deferred_tools.contains_key(direct_tool_name)),
|
||||
"direct tools should not also be deferred: {tool_names:?}"
|
||||
direct_tool_names.is_disjoint(&deferred_tool_names),
|
||||
"direct tools should not also be deferred: {direct_tool_names:?}"
|
||||
);
|
||||
assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event"));
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
|
||||
assert!(!deferred_tool_names.contains(&ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
)));
|
||||
assert!(deferred_tool_names.contains(&ToolName::namespaced("mcp__rmcp__", "tool_0")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -214,23 +211,24 @@ async fn always_defer_feature_preserves_explicit_apps() {
|
||||
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
|
||||
.expect("test config should allow feature update");
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mcp_tools = HashMap::from([
|
||||
(
|
||||
"mcp__rmcp__tool".to_string(),
|
||||
make_mcp_tool(
|
||||
"rmcp", "tool", /*connector_id*/ None, /*connector_name*/ None,
|
||||
),
|
||||
let mcp_tools = vec![
|
||||
make_mcp_tool(
|
||||
"rmcp",
|
||||
"tool",
|
||||
"mcp__rmcp__",
|
||||
"tool",
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
),
|
||||
]);
|
||||
];
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
@@ -241,16 +239,22 @@ async fn always_defer_feature_preserves_explicit_apps() {
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
let mut direct_tool_names: Vec<String> = exposure.direct_tools.into_keys().collect();
|
||||
direct_tool_names.sort();
|
||||
let direct_tool_names = tool_names(&exposure.direct_tools);
|
||||
assert_eq!(
|
||||
direct_tool_names,
|
||||
vec!["mcp__codex_apps__calendar_create_event".to_string()]
|
||||
HashSet::from([ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
)])
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("MCP tools should be discoverable through tool_search");
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool"));
|
||||
assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event"));
|
||||
let deferred_tool_names = tool_names(deferred_tools);
|
||||
assert!(deferred_tool_names.contains(&ToolName::namespaced("mcp__rmcp__", "tool")));
|
||||
assert!(!deferred_tool_names.contains(&ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
"_create_event"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::handlers::unified_exec::ExecCommandArgs;
|
||||
use codex_memories_read::usage::MEMORIES_USAGE_METRIC;
|
||||
use codex_memories_read::usage::memories_usage_kinds_from_command;
|
||||
@@ -17,14 +18,14 @@ pub(crate) async fn emit_metric_for_tool_read(invocation: &ToolInvocation, succe
|
||||
}
|
||||
|
||||
let success = if success { "true" } else { "false" };
|
||||
let tool_name = invocation.tool_name.display();
|
||||
let tool_name = flat_tool_name(&invocation.tool_name);
|
||||
for kind in kinds {
|
||||
invocation.turn.session_telemetry.counter(
|
||||
MEMORIES_USAGE_METRIC,
|
||||
/*inc*/ 1,
|
||||
&[
|
||||
("kind", kind.as_tag()),
|
||||
("tool", &tool_name),
|
||||
("tool", tool_name.as_ref()),
|
||||
("success", success),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_connectors::metadata::connector_display_label;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -14,7 +13,7 @@ use codex_mcp::ToolInfo;
|
||||
|
||||
pub(crate) fn build_plugin_injections(
|
||||
mentioned_plugins: &[PluginCapabilitySummary],
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
mcp_tools: &[ToolInfo],
|
||||
available_connectors: &[connectors::AppInfo],
|
||||
) -> Vec<ResponseItem> {
|
||||
if mentioned_plugins.is_empty() {
|
||||
@@ -27,7 +26,7 @@ pub(crate) fn build_plugin_injections(
|
||||
.iter()
|
||||
.filter_map(|plugin| {
|
||||
let available_mcp_servers = mcp_tools
|
||||
.values()
|
||||
.iter()
|
||||
.filter(|tool| {
|
||||
tool.server_name != CODEX_APPS_MCP_SERVER_NAME
|
||||
&& tool
|
||||
|
||||
@@ -1025,7 +1025,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn),
|
||||
call_id: "probe-call".to_string(),
|
||||
tool_name: "probe".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("probe"),
|
||||
};
|
||||
|
||||
orchestrator
|
||||
|
||||
@@ -195,10 +195,10 @@ pub(crate) async fn run_turn(
|
||||
{
|
||||
Ok(mcp_tools) => mcp_tools,
|
||||
Err(_) if turn_context.apps_enabled() => return None,
|
||||
Err(_) => HashMap::new(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
} else {
|
||||
HashMap::new()
|
||||
Vec::new()
|
||||
};
|
||||
let available_connectors = if turn_context.apps_enabled() {
|
||||
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
|
||||
@@ -1247,7 +1247,7 @@ pub(crate) async fn built_tools(
|
||||
let exposed_tool_names = mcp_tools
|
||||
.iter()
|
||||
.chain(deferred_mcp_tools.iter())
|
||||
.flat_map(|tools| tools.keys().map(String::as_str))
|
||||
.flat_map(|tools| tools.iter().map(codex_mcp::ToolInfo::canonical_tool_name))
|
||||
.collect::<HashSet<_>>();
|
||||
collect_unavailable_called_tools(input, &exposed_tool_names)
|
||||
} else {
|
||||
|
||||
@@ -236,7 +236,7 @@ pub(crate) async fn handle_output_item_done(
|
||||
tracing::info!(
|
||||
thread_id = %ctx.sess.conversation_id,
|
||||
"ToolCall: {} {}",
|
||||
call.tool_name.display(),
|
||||
call.tool_name,
|
||||
payload_preview
|
||||
);
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
call_id: call_id.clone(),
|
||||
tool_name: tool_name.display(),
|
||||
tool_name: tool_name.clone(),
|
||||
};
|
||||
let out = orchestrator
|
||||
.run(
|
||||
@@ -533,7 +533,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
call_id: call_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_name: ToolName::plain(tool_name),
|
||||
};
|
||||
let out = orchestrator
|
||||
.run(
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::tools::context::McpToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
@@ -38,12 +39,16 @@ impl ToolHandler for McpHandler {
|
||||
}
|
||||
|
||||
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
||||
let ToolPayload::Mcp { raw_arguments, .. } = &invocation.payload else {
|
||||
let ToolPayload::Mcp {
|
||||
tool,
|
||||
raw_arguments,
|
||||
..
|
||||
} = &invocation.payload
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::new(self.tool_name.display()),
|
||||
tool_name: mcp_hook_tool_name(tool, &self.tool_name),
|
||||
tool_input: mcp_hook_tool_input(raw_arguments),
|
||||
})
|
||||
}
|
||||
@@ -53,14 +58,14 @@ impl ToolHandler for McpHandler {
|
||||
invocation: &ToolInvocation,
|
||||
result: &Self::Output,
|
||||
) -> Option<PostToolUsePayload> {
|
||||
let ToolPayload::Mcp { .. } = &invocation.payload else {
|
||||
let ToolPayload::Mcp { tool, .. } = &invocation.payload else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let tool_response =
|
||||
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::new(self.tool_name.display()),
|
||||
tool_name: mcp_hook_tool_name(tool, &self.tool_name),
|
||||
tool_use_id: invocation.call_id.clone(),
|
||||
tool_input: result.tool_input.clone(),
|
||||
tool_response,
|
||||
@@ -93,13 +98,14 @@ impl ToolHandler for McpHandler {
|
||||
let arguments_str = raw_arguments;
|
||||
|
||||
let started = Instant::now();
|
||||
let hook_tool_name = mcp_hook_tool_name(&tool, &self.tool_name);
|
||||
let result = handle_mcp_tool_call(
|
||||
Arc::clone(&session),
|
||||
&turn,
|
||||
call_id.clone(),
|
||||
server,
|
||||
tool,
|
||||
self.tool_name.display(),
|
||||
hook_tool_name,
|
||||
arguments_str,
|
||||
)
|
||||
.await;
|
||||
@@ -114,6 +120,15 @@ impl ToolHandler for McpHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_hook_tool_name(raw_tool_name: &str, canonical_tool_name: &ToolName) -> HookToolName {
|
||||
let legacy_name = flat_tool_name(canonical_tool_name);
|
||||
if legacy_name.as_ref() == raw_tool_name {
|
||||
HookToolName::new(raw_tool_name.to_string())
|
||||
} else {
|
||||
HookToolName::new_with_aliases(raw_tool_name.to_string(), vec![legacy_name.into_owned()])
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
|
||||
if raw_arguments.trim().is_empty() {
|
||||
return Value::Object(serde_json::Map::new());
|
||||
@@ -134,7 +149,7 @@ mod tests {
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_pre_tool_use_payload_uses_model_tool_name_and_raw_args() {
|
||||
async fn mcp_pre_tool_use_payload_uses_raw_mcp_tool_name_and_args() {
|
||||
let payload = ToolPayload::Mcp {
|
||||
server: "memory".to_string(),
|
||||
tool: "create_entities".to_string(),
|
||||
@@ -164,7 +179,10 @@ mod tests {
|
||||
payload,
|
||||
}),
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::new("mcp__memory__create_entities"),
|
||||
tool_name: HookToolName::new_with_aliases(
|
||||
"create_entities",
|
||||
vec!["mcp__memory__create_entities".to_string()],
|
||||
),
|
||||
tool_input: json!({
|
||||
"entities": [{
|
||||
"name": "Ada",
|
||||
@@ -176,7 +194,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_post_tool_use_payload_uses_model_tool_name_args_and_result() {
|
||||
async fn mcp_post_tool_use_payload_uses_raw_mcp_tool_name_args_and_result() {
|
||||
let payload = ToolPayload::Mcp {
|
||||
server: "filesystem".to_string(),
|
||||
tool: "read_file".to_string(),
|
||||
@@ -219,7 +237,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
handler.post_tool_use_payload(&invocation, &output),
|
||||
Some(PostToolUsePayload {
|
||||
tool_name: HookToolName::new("mcp__filesystem__read_file"),
|
||||
tool_name: HookToolName::new_with_aliases(
|
||||
"read_file",
|
||||
vec!["mcp__filesystem__read_file".to_string()],
|
||||
),
|
||||
tool_use_id: "call-mcp-post".to_string(),
|
||||
tool_input: json!({
|
||||
"path": {
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_tools::ToolName;
|
||||
|
||||
mod container_exec;
|
||||
mod local_shell;
|
||||
@@ -71,7 +72,7 @@ fn shell_command_payload_command(payload: &ToolPayload) -> Option<String> {
|
||||
}
|
||||
|
||||
struct RunExecLikeArgs {
|
||||
tool_name: String,
|
||||
tool_name: ToolName,
|
||||
exec_params: ExecParams,
|
||||
hook_command: String,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
@@ -200,7 +201,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
turn.clone(),
|
||||
Some(&tracker),
|
||||
&call_id,
|
||||
tool_name.as_str(),
|
||||
tool_name.name.as_str(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ impl ToolHandler for ContainerExecHandler {
|
||||
let exec_params =
|
||||
ShellHandler::to_exec_params(¶ms, turn.as_ref(), session.conversation_id);
|
||||
run_exec_like(RunExecLikeArgs {
|
||||
tool_name: "container.exec".to_string(),
|
||||
tool_name: ToolName::plain("container.exec"),
|
||||
exec_params,
|
||||
hook_command: codex_shell_command::parse_command::shlex_join(¶ms.command),
|
||||
additional_permissions: params.additional_permissions.clone(),
|
||||
|
||||
@@ -85,7 +85,7 @@ impl ToolHandler for LocalShellHandler {
|
||||
let exec_params =
|
||||
ShellHandler::to_exec_params(¶ms, turn.as_ref(), session.conversation_id);
|
||||
run_exec_like(RunExecLikeArgs {
|
||||
tool_name: "local_shell".to_string(),
|
||||
tool_name: ToolName::plain("local_shell"),
|
||||
exec_params,
|
||||
hook_command: codex_shell_command::parse_command::shlex_join(¶ms.command),
|
||||
additional_permissions: None,
|
||||
|
||||
@@ -172,10 +172,10 @@ impl ToolHandler for ShellCommandHandler {
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
let tool_name = self.tool_name();
|
||||
let ToolPayload::Function { arguments } = payload else {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"unsupported payload for shell_command handler: {}",
|
||||
self.tool_name().display()
|
||||
"unsupported payload for shell_command handler: {tool_name}"
|
||||
)));
|
||||
};
|
||||
|
||||
@@ -198,7 +198,7 @@ impl ToolHandler for ShellCommandHandler {
|
||||
turn.tools_config.allow_login_shell,
|
||||
)?;
|
||||
run_exec_like(RunExecLikeArgs {
|
||||
tool_name: self.tool_name().display(),
|
||||
tool_name,
|
||||
exec_params,
|
||||
hook_command: params.command,
|
||||
additional_permissions: params.additional_permissions.clone(),
|
||||
|
||||
@@ -113,7 +113,7 @@ impl ToolHandler for ShellHandler {
|
||||
let exec_params =
|
||||
ShellHandler::to_exec_params(¶ms, turn.as_ref(), session.conversation_id);
|
||||
run_exec_like(RunExecLikeArgs {
|
||||
tool_name: "shell".to_string(),
|
||||
tool_name: ToolName::plain("shell"),
|
||||
exec_params,
|
||||
hook_command: codex_shell_command::parse_command::shlex_join(¶ms.command),
|
||||
additional_permissions: params.additional_permissions.clone(),
|
||||
|
||||
@@ -204,19 +204,11 @@ mod tests {
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let handler = handler_from_tools(
|
||||
Some(&std::collections::HashMap::from([
|
||||
(
|
||||
"mcp__calendar__create_event".to_string(),
|
||||
tool_info("calendar", "create_event", "Create events"),
|
||||
),
|
||||
(
|
||||
"mcp__calendar__list_events".to_string(),
|
||||
tool_info("calendar", "list_events", "List events"),
|
||||
),
|
||||
])),
|
||||
&dynamic_tools,
|
||||
);
|
||||
let mcp_tools = vec![
|
||||
tool_info("calendar", "create_event", "Create events"),
|
||||
tool_info("calendar", "list_events", "List events"),
|
||||
];
|
||||
let handler = handler_from_tools(Some(&mcp_tools), &dynamic_tools);
|
||||
let results = [
|
||||
&handler.entries[0],
|
||||
&handler.entries[2],
|
||||
@@ -376,18 +368,11 @@ mod tests {
|
||||
assert!(count_results_for_server(&results, "other-server") <= TOOL_SEARCH_DEFAULT_LIMIT);
|
||||
}
|
||||
|
||||
fn numbered_tools(
|
||||
server_name: &str,
|
||||
description_prefix: &str,
|
||||
count: usize,
|
||||
) -> std::collections::HashMap<String, ToolInfo> {
|
||||
fn numbered_tools(server_name: &str, description_prefix: &str, count: usize) -> Vec<ToolInfo> {
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let tool_name = format!("tool_{index:03}");
|
||||
(
|
||||
format!("mcp__{server_name}__{tool_name}"),
|
||||
tool_info(server_name, &tool_name, description_prefix),
|
||||
)
|
||||
tool_info(server_name, &tool_name, description_prefix)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -427,7 +412,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn handler_from_tools(
|
||||
mcp_tools: Option<&std::collections::HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<&[ToolInfo]>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolSearchHandler {
|
||||
ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools))
|
||||
|
||||
@@ -42,7 +42,7 @@ impl ToolHandler for UnavailableToolHandler {
|
||||
match payload {
|
||||
ToolPayload::Function { .. } => Ok(FunctionToolOutput::from_text(
|
||||
unavailable_tool_message(
|
||||
self.tool_name.display(),
|
||||
&self.tool_name,
|
||||
"Retry after the tool becomes available or ask the user to re-enable it.",
|
||||
),
|
||||
Some(false),
|
||||
|
||||
@@ -25,6 +25,15 @@ impl HookToolName {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a hook tool name with compatibility aliases used for matcher
|
||||
/// selection only.
|
||||
pub(crate) fn new_with_aliases(name: impl Into<String>, matcher_aliases: Vec<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
matcher_aliases,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hook identity for file edits performed through `apply_patch`.
|
||||
///
|
||||
/// The serialized name remains `apply_patch` so logs and policies can key
|
||||
|
||||
@@ -17,7 +17,10 @@ pub(crate) mod spec_plan_types;
|
||||
pub(crate) mod tool_dispatch_trace;
|
||||
pub(crate) mod tool_search_entry;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
@@ -30,6 +33,21 @@ pub(crate) const TELEMETRY_PREVIEW_MAX_LINES: usize = 64; // lines
|
||||
pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str =
|
||||
"[... telemetry preview truncated ...]";
|
||||
|
||||
/// Legacy boundaries such as telemetry tags and Responses tool names still
|
||||
/// require a single flattened string. Keep comparisons and sorting on
|
||||
/// `ToolName` itself; use this only when crossing those boundaries.
|
||||
pub(crate) fn flat_tool_name(tool_name: &ToolName) -> Cow<'_, str> {
|
||||
match tool_name.namespace.as_deref() {
|
||||
Some(namespace) => {
|
||||
let mut name = String::with_capacity(namespace.len() + tool_name.name.len());
|
||||
name.push_str(namespace);
|
||||
name.push_str(&tool_name.name);
|
||||
Cow::Owned(name)
|
||||
}
|
||||
None => Cow::Borrowed(tool_name.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the combined exec output for sending back to the model.
|
||||
/// Includes exit code and duration metadata; truncates large bodies safely.
|
||||
pub fn format_exec_output_for_model_structured(
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
use crate::network_policy_decision::network_approval_context_from_payload;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::network_approval::ActiveNetworkApproval;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
@@ -135,7 +136,7 @@ impl ToolOrchestrator {
|
||||
T: ToolRuntime<Rq, Out>,
|
||||
{
|
||||
let otel = turn_ctx.session_telemetry.clone();
|
||||
let otel_tn = &tool_ctx.tool_name;
|
||||
let otel_tn = flat_tool_name(&tool_ctx.tool_name).into_owned();
|
||||
let otel_ci = &tool_ctx.call_id;
|
||||
let strict_auto_review = tool_ctx.session.strict_auto_review_enabled_for_turn().await;
|
||||
let use_guardian = routes_approval_to_guardian(turn_ctx) || strict_auto_review;
|
||||
@@ -175,7 +176,7 @@ impl ToolOrchestrator {
|
||||
already_approved = true;
|
||||
} else {
|
||||
otel.tool_decision(
|
||||
otel_tn,
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
&ReviewDecision::Approved,
|
||||
ToolDecisionSource::Config,
|
||||
@@ -398,6 +399,7 @@ impl ToolOrchestrator {
|
||||
if evaluate_permission_request_hooks
|
||||
&& let Some(permission_request) = tool.permission_request_payload(req)
|
||||
{
|
||||
let tool_name = flat_tool_name(&tool_ctx.tool_name);
|
||||
match run_permission_request_hooks(
|
||||
approval_ctx.session,
|
||||
approval_ctx.turn,
|
||||
@@ -409,7 +411,7 @@ impl ToolOrchestrator {
|
||||
Some(PermissionRequestDecision::Allow) => {
|
||||
let decision = ReviewDecision::Approved;
|
||||
otel.tool_decision(
|
||||
&tool_ctx.tool_name,
|
||||
tool_name.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
&decision,
|
||||
ToolDecisionSource::Config,
|
||||
@@ -419,7 +421,7 @@ impl ToolOrchestrator {
|
||||
Some(PermissionRequestDecision::Deny { message }) => {
|
||||
let decision = ReviewDecision::Denied;
|
||||
otel.tool_decision(
|
||||
&tool_ctx.tool_name,
|
||||
tool_name.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
&decision,
|
||||
ToolDecisionSource::Config,
|
||||
@@ -436,8 +438,9 @@ impl ToolOrchestrator {
|
||||
ToolDecisionSource::User
|
||||
};
|
||||
let decision = tool.start_approval_async(req, approval_ctx).await;
|
||||
let tool_name = flat_tool_name(&tool_ctx.tool_name);
|
||||
otel.tool_decision(
|
||||
&tool_ctx.tool_name,
|
||||
tool_name.as_ref(),
|
||||
&tool_ctx.call_id,
|
||||
&decision,
|
||||
otel_source,
|
||||
|
||||
@@ -94,12 +94,11 @@ impl ToolCallRuntime {
|
||||
let lock = Arc::clone(&self.parallel_execution);
|
||||
let invocation_cancellation_token = cancellation_token.clone();
|
||||
let started = Instant::now();
|
||||
let display_name = call.tool_name.display();
|
||||
|
||||
let dispatch_span = trace_span!(
|
||||
"dispatch_tool_call_with_code_mode_result",
|
||||
otel.name = display_name.as_str(),
|
||||
tool_name = display_name.as_str(),
|
||||
otel.name = %call.tool_name,
|
||||
tool_name = %call.tool_name,
|
||||
call_id = call.call_id.as_str(),
|
||||
aborted = false,
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
|
||||
use codex_hooks::HookEvent;
|
||||
@@ -263,7 +264,7 @@ impl ToolRegistry {
|
||||
invocation: ToolInvocation,
|
||||
) -> Result<AnyToolResult, FunctionCallError> {
|
||||
let tool_name = invocation.tool_name.clone();
|
||||
let display_name = tool_name.display();
|
||||
let tool_name_flat = flat_tool_name(&tool_name);
|
||||
let call_id_owned = invocation.call_id.clone();
|
||||
let otel = invocation.turn.session_telemetry.clone();
|
||||
let payload_for_response = invocation.payload.clone();
|
||||
@@ -316,7 +317,7 @@ impl ToolRegistry {
|
||||
None => {
|
||||
let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
|
||||
otel.tool_result_with_tags(
|
||||
&display_name,
|
||||
tool_name_flat.as_ref(),
|
||||
&call_id_owned,
|
||||
log_payload.as_ref(),
|
||||
Duration::ZERO,
|
||||
@@ -333,9 +334,9 @@ impl ToolRegistry {
|
||||
};
|
||||
|
||||
if !handler.matches_kind(&invocation.payload) {
|
||||
let message = format!("tool {display_name} invoked with incompatible payload");
|
||||
let message = format!("tool {tool_name} invoked with incompatible payload");
|
||||
otel.tool_result_with_tags(
|
||||
&display_name,
|
||||
tool_name_flat.as_ref(),
|
||||
&call_id_owned,
|
||||
log_payload.as_ref(),
|
||||
Duration::ZERO,
|
||||
@@ -372,7 +373,7 @@ impl ToolRegistry {
|
||||
let started = Instant::now();
|
||||
let result = otel
|
||||
.log_tool_result_with_tags(
|
||||
&display_name,
|
||||
tool_name_flat.as_ref(),
|
||||
&call_id_owned,
|
||||
log_payload.as_ref(),
|
||||
&metric_tags,
|
||||
@@ -540,10 +541,10 @@ impl ToolRegistryBuilder {
|
||||
H: ToolHandler + 'static,
|
||||
{
|
||||
let name = handler.tool_name();
|
||||
let display_name = name.display();
|
||||
let duplicate_name = name.clone();
|
||||
let handler: Arc<dyn AnyToolHandler> = handler;
|
||||
if self.handlers.insert(name, handler).is_some() {
|
||||
warn!("overwriting handler for tool {display_name}");
|
||||
warn!("overwriting handler for tool {duplicate_name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +555,6 @@ impl ToolRegistryBuilder {
|
||||
}
|
||||
|
||||
fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> String {
|
||||
let tool_name = tool_name.display();
|
||||
match payload {
|
||||
ToolPayload::Custom { .. } => format!("unsupported custom tool call: {tool_name}"),
|
||||
_ => format!("unsupported call: {tool_name}"),
|
||||
@@ -627,6 +627,10 @@ async fn dispatch_after_tool_use_hook(
|
||||
let session = invocation.session.as_ref();
|
||||
let turn = invocation.turn.as_ref();
|
||||
let tool_input = HookToolInput::from(&invocation.payload);
|
||||
let hook_tool_name = match &invocation.payload {
|
||||
ToolPayload::Mcp { tool, .. } => tool.clone(),
|
||||
_ => flat_tool_name(&invocation.tool_name).into_owned(),
|
||||
};
|
||||
let hook_outcomes = session
|
||||
.hooks()
|
||||
.dispatch(HookPayload {
|
||||
@@ -638,7 +642,7 @@ async fn dispatch_after_tool_use_hook(
|
||||
event: HookEventAfterToolUse {
|
||||
turn_id: turn.sub_id.clone(),
|
||||
call_id: invocation.call_id.clone(),
|
||||
tool_name: invocation.tool_name.display(),
|
||||
tool_name: hook_tool_name,
|
||||
tool_kind: hook_tool_kind(&tool_input),
|
||||
tool_input,
|
||||
executed: dispatch.executed,
|
||||
@@ -669,7 +673,7 @@ async fn dispatch_after_tool_use_hook(
|
||||
HookResult::FailedContinue(error) => {
|
||||
warn!(
|
||||
call_id = %invocation.call_id,
|
||||
tool_name = %invocation.tool_name.display(),
|
||||
tool_name = %invocation.tool_name,
|
||||
hook_name = %hook_name,
|
||||
error = %error,
|
||||
"after_tool_use hook failed; continuing"
|
||||
@@ -678,7 +682,7 @@ async fn dispatch_after_tool_use_hook(
|
||||
HookResult::FailedAbort(error) => {
|
||||
warn!(
|
||||
call_id = %invocation.call_id,
|
||||
tool_name = %invocation.tool_name.display(),
|
||||
tool_name = %invocation.tool_name,
|
||||
hook_name = %hook_name,
|
||||
error = %error,
|
||||
"after_tool_use hook failed; aborting operation"
|
||||
|
||||
@@ -21,7 +21,6 @@ use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolsConfig;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -44,8 +43,8 @@ pub struct ToolRouter {
|
||||
}
|
||||
|
||||
pub(crate) struct ToolRouterParams<'a> {
|
||||
pub(crate) mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) mcp_tools: Option<Vec<ToolInfo>>,
|
||||
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
||||
pub(crate) unavailable_called_tools: Vec<ToolName>,
|
||||
pub(crate) parallel_mcp_server_names: HashSet<String>,
|
||||
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -227,7 +228,7 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
mode: NetworkApprovalMode::Immediate,
|
||||
trigger: GuardianNetworkAccessTrigger {
|
||||
call_id: ctx.call_id.clone(),
|
||||
tool_name: ctx.tool_name.clone(),
|
||||
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
||||
command: req.command.clone(),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecServerEnvConfig;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -233,7 +234,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
mode: NetworkApprovalMode::Deferred,
|
||||
trigger: GuardianNetworkAccessTrigger {
|
||||
call_id: ctx.call_id.clone(),
|
||||
tool_name: ctx.tool_name.clone(),
|
||||
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
||||
command: req.command.clone(),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
|
||||
@@ -27,6 +27,7 @@ use codex_sandboxing::SandboxTransformError;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::Future;
|
||||
use futures::future::BoxFuture;
|
||||
@@ -344,7 +345,7 @@ pub(crate) struct ToolCtx {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
pub call_id: String,
|
||||
pub tool_name: String,
|
||||
pub tool_name: ToolName,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::flat_tool_name;
|
||||
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
|
||||
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
|
||||
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
||||
@@ -42,17 +43,17 @@ struct McpToolPlanInputs<'a> {
|
||||
tool_namespaces: HashMap<String, ToolNamespace>,
|
||||
}
|
||||
|
||||
fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanInputs<'_> {
|
||||
fn map_mcp_tools_for_plan(mcp_tools: &[ToolInfo]) -> McpToolPlanInputs<'_> {
|
||||
McpToolPlanInputs {
|
||||
mcp_tools: mcp_tools
|
||||
.values()
|
||||
.iter()
|
||||
.map(|tool| ToolRegistryPlanMcpTool {
|
||||
name: tool.canonical_tool_name(),
|
||||
tool: &tool.tool,
|
||||
})
|
||||
.collect(),
|
||||
tool_namespaces: mcp_tools
|
||||
.values()
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
(
|
||||
tool.callable_namespace.clone(),
|
||||
@@ -68,8 +69,8 @@ fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanI
|
||||
|
||||
pub(crate) fn build_specs_with_discoverable_tools(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<Vec<ToolInfo>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
||||
unavailable_called_tools: Vec<ToolName>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
@@ -114,10 +115,10 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
|
||||
|
||||
let mut builder = ToolRegistryBuilder::new();
|
||||
let mcp_tool_plan_inputs = mcp_tools.as_ref().map(map_mcp_tools_for_plan);
|
||||
let mcp_tool_plan_inputs = mcp_tools.as_deref().map(map_mcp_tools_for_plan);
|
||||
let deferred_mcp_tool_sources = deferred_mcp_tools.as_ref().map(|tools| {
|
||||
tools
|
||||
.values()
|
||||
.iter()
|
||||
.map(|tool| ToolRegistryPlanDeferredTool {
|
||||
name: tool.canonical_tool_name(),
|
||||
server_name: tool.server_name.as_str(),
|
||||
@@ -279,7 +280,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
ToolHandlerKind::ToolSearch => {
|
||||
let entries = build_tool_search_entries_for_config(
|
||||
config,
|
||||
deferred_mcp_tools.as_ref(),
|
||||
deferred_mcp_tools.as_deref(),
|
||||
&deferred_dynamic_tools,
|
||||
);
|
||||
builder.register_handler(Arc::new(ToolSearchHandler::new(entries)));
|
||||
@@ -305,17 +306,20 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
}
|
||||
}
|
||||
if let Some(deferred_mcp_tools) = deferred_mcp_tools.as_ref() {
|
||||
for (_, tool) in deferred_mcp_tools.iter().filter(|(name, _)| {
|
||||
!mcp_tools
|
||||
.as_ref()
|
||||
.is_some_and(|tools| tools.contains_key(*name))
|
||||
for tool in deferred_mcp_tools.iter().filter(|tool| {
|
||||
let tool_name = tool.canonical_tool_name();
|
||||
!mcp_tools.as_ref().is_some_and(|direct_tools| {
|
||||
direct_tools
|
||||
.iter()
|
||||
.any(|direct_tool| direct_tool.canonical_tool_name() == tool_name)
|
||||
})
|
||||
}) {
|
||||
builder.register_handler(Arc::new(McpHandler::new(tool.canonical_tool_name())));
|
||||
}
|
||||
}
|
||||
|
||||
for unavailable_tool in unavailable_called_tools {
|
||||
let tool_name = unavailable_tool.display();
|
||||
let tool_name = flat_tool_name(&unavailable_tool).into_owned();
|
||||
if existing_spec_names.insert(tool_name.clone()) {
|
||||
let spec = codex_tools::ToolSpec::Function(ResponsesApiTool {
|
||||
name: tool_name.clone(),
|
||||
|
||||
@@ -512,7 +512,7 @@ pub fn build_tool_registry_plan(
|
||||
|
||||
if let Some(mcp_tools) = params.mcp_tools {
|
||||
let mut entries = mcp_tools.to_vec();
|
||||
entries.sort_by_key(|tool| tool.name.display());
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut namespace_entries = BTreeMap::new();
|
||||
|
||||
for tool in entries {
|
||||
|
||||
@@ -266,8 +266,8 @@ async fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
||||
/// Builds the tool registry builder while collecting tool specs for later serialization.
|
||||
fn build_specs(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<Vec<ToolInfo>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
build_specs_with_unavailable_tools(
|
||||
@@ -281,8 +281,8 @@ fn build_specs(
|
||||
|
||||
fn build_specs_with_unavailable_tools(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<Vec<ToolInfo>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
||||
unavailable_called_tools: Vec<ToolName>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
@@ -630,7 +630,7 @@ async fn test_build_specs_default_shell_present() {
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::new()),
|
||||
Some(Vec::new()),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -856,7 +856,7 @@ async fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::new()),
|
||||
Some(Vec::new()),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -890,23 +890,20 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar_create_event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
Some(vec![ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar_create_event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
}]),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -940,55 +937,46 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
let (_, registry) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-create-event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_list_events".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-list-events",
|
||||
"List calendar events",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
),
|
||||
])),
|
||||
Some(vec![
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-create-event",
|
||||
"Create calendar event",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: "_list_events".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool(
|
||||
"calendar-list-events",
|
||||
"List calendar events",
|
||||
serde_json::json!({"type": "object"}),
|
||||
),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
namespace_description: None,
|
||||
tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
]),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
@@ -1018,14 +1006,11 @@ async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_dis
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
tools_config.namespace_tools = false;
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__test_server__echo".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
)),
|
||||
)]);
|
||||
let mcp_tools = vec![mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
))];
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
@@ -1077,14 +1062,11 @@ async fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
|
||||
let (_, registry) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"mcp__test_server__echo".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
)),
|
||||
)])),
|
||||
Some(vec![mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
))]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -1163,22 +1145,19 @@ async fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/search".to_string(),
|
||||
mcp_tool_info_with_display_name(
|
||||
"dash/search",
|
||||
mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
),
|
||||
Some(vec![mcp_tool_info_with_display_name(
|
||||
"dash/search",
|
||||
mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
)]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -1226,20 +1205,17 @@ async fn test_mcp_tool_preserves_integer_schema() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/paginate".to_string(),
|
||||
mcp_tool_info_with_display_name(
|
||||
"dash/paginate",
|
||||
mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
),
|
||||
Some(vec![mcp_tool_info_with_display_name(
|
||||
"dash/paginate",
|
||||
mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
)]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -1288,20 +1264,17 @@ async fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/tags".to_string(),
|
||||
mcp_tool_info_with_display_name(
|
||||
"dash/tags",
|
||||
mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
),
|
||||
Some(vec![mcp_tool_info_with_display_name(
|
||||
"dash/tags",
|
||||
mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
)]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -1352,22 +1325,19 @@ async fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/value".to_string(),
|
||||
mcp_tool_info_with_display_name(
|
||||
"dash/value",
|
||||
mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
),
|
||||
Some(vec![mcp_tool_info_with_display_name(
|
||||
"dash/value",
|
||||
mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
)]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
@@ -1420,39 +1390,36 @@ async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_tool_info_with_display_name(
|
||||
"test_server/do_something_cool",
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
Some(vec![mcp_tool_info_with_display_name(
|
||||
"test_server/do_something_cool",
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": {"type": "string"},
|
||||
"number_argument": {"type": "number"},
|
||||
"object_argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": {"type": "string"},
|
||||
"number_argument": {"type": "number"},
|
||||
"object_argument": {
|
||||
"string_property": {"type": "string"},
|
||||
"number_property": {"type": "number"}
|
||||
},
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_property": {"type": "string"},
|
||||
"number_property": {"type": "number"}
|
||||
"addtl_prop": {"type": "string"}
|
||||
},
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addtl_prop": {"type": "string"}
|
||||
},
|
||||
"required": ["addtl_prop"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
"required": ["addtl_prop"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
)]),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::tools::flat_tool_name;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::LoadableToolSpec;
|
||||
@@ -5,7 +6,6 @@ use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::dynamic_tool_to_loadable_tool_spec;
|
||||
use codex_tools::tool_search_result_source_to_loadable_tool_spec;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ToolSearchEntry {
|
||||
@@ -15,15 +15,15 @@ pub(crate) struct ToolSearchEntry {
|
||||
}
|
||||
|
||||
pub(crate) fn build_tool_search_entries(
|
||||
mcp_tools: Option<&HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<&[ToolInfo]>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> Vec<ToolSearchEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let mut mcp_tools = mcp_tools
|
||||
.map(|tools| tools.values().collect::<Vec<_>>())
|
||||
.map(|tools| tools.iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
mcp_tools.sort_by_key(|info| info.canonical_tool_name().display());
|
||||
mcp_tools.sort_by_key(|info| info.canonical_tool_name());
|
||||
for info in mcp_tools {
|
||||
match mcp_tool_search_entry(info) {
|
||||
Ok(entry) => entries.push(entry),
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn build_tool_search_entries(
|
||||
|
||||
pub(crate) fn build_tool_search_entries_for_config(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<&HashMap<String, ToolInfo>>,
|
||||
mcp_tools: Option<&[ToolInfo]>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> Vec<ToolSearchEntry> {
|
||||
let mcp_tools = if config.namespace_tools {
|
||||
@@ -95,8 +95,9 @@ fn dynamic_tool_search_entry(tool: &DynamicToolSpec) -> Result<ToolSearchEntry,
|
||||
}
|
||||
|
||||
fn build_mcp_search_text(info: &ToolInfo) -> String {
|
||||
let tool_name = info.canonical_tool_name();
|
||||
let mut parts = vec![
|
||||
info.canonical_tool_name().display(),
|
||||
flat_tool_name(&tool_name).into_owned(),
|
||||
info.callable_name.clone(),
|
||||
info.tool.name.to_string(),
|
||||
info.server_name.clone(),
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::tools::flat_tool_name;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_tools::ToolName;
|
||||
|
||||
pub(crate) fn collect_unavailable_called_tools(
|
||||
input: &[ResponseItem],
|
||||
exposed_tool_names: &HashSet<&str>,
|
||||
exposed_tool_names: &HashSet<ToolName>,
|
||||
) -> Vec<ToolName> {
|
||||
let mut unavailable_tools = BTreeMap::new();
|
||||
let exposed_display_names = exposed_tool_names
|
||||
.iter()
|
||||
.map(|name| flat_tool_name(name).into_owned())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for item in input {
|
||||
let ResponseItem::FunctionCall {
|
||||
@@ -25,13 +30,13 @@ pub(crate) fn collect_unavailable_called_tools(
|
||||
Some(namespace) => ToolName::namespaced(namespace.clone(), name.clone()),
|
||||
None => ToolName::plain(name.clone()),
|
||||
};
|
||||
let display_name = tool_name.display();
|
||||
if exposed_tool_names.contains(display_name.as_str()) {
|
||||
let display_name = flat_tool_name(&tool_name);
|
||||
if exposed_display_names.contains(display_name.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unavailable_tools
|
||||
.entry(display_name)
|
||||
.entry(tool_name.clone())
|
||||
.or_insert_with(|| tool_name);
|
||||
}
|
||||
|
||||
@@ -78,7 +83,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn collect_unavailable_called_tools_skips_currently_available_tools() {
|
||||
let exposed_tool_names = HashSet::from(["mcp__server__lookup", "mcp__server__search"]);
|
||||
let exposed_tool_names = HashSet::from([
|
||||
ToolName::plain("mcp__server__lookup"),
|
||||
ToolName::plain("mcp__server__search"),
|
||||
]);
|
||||
let input = vec![
|
||||
function_call("mcp__server__lookup", /*namespace*/ None),
|
||||
function_call("mcp__server__search", /*namespace*/ None),
|
||||
@@ -89,4 +97,17 @@ mod tests {
|
||||
|
||||
assert_eq!(tools, vec![ToolName::plain("mcp__server__missing")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_unavailable_called_tools_matches_exposed_display_names() {
|
||||
let exposed_tool_names = HashSet::from([ToolName::namespaced("mcp__server__", "lookup")]);
|
||||
let input = vec![function_call(
|
||||
"mcp__server__lookup",
|
||||
/*namespace*/ None,
|
||||
)];
|
||||
|
||||
let tools = collect_unavailable_called_tools(&input, &exposed_tool_names);
|
||||
|
||||
assert_eq!(tools, Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
|
||||
@@ -1040,7 +1041,7 @@ impl UnifiedExecProcessManager {
|
||||
session: context.session.clone(),
|
||||
turn: context.turn.clone(),
|
||||
call_id: context.call_id.clone(),
|
||||
tool_name: "exec_command".to_string(),
|
||||
tool_name: ToolName::plain("exec_command"),
|
||||
};
|
||||
orchestrator
|
||||
.run(
|
||||
|
||||
@@ -27,7 +27,7 @@ use serde_json::json;
|
||||
|
||||
const RMCP_SERVER: &str = "rmcp";
|
||||
const RMCP_NAMESPACE: &str = "mcp__rmcp__";
|
||||
const RMCP_ECHO_TOOL_NAME: &str = "mcp__rmcp__echo";
|
||||
const RMCP_ECHO_TOOL_NAME: &str = "echo";
|
||||
const RMCP_HOOK_MATCHER: &str = "mcp__rmcp__.*";
|
||||
const RMCP_ECHO_MESSAGE: &str = "hook e2e ping";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
/// Identifies a callable tool, preserving the namespace split when the model
|
||||
@@ -31,21 +32,31 @@ impl ToolName {
|
||||
namespace: Some(namespace.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
match &self.namespace {
|
||||
Some(namespace) => format!("{namespace}{}", self.name),
|
||||
None => self.name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ToolName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.namespace {
|
||||
Some(namespace) => write!(f, "{namespace}{}", self.name),
|
||||
None => f.write_str(&self.name),
|
||||
}
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ToolName {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let lhs = match &self.namespace {
|
||||
Some(namespace) => (namespace.as_str(), Some(self.name.as_str())),
|
||||
None => (self.name.as_str(), None),
|
||||
};
|
||||
let rhs = match &other.namespace {
|
||||
Some(namespace) => (namespace.as_str(), Some(other.name.as_str())),
|
||||
None => (other.name.as_str(), None),
|
||||
};
|
||||
lhs.cmp(&rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ToolName {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,3 +71,17 @@ impl From<&str> for ToolName {
|
||||
Self::plain(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn display_omits_namespace_prefix() {
|
||||
assert_eq!(
|
||||
ToolName::namespaced("mcp__server__", "lookup").to_string(),
|
||||
"lookup"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user