diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 044c445046..6d6a3f2368 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -2344,6 +2344,94 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only( assert!(!description.contains("### `view_image`")); } +#[test] +fn files_namespace_is_gated_by_experimental_supported_tool() { + let available_models = Vec::new(); + + let default_model_info = model_info(); + let default_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &default_model_info, + available_models: &available_models, + features: &Features::with_defaults(), + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (default_tools, default_registry) = build_specs( + &default_config, + /*mcp_tools*/ None, + /*deferred_mcp_tools*/ None, + &[], + ); + + assert!(!default_tools.iter().any(|tool| tool.name() == "files")); + assert!(!default_registry.has_handler(&ToolName::namespaced("files", "materialize"))); + + let mut supported_model_info = model_info(); + supported_model_info.experimental_supported_tools = vec!["code_mode_files".to_string()]; + let supported_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &supported_model_info, + available_models: &available_models, + features: &Features::with_defaults(), + image_generation_tool_auth_allowed: true, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + permission_profile: &PermissionProfile::Disabled, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, registry) = build_specs( + &supported_config, + /*mcp_tools*/ None, + /*deferred_mcp_tools*/ None, + &[], + ); + + assert_eq!( + namespace_function_names(&tools, "files"), + vec!["copy", "export_for_tool", "materialize"] + ); + assert!(registry.has_handler(&ToolName::namespaced("files", "materialize"))); + assert!(registry.has_handler(&ToolName::namespaced("files", "copy"))); + assert!(registry.has_handler(&ToolName::namespaced("files", "export_for_tool"))); +} + +#[test] +fn code_mode_exec_description_includes_gated_files_tools() { + let mut model_info = model_info(); + model_info.experimental_supported_tools = vec!["code_mode_files".to_string()]; + 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, + permission_profile: &PermissionProfile::Disabled, + 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") else { + panic!("expected freeform tool"); + }; + + assert!(description.contains("### `files_copy`")); + assert!(description.contains("### `files_export_for_tool`")); + assert!(description.contains("### `files_materialize`")); + assert!(description.contains("data URI")); +} + fn model_info() -> ModelInfo { serde_json::from_value(json!({ "slug": "gpt-5-codex", diff --git a/codex-rs/tools/src/code_mode_tests.rs b/codex-rs/tools/src/code_mode_tests.rs index c4c4c7ce26..60154b46d2 100644 --- a/codex-rs/tools/src/code_mode_tests.rs +++ b/codex-rs/tools/src/code_mode_tests.rs @@ -1,9 +1,12 @@ use super::augment_tool_spec_for_code_mode; +use super::collect_code_mode_tool_definitions; use super::tool_spec_to_code_mode_tool_definition; use crate::AdditionalProperties; use crate::FreeformTool; use crate::FreeformToolFormat; use crate::JsonSchema; +use crate::ResponsesApiNamespace; +use crate::ResponsesApiNamespaceTool; use crate::ResponsesApiTool; use crate::ToolName; use crate::ToolSpec; @@ -135,3 +138,53 @@ fn tool_spec_to_code_mode_tool_definition_skips_unsupported_variants() { None ); } + +#[test] +fn namespace_tools_are_flattened_for_code_mode_runtime() { + let tools = collect_code_mode_tool_definitions([&ToolSpec::Namespace(ResponsesApiNamespace { + name: "files".to_string(), + description: "File tools".to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "export_for_tool".to_string(), + description: "Export a file ref".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + BTreeMap::from([( + "file_uri".to_string(), + JsonSchema::string(/*description*/ None), + )]), + Some(vec!["file_uri".to_string()]), + Some(AdditionalProperties::Boolean(false)), + ), + output_schema: None, + })], + })]); + + assert_eq!( + tools, + vec![codex_code_mode::ToolDefinition { + name: "files_export_for_tool".to_string(), + tool_name: ToolName::namespaced("files", "export_for_tool"), + description: r#"Export a file ref + +exec tool declaration: +```ts +declare const tools: { files_export_for_tool(args: { file_uri: string; }): Promise; }; +```"# + .to_string(), + kind: codex_code_mode::CodeModeToolKind::Function, + input_schema: Some(json!({ + "type": "object", + "properties": { + "file_uri": { + "type": "string" + } + }, + "required": ["file_uri"], + "additionalProperties": false + })), + output_schema: None, + }] + ); +}