mirror of
https://github.com/openai/codex.git
synced 2026-05-03 10:56:37 +00:00
- [x] Expand tool search to custom MCPs.
- [x] Rename several variables/fields to be more generic.
Updated tool & server name lifecycles:
**Raw Identity**
ToolInfo.server_name is raw MCP server name.
ToolInfo.tool.name is raw MCP tool name.
MCP calls route back to raw via parse_tool_name() returning
(tool.server_name, tool.tool.name).
mcpServerStatus/list now groups by raw server and keys tools by
Tool.name: mod.rs:599
App-server just forwards that grouped raw snapshot:
codex_message_processor.rs:5245
**Callable Names**
On list-tools, we create provisional callable_namespace / callable_name:
mcp_connection_manager.rs:1556
For non-app MCP, provisional callable name starts as raw tool name.
For codex-apps, provisional callable name is sanitized and strips
connector name/id prefix; namespace includes connector name.
Then qualify_tools() sanitizes callable namespace + name to ASCII alnum
/ _ only: mcp_tool_names.rs:128
Note: this is stricter than Responses API. Hyphen is currently replaced
with _ for code-mode compatibility.
**Collision Handling**
We do initially collapse example-server and example_server to the same
base.
Then qualify_tools() detects distinct raw namespace identities behind
the same sanitized namespace and appends a hash to the callable
namespace: mcp_tool_names.rs:137
Same idea for tool-name collisions: hash suffix goes on callable tool
name.
Final list_all_tools() map key is callable_namespace + callable_name:
mcp_connection_manager.rs:769
**Direct Model Tools**
Direct MCP tool declarations use the full qualified sanitized key as the
Responses function name.
The raw rmcp Tool is converted but renamed for model exposure.
**Tool Search / Deferred**
Tool search result namespace = final ToolInfo.callable_namespace:
tool_search.rs:85
Tool search result nested name = final ToolInfo.callable_name:
tool_search.rs:86
Deferred tool handler is registered as "{namespace}:{name}":
tool_registry_plan.rs:248
When a function call comes back, core recombines namespace + name, looks
up the full qualified key, and gets the raw server/tool for MCP
execution: codex.rs:4353
**Separate Legacy Snapshot**
collect_mcp_snapshot_from_manager_with_detail() still returns a map
keyed by qualified callable name.
mcpServerStatus/list no longer uses that; it uses
McpServerStatusSnapshot, which is raw-inventory shaped.
2075 lines
72 KiB
Rust
2075 lines
72 KiB
Rust
use super::*;
|
|
use crate::AdditionalProperties;
|
|
use crate::ConfiguredToolSpec;
|
|
use crate::DiscoverablePluginInfo;
|
|
use crate::DiscoverableTool;
|
|
use crate::FreeformTool;
|
|
use crate::JsonSchema;
|
|
use crate::JsonSchemaPrimitiveType;
|
|
use crate::JsonSchemaType;
|
|
use crate::ResponsesApiTool;
|
|
use crate::ResponsesApiWebSearchFilters;
|
|
use crate::ResponsesApiWebSearchUserLocation;
|
|
use crate::ToolHandlerSpec;
|
|
use crate::ToolNamespace;
|
|
use crate::ToolRegistryPlanDeferredTool;
|
|
use crate::ToolsConfigParams;
|
|
use crate::WaitAgentTimeoutOptions;
|
|
use crate::mcp_call_tool_result_output_schema;
|
|
use codex_app_server_protocol::AppInfo;
|
|
use codex_features::Feature;
|
|
use codex_features::Features;
|
|
use codex_protocol::config_types::WebSearchConfig;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
|
use codex_protocol::openai_models::InputModality;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::WebSearchToolType;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
|
|
const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
|
const DEFAULT_AGENT_TYPE_DESCRIPTION: &str = "Test agent type description.";
|
|
const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
|
|
const MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
|
|
const MAX_WAIT_TIMEOUT_MS: i64 = 3_600_000;
|
|
|
|
#[test]
|
|
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let mut actual = BTreeMap::new();
|
|
let mut duplicate_names = Vec::new();
|
|
for tool in &tools {
|
|
let name = tool.name().to_string();
|
|
if actual.insert(name.clone(), tool.spec.clone()).is_some() {
|
|
duplicate_names.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicate_names.is_empty(),
|
|
"duplicate tool entries detected: {duplicate_names:?}"
|
|
);
|
|
|
|
let mut expected = BTreeMap::new();
|
|
for spec in [
|
|
create_exec_command_tool(CommandToolOptions {
|
|
allow_login_shell: true,
|
|
exec_permission_approvals_enabled: false,
|
|
}),
|
|
create_write_stdin_tool(),
|
|
create_update_plan_tool(),
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ false),
|
|
create_apply_patch_freeform_tool(),
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
},
|
|
create_view_image_tool(ViewImageToolOptions {
|
|
can_request_original_image_detail: config.can_request_original_image_detail,
|
|
}),
|
|
] {
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
let collab_specs = if config.multi_agent_v2 {
|
|
vec![
|
|
create_spawn_agent_tool_v2(spawn_agent_tool_options(&config)),
|
|
create_send_message_tool(),
|
|
create_wait_agent_tool_v2(wait_agent_timeout_options()),
|
|
create_close_agent_tool_v2(),
|
|
]
|
|
} else {
|
|
vec![
|
|
create_spawn_agent_tool_v1(spawn_agent_tool_options(&config)),
|
|
create_send_input_tool_v1(),
|
|
create_wait_agent_tool_v1(wait_agent_timeout_options()),
|
|
create_close_agent_tool_v1(),
|
|
]
|
|
};
|
|
for spec in collab_specs {
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
if !config.multi_agent_v2 {
|
|
let spec = create_resume_agent_tool();
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
|
|
if config.exec_permission_approvals_enabled {
|
|
let spec = create_request_permissions_tool(request_permissions_tool_description());
|
|
expected.insert(spec.name().to_string(), spec);
|
|
}
|
|
|
|
assert_eq!(
|
|
actual.keys().collect::<Vec<_>>(),
|
|
expected.keys().collect::<Vec<_>>(),
|
|
"tool name set mismatch"
|
|
);
|
|
|
|
for name in expected.keys() {
|
|
let mut actual_spec = actual.get(name).expect("present").clone();
|
|
let mut expected_spec = expected.get(name).expect("present").clone();
|
|
strip_descriptions_tool(&mut actual_spec);
|
|
strip_descriptions_tool(&mut expected_spec);
|
|
assert_eq!(actual_spec, expected_spec, "spec mismatch for {name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_collab_tools_enabled() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Collab);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&["spawn_agent", "send_input", "wait_agent", "close_agent"],
|
|
);
|
|
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
|
|
assert_lacks_tool_name(&tools, "list_agents");
|
|
|
|
let spawn_agent = find_tool(&tools, "spawn_agent");
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &spawn_agent.spec else {
|
|
panic!("spawn_agent should be a function tool");
|
|
};
|
|
let (properties, _) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("fork_context"));
|
|
assert!(!properties.contains_key("fork_turns"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Collab);
|
|
features.enable(Feature::MultiAgentV2);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_message",
|
|
"followup_task",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"list_agents",
|
|
],
|
|
);
|
|
|
|
let spawn_agent = find_tool(&tools, "spawn_agent");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &spawn_agent.spec
|
|
else {
|
|
panic!("spawn_agent should be a function tool");
|
|
};
|
|
let (properties, required) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("task_name"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(properties.contains_key("fork_turns"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert!(!properties.contains_key("fork_context"));
|
|
assert_eq!(
|
|
required,
|
|
Some(&vec!["task_name".to_string(), "message".to_string()])
|
|
);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("spawn_agent should define output schema");
|
|
assert_eq!(output_schema["required"], json!(["task_name", "nickname"]));
|
|
|
|
let send_message = find_tool(&tools, "send_message");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &send_message.spec
|
|
else {
|
|
panic!("send_message should be a function tool");
|
|
};
|
|
assert_eq!(output_schema, &None);
|
|
let (properties, required) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("target"));
|
|
assert!(!properties.contains_key("interrupt"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert_eq!(
|
|
required,
|
|
Some(&vec!["target".to_string(), "message".to_string()])
|
|
);
|
|
|
|
let followup_task = find_tool(&tools, "followup_task");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &followup_task.spec
|
|
else {
|
|
panic!("followup_task should be a function tool");
|
|
};
|
|
assert_eq!(output_schema, &None);
|
|
let (properties, required) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("target"));
|
|
assert!(properties.contains_key("message"));
|
|
assert!(!properties.contains_key("items"));
|
|
assert_eq!(
|
|
required,
|
|
Some(&vec!["target".to_string(), "message".to_string()])
|
|
);
|
|
|
|
let wait_agent = find_tool(&tools, "wait_agent");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &wait_agent.spec
|
|
else {
|
|
panic!("wait_agent should be a function tool");
|
|
};
|
|
let (properties, required) = expect_object_schema(parameters);
|
|
assert!(!properties.contains_key("targets"));
|
|
assert!(properties.contains_key("timeout_ms"));
|
|
assert_eq!(required, None);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("wait_agent should define output schema");
|
|
assert_eq!(
|
|
output_schema["properties"]["message"]["description"],
|
|
json!("Brief wait summary without the agent's final content.")
|
|
);
|
|
|
|
let list_agents = find_tool(&tools, "list_agents");
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
parameters,
|
|
output_schema,
|
|
..
|
|
}) = &list_agents.spec
|
|
else {
|
|
panic!("list_agents should be a function tool");
|
|
};
|
|
let (properties, required) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("path_prefix"));
|
|
assert_eq!(required, None);
|
|
let output_schema = output_schema
|
|
.as_ref()
|
|
.expect("list_agents should define output schema");
|
|
assert_eq!(
|
|
output_schema["properties"]["agents"]["items"]["required"],
|
|
json!(["agent_name", "agent_status", "last_task_message"])
|
|
);
|
|
assert_lacks_tool_name(&tools, "send_input");
|
|
assert_lacks_tool_name(&tools, "resume_agent");
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_omits_detail_without_original_detail_feature() {
|
|
let mut model_info = model_info();
|
|
model_info.supports_image_detail_original = true;
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let (properties, _) = expect_object_schema(parameters);
|
|
assert!(!properties.contains_key("detail"));
|
|
}
|
|
|
|
#[test]
|
|
fn view_image_tool_includes_detail_with_original_detail_feature() {
|
|
let mut model_info = model_info();
|
|
model_info.supports_image_detail_original = true;
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ImageDetailOriginal);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else {
|
|
panic!("view_image should be a function tool");
|
|
};
|
|
let (properties, _) = expect_object_schema(parameters);
|
|
assert!(properties.contains_key("detail"));
|
|
let description = expect_string_description(
|
|
properties
|
|
.get("detail")
|
|
.expect("view_image detail should include a description"),
|
|
);
|
|
assert!(description.contains("only supported value is `original`"));
|
|
assert!(description.contains("omit this field for default resized behavior"));
|
|
}
|
|
|
|
#[test]
|
|
fn disabled_environment_omits_environment_backed_tools() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
features.enable(Feature::JsRepl);
|
|
let available_models = Vec::new();
|
|
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
})
|
|
.with_has_environment(/*has_environment*/ false);
|
|
tools_config
|
|
.experimental_supported_tools
|
|
.push("list_dir".to_string());
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_lacks_tool_name(&tools, "exec_command");
|
|
assert_lacks_tool_name(&tools, "write_stdin");
|
|
assert_lacks_tool_name(&tools, "js_repl");
|
|
assert_lacks_tool_name(&tools, "js_repl_reset");
|
|
assert_lacks_tool_name(&tools, "apply_patch");
|
|
assert_lacks_tool_name(&tools, "list_dir");
|
|
assert_lacks_tool_name(&tools, VIEW_IMAGE_TOOL_NAME);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_agent_job_worker_tools_enabled() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::SpawnCsv);
|
|
features.normalize_dependencies();
|
|
features.enable(Feature::Sqlite);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::SubAgent(SubAgentSource::Other(
|
|
"agent_job:test".to_string(),
|
|
)),
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"spawn_agent",
|
|
"send_input",
|
|
"resume_agent",
|
|
"wait_agent",
|
|
"close_agent",
|
|
"spawn_agents_on_csv",
|
|
"report_agent_job_result",
|
|
REQUEST_USER_INPUT_TOOL_NAME,
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_user_input_description_reflects_default_mode_feature_flag() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ false)
|
|
);
|
|
|
|
features.enable(Feature::DefaultModeRequestUserInput);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
|
assert_eq!(
|
|
request_user_input_tool.spec,
|
|
request_user_input_tool_spec(/*default_mode_request_user_input*/ true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_requires_feature_flag() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::RequestPermissionsTool);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let request_permissions_tool = find_tool(&tools, "request_permissions");
|
|
assert_eq!(
|
|
request_permissions_tool.spec,
|
|
create_request_permissions_tool(request_permissions_tool_description())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_is_independent_from_additional_permissions() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ExecPermissionApprovals);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_lacks_tool_name(&tools, "request_permissions");
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_requires_feature_flag() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl"),
|
|
"js_repl should be disabled when the feature is off"
|
|
);
|
|
assert!(
|
|
!tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"),
|
|
"js_repl_reset should be disabled when the feature is off"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_enabled_adds_tools() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::JsRepl);
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
|
|
}
|
|
|
|
#[test]
|
|
fn image_generation_tools_require_feature_and_supported_model() {
|
|
let supported_model_info = model_info();
|
|
let mut unsupported_model_info = supported_model_info.clone();
|
|
unsupported_model_info.input_modalities = vec![InputModality::Text];
|
|
let mut image_generation_disabled_features = Features::with_defaults();
|
|
image_generation_disabled_features.disable(Feature::ImageGeneration);
|
|
let mut image_generation_features = Features::with_defaults();
|
|
image_generation_features.enable(Feature::ImageGeneration);
|
|
|
|
let available_models = Vec::new();
|
|
let default_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_disabled_features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (default_tools, _) = build_specs(
|
|
&default_tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
assert!(
|
|
!default_tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled when the feature is disabled"
|
|
);
|
|
|
|
let supported_tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &supported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (supported_tools, _) = build_specs(
|
|
&supported_tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
assert_contains_tool_names(&supported_tools, &["image_generation"]);
|
|
let image_generation_tool = find_tool(&supported_tools, "image_generation");
|
|
assert_eq!(
|
|
serde_json::to_value(&image_generation_tool.spec).expect("serialize image tool"),
|
|
serde_json::json!({
|
|
"type": "image_generation",
|
|
"output_format": "png"
|
|
})
|
|
);
|
|
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &unsupported_model_info,
|
|
available_models: &available_models,
|
|
features: &image_generation_features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool.spec.name() == "image_generation"),
|
|
"image_generation should be disabled for unsupported models"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_cached_sets_external_web_access_false() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(false),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_mode_live_sets_external_web_access_true() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_config_is_forwarded_to_tool_spec() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let web_search_config = WebSearchConfig {
|
|
filters: Some(codex_protocol::config_types::WebSearchFilters {
|
|
allowed_domains: Some(vec!["example.com".to_string()]),
|
|
}),
|
|
user_location: Some(codex_protocol::config_types::WebSearchUserLocation {
|
|
r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate,
|
|
country: Some("US".to_string()),
|
|
region: Some("California".to_string()),
|
|
city: Some("San Francisco".to_string()),
|
|
timezone: Some("America/Los_Angeles".to_string()),
|
|
}),
|
|
search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High),
|
|
};
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
})
|
|
.with_web_search_config(Some(web_search_config.clone()));
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: web_search_config
|
|
.filters
|
|
.map(ResponsesApiWebSearchFilters::from),
|
|
user_location: web_search_config
|
|
.user_location
|
|
.map(ResponsesApiWebSearchUserLocation::from),
|
|
search_context_size: web_search_config.search_context_size,
|
|
search_content_types: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn web_search_tool_type_text_and_image_sets_search_content_types() {
|
|
let mut model_info = model_info();
|
|
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
|
|
let features = Features::with_defaults();
|
|
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "web_search");
|
|
assert_eq!(
|
|
tool.spec,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(true),
|
|
filters: None,
|
|
user_location: None,
|
|
search_context_size: None,
|
|
search_content_types: Some(vec!["text".to_string(), "image".to_string()]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_hidden_without_mcp_servers() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools.iter().any(|tool| matches!(
|
|
tool.spec.name(),
|
|
"list_mcp_resources" | "list_mcp_resource_templates" | "read_mcp_resource"
|
|
)),
|
|
"MCP resource tools should be omitted when no MCP servers are configured"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_resource_tools_are_included_when_mcp_servers_are_present() {
|
|
let model_info = model_info();
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::new()),
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(
|
|
&tools,
|
|
&[
|
|
"list_mcp_resources",
|
|
"list_mcp_resource_templates",
|
|
"read_mcp_resource",
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_parallel_support_flags() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls);
|
|
assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls);
|
|
}
|
|
|
|
#[test]
|
|
fn test_test_model_info_includes_sync_tool() {
|
|
let mut model_info = model_info();
|
|
model_info.experimental_supported_tools = vec!["test_sync_tool".to_string()];
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_converted() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Live),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"test_server/do_something_cool".to_string(),
|
|
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_property": { "type": "string" },
|
|
"number_property": { "type": "number" },
|
|
},
|
|
"required": ["string_property", "number_property"],
|
|
"additionalProperties": false,
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
)])),
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let tool = find_tool(&tools, "test_server/do_something_cool");
|
|
assert_eq!(
|
|
&tool.spec,
|
|
&ToolSpec::Function(ResponsesApiTool {
|
|
name: "test_server/do_something_cool".to_string(),
|
|
parameters: JsonSchema::object(
|
|
BTreeMap::from([
|
|
(
|
|
"string_argument".to_string(),
|
|
JsonSchema::string(/*description*/ None),
|
|
),
|
|
(
|
|
"number_argument".to_string(),
|
|
JsonSchema::number(/*description*/ None),
|
|
),
|
|
(
|
|
"object_argument".to_string(),
|
|
JsonSchema::object(
|
|
BTreeMap::from([
|
|
(
|
|
"string_property".to_string(),
|
|
JsonSchema::string(/*description*/ None),
|
|
),
|
|
(
|
|
"number_property".to_string(),
|
|
JsonSchema::number(/*description*/ None),
|
|
),
|
|
]),
|
|
Some(vec![
|
|
"string_property".to_string(),
|
|
"number_property".to_string(),
|
|
]),
|
|
Some(false.into()),
|
|
),
|
|
),
|
|
]),
|
|
/*required*/ None,
|
|
/*additional_properties*/ None
|
|
),
|
|
description: "Do something cool".to_string(),
|
|
strict: false,
|
|
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
|
|
defer_loading: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_specs_mcp_tools_sorted_by_name() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let tools_map = HashMap::from([
|
|
(
|
|
"test_server/do".to_string(),
|
|
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/something".to_string(),
|
|
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
|
),
|
|
(
|
|
"test_server/cool".to_string(),
|
|
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
|
),
|
|
]);
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(tools_map),
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let mcp_names: Vec<_> = tools
|
|
.iter()
|
|
.map(|tool| tool.name().to_string())
|
|
.filter(|name| name.starts_with("test_server/"))
|
|
.collect();
|
|
let expected = vec![
|
|
"test_server/cool".to_string(),
|
|
"test_server/do".to_string(),
|
|
"test_server/something".to_string(),
|
|
];
|
|
assert_eq!(mcp_names, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_description_lists_each_mcp_source_once() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::ToolSearch);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, handlers) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([
|
|
(
|
|
"mcp__codex_apps__calendar_create_event".to_string(),
|
|
mcp_tool(
|
|
"calendar_create_event",
|
|
"Create calendar event",
|
|
serde_json::json!({"type": "object"}),
|
|
),
|
|
),
|
|
(
|
|
"mcp__rmcp__echo".to_string(),
|
|
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
|
),
|
|
])),
|
|
Some(vec![
|
|
deferred_mcp_tool(
|
|
"_create_event",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
Some("Plan events and manage your calendar."),
|
|
),
|
|
deferred_mcp_tool(
|
|
"_list_events",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
Some("Plan events and manage your calendar."),
|
|
),
|
|
deferred_mcp_tool(
|
|
"_search_threads",
|
|
"mcp__codex_apps__gmail",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Gmail"),
|
|
Some("Find and summarize email threads."),
|
|
),
|
|
deferred_mcp_tool(
|
|
"echo",
|
|
"mcp__rmcp__",
|
|
"rmcp",
|
|
/*connector_name*/ None,
|
|
/*connector_description*/ None,
|
|
),
|
|
]),
|
|
&[],
|
|
);
|
|
|
|
let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else {
|
|
panic!("expected tool_search tool");
|
|
};
|
|
let description = description.as_str();
|
|
assert!(description.contains("- Calendar: Plan events and manage your calendar."));
|
|
assert!(description.contains("- Gmail: Find and summarize email threads."));
|
|
assert_eq!(
|
|
description
|
|
.matches("- Calendar: Plan events and manage your calendar.")
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(description.contains("- rmcp"));
|
|
assert!(!description.contains("mcp__rmcp__echo"));
|
|
|
|
assert!(handlers.contains(&ToolHandlerSpec {
|
|
name: "mcp__codex_apps__calendar:_create_event".to_string(),
|
|
kind: ToolHandlerKind::Mcp,
|
|
}));
|
|
assert!(handlers.contains(&ToolHandlerSpec {
|
|
name: "mcp__rmcp__:echo".to_string(),
|
|
kind: ToolHandlerKind::Mcp,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn search_tool_requires_model_capability_and_feature_flag() {
|
|
let model_info = search_capable_model_info();
|
|
let deferred_mcp_tools = Some(vec![deferred_mcp_tool(
|
|
"_create_event",
|
|
"mcp__codex_apps__calendar",
|
|
CODEX_APPS_MCP_SERVER_NAME,
|
|
Some("Calendar"),
|
|
/*connector_description*/ None,
|
|
)]);
|
|
|
|
let features = Features::with_defaults();
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &ModelInfo {
|
|
supports_search_tool: false,
|
|
..model_info.clone()
|
|
},
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
deferred_mcp_tools.clone(),
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
deferred_mcp_tools.clone(),
|
|
&[],
|
|
);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ToolSearch);
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
deferred_mcp_tools,
|
|
&[],
|
|
);
|
|
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_is_not_registered_without_feature_flag() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ToolSearch);
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.disable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
Some(vec![discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
)]),
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
!tools
|
|
.iter()
|
|
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_can_be_registered_without_search_tool() {
|
|
let model_info = ModelInfo {
|
|
supports_search_tool: false,
|
|
..search_capable_model_info()
|
|
};
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.enable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
Some(vec![discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
)]),
|
|
&[],
|
|
);
|
|
|
|
assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]);
|
|
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
|
|
|
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert!(description.contains(
|
|
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
|
));
|
|
assert!(description.contains(
|
|
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_description_lists_discoverable_tools() {
|
|
let model_info = search_capable_model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::Apps);
|
|
features.enable(Feature::Plugins);
|
|
features.enable(Feature::ToolSearch);
|
|
features.enable(Feature::ToolSuggest);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let discoverable_tools = vec![
|
|
discoverable_connector(
|
|
"connector_2128aebfecb84f64a069897515042a44",
|
|
"Google Calendar",
|
|
"Plan events and schedules.",
|
|
),
|
|
discoverable_connector(
|
|
"connector_68df038e0ba48191908c8434991bbac2",
|
|
"Gmail",
|
|
"Find and summarize email threads.",
|
|
),
|
|
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
|
id: "sample@test".to_string(),
|
|
name: "Sample Plugin".to_string(),
|
|
description: None,
|
|
has_skills: true,
|
|
mcp_server_names: vec!["sample-docs".to_string()],
|
|
app_connector_ids: vec!["connector_sample".to_string()],
|
|
})),
|
|
];
|
|
|
|
let (tools, _) = build_specs_with_discoverable_tools(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
Some(discoverable_tools),
|
|
&[],
|
|
);
|
|
|
|
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
|
let ToolSpec::Function(ResponsesApiTool {
|
|
description,
|
|
parameters,
|
|
..
|
|
}) = &tool_suggest.spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
assert!(description.contains(
|
|
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
|
));
|
|
assert!(description.contains("Google Calendar"));
|
|
assert!(description.contains("Gmail"));
|
|
assert!(description.contains("Sample Plugin"));
|
|
assert!(description.contains("Plan events and schedules."));
|
|
assert!(description.contains("Find and summarize email threads."));
|
|
assert!(description.contains("id: `sample@test`, type: plugin, action: install"));
|
|
assert!(description.contains("`action_type`: `install` or `enable`"));
|
|
assert!(
|
|
description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample")
|
|
);
|
|
assert!(
|
|
description.contains(
|
|
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
|
)
|
|
);
|
|
assert!(description.contains(
|
|
"For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely."
|
|
));
|
|
assert!(description.contains(
|
|
"For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself."
|
|
));
|
|
assert!(description.contains(
|
|
"Do not suggest a plugin just because one of its connectors or capabilities seems relevant."
|
|
));
|
|
assert!(description.contains(
|
|
"Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard."
|
|
));
|
|
assert!(description.contains("DO NOT explore or recommend tools that are not on this list."));
|
|
assert!(!description.contains("{{discoverable_tools}}"));
|
|
assert!(!description.contains("tool_search fails to find a good match"));
|
|
let (_, required) = expect_object_schema(parameters);
|
|
assert_eq!(
|
|
required,
|
|
Some(&vec![
|
|
"tool_type".to_string(),
|
|
"action_type".to_string(),
|
|
"tool_id".to_string(),
|
|
"suggest_reason".to_string(),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::CodeModeOnly);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"mcp__sample__echo".to_string(),
|
|
mcp_tool(
|
|
"echo",
|
|
"Echo text",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {"type": "string"}
|
|
},
|
|
"required": ["message"],
|
|
"additionalProperties": false
|
|
}),
|
|
),
|
|
)])),
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, "mcp__sample__echo").spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
r#"Echo text
|
|
|
|
exec tool declaration:
|
|
```ts
|
|
declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
|
|
```"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
Some(HashMap::from([(
|
|
"mcp__sample__fn".to_string(),
|
|
mcp_tool(
|
|
"fn",
|
|
"Sample fn",
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"open": {
|
|
"anyOf": [
|
|
{
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"ref_id": {"type": "string"},
|
|
"lineno": {"anyOf": [{"type": "integer"}, {"type": "null"}]}
|
|
},
|
|
"required": ["ref_id"],
|
|
"additionalProperties": false
|
|
}
|
|
},
|
|
{"type": "null"}
|
|
]
|
|
},
|
|
"tagged_list": {
|
|
"anyOf": [
|
|
{
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"kind": {"type": "const", "const": "tagged"},
|
|
"variant": {"type": "enum", "enum": ["alpha", "beta"]},
|
|
"scope": {"type": "enum", "enum": ["one", "two"]}
|
|
},
|
|
"required": ["kind", "variant", "scope"]
|
|
}
|
|
},
|
|
{"type": "null"}
|
|
]
|
|
},
|
|
"response_length": {"type": "enum", "enum": ["short", "medium", "long"]}
|
|
},
|
|
"additionalProperties": false
|
|
}),
|
|
),
|
|
)])),
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, "mcp__sample__fn").spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert!(description.contains(
|
|
r#"exec tool declaration:
|
|
```ts
|
|
declare const tools: { mcp__sample__fn(args: { open?: Array<{ lineno?: number | null; ref_id: string; }> | null; response_length?: "short" | "medium" | "long"; tagged_list?: Array<{ kind: "tagged"; scope: "one" | "two"; variant: "alpha" | "beta"; }> | null; }): Promise<{ _meta?: unknown; content: Array<unknown>; isError?: boolean; structuredContent?: unknown; }>; };
|
|
```"#
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::UnifiedExec);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
|
|
&find_tool(&tools, VIEW_IMAGE_TOOL_NAME).spec
|
|
else {
|
|
panic!("expected function tool");
|
|
};
|
|
|
|
assert_eq!(
|
|
description,
|
|
"View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `original` when original resolution is preserved, otherwise `null`.\n detail: string | null;\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_exec_description_includes_full_nested_tool_details() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
features.enable(Feature::CodeModeOnly);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.contains("Enabled nested tools:"));
|
|
assert!(!description.contains("Nested tool reference:"));
|
|
assert!(description.starts_with(
|
|
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() {
|
|
let model_info = model_info();
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeMode);
|
|
let available_models = Vec::new();
|
|
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
|
model_info: &model_info,
|
|
available_models: &available_models,
|
|
features: &features,
|
|
image_generation_tool_auth_allowed: true,
|
|
web_search_mode: Some(WebSearchMode::Cached),
|
|
session_source: SessionSource::Cli,
|
|
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
});
|
|
|
|
let (tools, _) = build_specs(
|
|
&tools_config,
|
|
/*mcp_tools*/ None,
|
|
/*deferred_mcp_tools*/ None,
|
|
&[],
|
|
);
|
|
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
|
|
else {
|
|
panic!("expected freeform tool");
|
|
};
|
|
|
|
assert!(!description.starts_with(
|
|
"Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"
|
|
));
|
|
assert!(!description.contains("### `update_plan` (`update_plan`)"));
|
|
assert!(!description.contains("### `view_image` (`view_image`)"));
|
|
}
|
|
|
|
fn model_info() -> ModelInfo {
|
|
serde_json::from_value(json!({
|
|
"slug": "gpt-5-codex",
|
|
"display_name": "GPT-5 Codex",
|
|
"description": null,
|
|
"supported_reasoning_levels": [],
|
|
"shell_type": "shell_command",
|
|
"visibility": "list",
|
|
"supported_in_api": true,
|
|
"priority": 1,
|
|
"availability_nux": null,
|
|
"upgrade": null,
|
|
"base_instructions": "base",
|
|
"model_messages": null,
|
|
"supports_reasoning_summaries": false,
|
|
"default_reasoning_summary": "auto",
|
|
"support_verbosity": false,
|
|
"default_verbosity": null,
|
|
"apply_patch_tool_type": "freeform",
|
|
"truncation_policy": {
|
|
"mode": "bytes",
|
|
"limit": 10000
|
|
},
|
|
"supports_parallel_tool_calls": false,
|
|
"supports_image_detail_original": false,
|
|
"context_window": null,
|
|
"auto_compact_token_limit": null,
|
|
"effective_context_window_percent": 95,
|
|
"experimental_supported_tools": [],
|
|
"input_modalities": ["text", "image"],
|
|
"supports_search_tool": false
|
|
}))
|
|
.expect("deserialize test model")
|
|
}
|
|
|
|
fn search_capable_model_info() -> ModelInfo {
|
|
ModelInfo {
|
|
supports_search_tool: true,
|
|
..model_info()
|
|
}
|
|
}
|
|
|
|
fn build_specs<'a>(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
|
build_specs_with_discoverable_tools(
|
|
config,
|
|
mcp_tools,
|
|
deferred_mcp_tools,
|
|
/*discoverable_tools*/ None,
|
|
dynamic_tools,
|
|
)
|
|
}
|
|
|
|
fn build_specs_with_discoverable_tools<'a>(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
|
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
|
build_specs_with_optional_tool_namespaces(
|
|
config,
|
|
mcp_tools,
|
|
deferred_mcp_tools,
|
|
/*tool_namespaces*/ None,
|
|
discoverable_tools,
|
|
dynamic_tools,
|
|
)
|
|
}
|
|
|
|
fn build_specs_with_optional_tool_namespaces<'a>(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
|
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
|
|
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
|
let plan = build_tool_registry_plan(
|
|
config,
|
|
ToolRegistryPlanParams {
|
|
mcp_tools: mcp_tools.as_ref(),
|
|
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
|
|
tool_namespaces: tool_namespaces.as_ref(),
|
|
discoverable_tools: discoverable_tools.as_deref(),
|
|
dynamic_tools,
|
|
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
|
|
wait_agent_timeouts: wait_agent_timeout_options(),
|
|
},
|
|
);
|
|
(plan.specs, plan.handlers)
|
|
}
|
|
|
|
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
|
rmcp::model::Tool {
|
|
name: name.to_string().into(),
|
|
title: None,
|
|
description: Some(description.to_string().into()),
|
|
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
|
output_schema: None,
|
|
annotations: None,
|
|
execution: None,
|
|
icons: None,
|
|
meta: None,
|
|
}
|
|
}
|
|
|
|
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
|
let slug = name.replace(' ', "-").to_lowercase();
|
|
DiscoverableTool::Connector(Box::new(AppInfo {
|
|
id: id.to_string(),
|
|
name: name.to_string(),
|
|
description: Some(description.to_string()),
|
|
logo_url: None,
|
|
logo_url_dark: None,
|
|
distribution_channel: None,
|
|
branding: None,
|
|
app_metadata: None,
|
|
labels: None,
|
|
install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")),
|
|
is_accessible: false,
|
|
is_enabled: true,
|
|
plugin_display_names: Vec::new(),
|
|
}))
|
|
}
|
|
|
|
fn deferred_mcp_tool<'a>(
|
|
tool_name: &'a str,
|
|
tool_namespace: &'a str,
|
|
server_name: &'a str,
|
|
connector_name: Option<&'a str>,
|
|
connector_description: Option<&'a str>,
|
|
) -> ToolRegistryPlanDeferredTool<'a> {
|
|
ToolRegistryPlanDeferredTool {
|
|
tool_name,
|
|
tool_namespace,
|
|
server_name,
|
|
connector_name,
|
|
connector_description,
|
|
}
|
|
}
|
|
|
|
fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) {
|
|
use std::collections::HashSet;
|
|
|
|
let mut names = HashSet::new();
|
|
let mut duplicates = Vec::new();
|
|
for name in tools.iter().map(ConfiguredToolSpec::name) {
|
|
if !names.insert(name) {
|
|
duplicates.push(name);
|
|
}
|
|
}
|
|
assert!(
|
|
duplicates.is_empty(),
|
|
"duplicate tool entries detected: {duplicates:?}"
|
|
);
|
|
for expected in expected_subset {
|
|
assert!(
|
|
names.contains(expected),
|
|
"expected tool {expected} to be present; had: {names:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
|
let names = tools
|
|
.iter()
|
|
.map(ConfiguredToolSpec::name)
|
|
.collect::<Vec<_>>();
|
|
assert!(
|
|
!names.contains(&expected_absent),
|
|
"expected tool {expected_absent} to be absent; had: {names:?}"
|
|
);
|
|
}
|
|
|
|
fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec {
|
|
create_request_user_input_tool(request_user_input_tool_description(
|
|
default_mode_request_user_input,
|
|
))
|
|
}
|
|
|
|
fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> {
|
|
SpawnAgentToolOptions {
|
|
available_models: &config.available_models,
|
|
agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION),
|
|
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
|
|
include_usage_hint: config.spawn_agent_usage_hint,
|
|
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
|
|
}
|
|
}
|
|
|
|
fn wait_agent_timeout_options() -> WaitAgentTimeoutOptions {
|
|
WaitAgentTimeoutOptions {
|
|
default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS,
|
|
min_timeout_ms: MIN_WAIT_TIMEOUT_MS,
|
|
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
|
|
}
|
|
}
|
|
|
|
fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec {
|
|
tools
|
|
.iter()
|
|
.find(|tool| tool.name() == expected_name)
|
|
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
|
|
}
|
|
|
|
fn expect_object_schema(
|
|
schema: &JsonSchema,
|
|
) -> (&BTreeMap<String, JsonSchema>, Option<&Vec<String>>) {
|
|
assert_eq!(
|
|
schema.schema_type,
|
|
Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))
|
|
);
|
|
let properties = schema
|
|
.properties
|
|
.as_ref()
|
|
.expect("expected object properties");
|
|
(properties, schema.required.as_ref())
|
|
}
|
|
|
|
fn expect_string_description(schema: &JsonSchema) -> &str {
|
|
assert_eq!(
|
|
schema.schema_type,
|
|
Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::String))
|
|
);
|
|
schema.description.as_deref().expect("expected description")
|
|
}
|
|
|
|
fn strip_descriptions_schema(schema: &mut JsonSchema) {
|
|
if let Some(variants) = &mut schema.any_of {
|
|
for variant in variants {
|
|
strip_descriptions_schema(variant);
|
|
}
|
|
}
|
|
if let Some(items) = &mut schema.items {
|
|
strip_descriptions_schema(items);
|
|
}
|
|
if let Some(properties) = &mut schema.properties {
|
|
for value in properties.values_mut() {
|
|
strip_descriptions_schema(value);
|
|
}
|
|
}
|
|
if let Some(AdditionalProperties::Schema(schema)) = &mut schema.additional_properties {
|
|
strip_descriptions_schema(schema);
|
|
}
|
|
schema.description = None;
|
|
}
|
|
|
|
fn strip_descriptions_tool(spec: &mut ToolSpec) {
|
|
match spec {
|
|
ToolSpec::ToolSearch { parameters, .. } => strip_descriptions_schema(parameters),
|
|
ToolSpec::Function(ResponsesApiTool { parameters, .. }) => {
|
|
strip_descriptions_schema(parameters);
|
|
}
|
|
ToolSpec::Freeform(FreeformTool { .. })
|
|
| ToolSpec::LocalShell {}
|
|
| ToolSpec::ImageGeneration { .. }
|
|
| ToolSpec::WebSearch { .. } => {}
|
|
}
|
|
}
|