diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 78adcbfdbd..2734c21c65 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -10,12 +10,12 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::Verbosity; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::DisabledTool; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnAbortReason; -use codex_protocol::protocol::default_disabled_tools; use paste::paste; use serde::Deserialize; use serde::Serialize; @@ -551,8 +551,8 @@ pub struct SendUserTurnParams { #[serde(skip_serializing_if = "Option::is_none")] pub effort: Option, pub summary: ReasoningSummary, - #[serde(default = "default_disabled_tools")] - pub disabled_tools: Vec, + #[serde(default = "DisabledTool::defaults")] + pub disabled_tools: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] diff --git a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs index ce9302d22f..6403459559 100644 --- a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs +++ b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs @@ -27,10 +27,10 @@ use codex_core::protocol_config_types::ReasoningEffort; use codex_core::protocol_config_types::ReasoningSummary; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_protocol::config_types::SandboxMode; +use codex_protocol::protocol::DisabledTool; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InputMessageKind; -use codex_protocol::protocol::default_disabled_tools; use pretty_assertions::assert_eq; use std::env; use tempfile::TempDir; @@ -346,7 +346,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() { model: "mock-model".to_string(), effort: Some(ReasoningEffort::Medium), summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .expect("send sendUserTurn"); @@ -485,7 +485,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() { model: model.clone(), effort: Some(ReasoningEffort::Medium), summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .expect("send first sendUserTurn"); @@ -516,7 +516,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() { model: model.clone(), effort: Some(ReasoningEffort::Medium), summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .expect("send second sendUserTurn"); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 9b4538ced8..78d8c101c4 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -239,8 +239,7 @@ impl ModelClient { instructions: &full_instructions, input: &input_with_instructions, tools: &tools_json, - allowed_tools: prompt.allowed_tools.as_slice(), - tool_choice: "auto", + tool_choice: prompt.tool_choice(), parallel_tool_calls: prompt.parallel_tool_calls, reasoning, store: azure_workaround, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index e67f9e4ff8..3a7e04b4ec 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -43,7 +43,7 @@ pub struct Prompt { pub output_schema: Option, /// The set of tools that are allowed to be used by the model. - pub(crate) allowed_tools: Vec, + pub(crate) allowed_tools: Option>, } impl Prompt { @@ -88,10 +88,17 @@ impl Prompt { input } -} -fn json_slice_is_empty(values: &[Value]) -> bool { - values.is_empty() + pub(crate) fn tool_choice(&self) -> ToolChoice<'_> { + match &self.allowed_tools { + Some(tools) => ToolChoice::AllowedTools(AllowedToolsChoice { + choice_type: "allowed_tools", + mode: "auto", + tools, + }), + None => ToolChoice::Auto("auto"), + } + } } fn reserialize_shell_outputs(items: &mut [ResponseItem]) { @@ -275,9 +282,7 @@ pub(crate) struct ResponsesApiRequest<'a> { // separate enum for serialization. pub(crate) input: &'a Vec, pub(crate) tools: &'a [serde_json::Value], - #[serde(skip_serializing_if = "json_slice_is_empty")] - pub(crate) allowed_tools: &'a [serde_json::Value], - pub(crate) tool_choice: &'static str, + pub(crate) tool_choice: ToolChoice<'a>, pub(crate) parallel_tool_calls: bool, pub(crate) reasoning: Option, pub(crate) store: bool, @@ -289,6 +294,21 @@ pub(crate) struct ResponsesApiRequest<'a> { pub(crate) text: Option, } +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum ToolChoice<'a> { + Auto(&'static str), + AllowedTools(AllowedToolsChoice<'a>), +} + +#[derive(Debug, Serialize)] +pub(crate) struct AllowedToolsChoice<'a> { + #[serde(rename = "type")] + pub(crate) choice_type: &'static str, + pub(crate) mode: &'static str, + pub(crate) tools: &'a [Value], +} + pub(crate) mod tools { use crate::openai_tools::JsonSchema; use serde::Deserialize; @@ -466,8 +486,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - allowed_tools: &[], - tool_choice: "auto", + tool_choice: ToolChoice::Auto("auto"), parallel_tool_calls: true, reasoning: None, store: false, @@ -508,8 +527,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - allowed_tools: &[], - tool_choice: "auto", + tool_choice: ToolChoice::Auto("auto"), parallel_tool_calls: true, reasoning: None, store: false, @@ -545,8 +563,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - allowed_tools: &[], - tool_choice: "auto", + tool_choice: ToolChoice::Auto("auto"), parallel_tool_calls: true, reasoning: None, store: false, @@ -573,8 +590,11 @@ mod tests { instructions: "i", input: &input, tools: &tools, - allowed_tools: allowed.as_slice(), - tool_choice: "auto", + tool_choice: ToolChoice::AllowedTools(AllowedToolsChoice { + choice_type: "allowed_tools", + mode: "auto", + tools: allowed.as_slice(), + }), parallel_tool_calls: true, reasoning: None, store: false, @@ -585,17 +605,21 @@ mod tests { }; let v = serde_json::to_value(&req).expect("json"); + let choice = v.get("tool_choice").expect("tool_choice field"); assert_eq!( - v.get("allowed_tools") - .and_then(|val| val.as_array()) - .map(std::vec::Vec::len), - Some(1) + choice.get("type"), + Some(&serde_json::Value::String("allowed_tools".into())) ); - let first = v - .get("allowed_tools") + assert_eq!( + choice.get("mode"), + Some(&serde_json::Value::String("auto".into())) + ); + let tools_array = choice + .get("tools") .and_then(|val| val.as_array()) - .and_then(|arr| arr.first()) - .expect("allowed tool entry"); + .expect("tools array"); + assert_eq!(tools_array.len(), 1); + let first = &tools_array[0]; assert_eq!( first.get("type"), Some(&serde_json::Value::String("function".into())) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9d0e8b17ee..c0b699e08b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -16,6 +16,7 @@ use async_channel::Sender; use codex_apply_patch::ApplyPatchAction; use codex_protocol::ConversationId; use codex_protocol::protocol::ConversationPathResponseEvent; +use codex_protocol::protocol::DisabledTool; use codex_protocol::protocol::ExitedReviewModeEvent; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; @@ -23,7 +24,6 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TaskStartedEvent; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; -use codex_protocol::protocol::default_disabled_tools; use futures::future::BoxFuture; use futures::prelude::*; use futures::stream::FuturesOrdered; @@ -265,7 +265,7 @@ pub(crate) struct TurnContext { pub(crate) tools_config: ToolsConfig, pub(crate) is_review_mode: bool, pub(crate) final_output_json_schema: Option, - pub(crate) disabled_tools: Vec, + pub(crate) disabled_tools: Vec, } impl TurnContext { @@ -461,7 +461,7 @@ impl Session { cwd, is_review_mode: false, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }; let services = SessionServices { mcp_connection_manager, @@ -1211,7 +1211,7 @@ async fn submission_loop( cwd: new_cwd.clone(), is_review_mode: false, final_output_json_schema: None, - disabled_tools: disabled_tools.unwrap_or_else(default_disabled_tools), + disabled_tools: disabled_tools.unwrap_or_else(DisabledTool::defaults), }; // Install the new persistent context for subsequent tasks/turns. @@ -1586,7 +1586,7 @@ async fn spawn_review_thread( cwd: parent_turn_context.cwd.clone(), is_review_mode: true, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }; // Seed the child task with the review prompt as the initial user message. @@ -1953,20 +1953,29 @@ async fn run_turn( .get_model_family() .supports_parallel_tool_calls; let parallel_tool_calls = model_supports_parallel; - let allowed_tools = router - .allowed_tools() - .into_iter() - .filter(|tool| { - tool.get("name") - .and_then(|val| val.as_str()) - .is_none_or(|name| { - !turn_context - .disabled_tools - .iter() - .any(|disabled| disabled == name) - }) - }) - .collect::>(); + let mut allowed_tools = Vec::new(); + let mut restricted_tool_choice = false; + for tool in router.allowed_tools() { + let is_disabled = tool + .get("name") + .and_then(|val| val.as_str()) + .is_some_and(|name| { + turn_context + .disabled_tools + .iter() + .any(|disabled| disabled.matches_tool_name(name)) + }); + if is_disabled { + restricted_tool_choice = true; + continue; + } + allowed_tools.push(tool); + } + let allowed_tools = if restricted_tool_choice { + Some(allowed_tools) + } else { + None + }; let prompt = Prompt { input, tools: router.specs(), @@ -2781,7 +2790,7 @@ mod tests { tools_config, is_review_mode: false, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }; let services = SessionServices { mcp_connection_manager: McpConnectionManager::default(), @@ -2850,7 +2859,7 @@ mod tests { tools_config, is_review_mode: false, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }); let services = SessionServices { mcp_connection_manager: McpConnectionManager::default(), diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index b8314b0c14..6357e1af25 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -245,6 +245,6 @@ pub const FEATURES: &[FeatureSpec] = &[ id: Feature::WebSearchRequest, key: "web_search_request", stage: Stage::Stable, - default_enabled: false, + default_enabled: true, }, ]; diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 2c35268de1..5ba5c87bcf 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -124,7 +124,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { .unwrap_or_default() .to_string(); let tool_calls = requests[0]["tools"].clone(); - let allowed_tools = requests[0]["allowed_tools"].clone(); + let tool_choice = requests[0]["tool_choice"].clone(); let prompt_cache_key = requests[0]["prompt_cache_key"] .as_str() .unwrap_or_default() @@ -171,8 +171,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { } ], "tools": tool_calls, - "allowed_tools": allowed_tools, - "tool_choice": "auto", + "tool_choice": tool_choice, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -241,8 +240,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { } ], "tools": [], - "allowed_tools": allowed_tools, - "tool_choice": "auto", + "tool_choice": tool_choice, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -307,8 +305,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "allowed_tools": allowed_tools, - "tool_choice": "auto", + "tool_choice": tool_choice, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -393,8 +390,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "allowed_tools": allowed_tools, - "tool_choice": "auto", + "tool_choice": tool_choice, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -479,8 +475,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "allowed_tools": allowed_tools, - "tool_choice": "auto", + "tool_choice": tool_choice, "parallel_tool_calls": false, "reasoning": { "summary": "auto" diff --git a/codex-rs/core/tests/suite/grep_files.rs b/codex-rs/core/tests/suite/grep_files.rs index 5a83d6831f..3f35c22257 100644 --- a/codex-rs/core/tests/suite/grep_files.rs +++ b/codex-rs/core/tests/suite/grep_files.rs @@ -3,11 +3,11 @@ use anyhow::Result; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -160,7 +160,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str) -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 568a6df28e..294c7e9058 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -1,11 +1,11 @@ #![cfg(not(target_os = "windows"))] use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::skip_if_no_network; @@ -85,7 +85,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/list_dir.rs b/codex-rs/core/tests/suite/list_dir.rs index 4d5c5cbf71..f98316e38f 100644 --- a/codex-rs/core/tests/suite/list_dir.rs +++ b/codex-rs/core/tests/suite/list_dir.rs @@ -1,11 +1,11 @@ #![cfg(not(target_os = "windows"))] use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -77,7 +77,7 @@ async fn list_dir_tool_returns_entries() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -183,7 +183,7 @@ async fn list_dir_tool_depth_one_omits_children() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -296,7 +296,7 @@ async fn list_dir_tool_depth_two_includes_children_only() -> anyhow::Result<()> model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -412,7 +412,7 @@ async fn list_dir_tool_depth_three_includes_grandchildren() -> anyhow::Result<() model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 336406bba8..cf3cd18275 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -8,11 +8,11 @@ use codex_core::config::OPENAI_DEFAULT_MODEL; use codex_core::features::Feature; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_core::protocol_config_types::ReasoningEffort; use codex_core::protocol_config_types::ReasoningSummary; use codex_core::shell::Shell; @@ -579,7 +579,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() { effort: Some(ReasoningEffort::High), summary: ReasoningSummary::Detailed, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .unwrap(); @@ -691,7 +691,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() { effort: default_effort, summary: default_summary, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .unwrap(); @@ -709,7 +709,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() { effort: default_effort, summary: default_summary, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .unwrap(); @@ -807,7 +807,7 @@ async fn send_user_turn_with_changes_sends_environment_context() { effort: default_effort, summary: default_summary, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .unwrap(); @@ -825,7 +825,7 @@ async fn send_user_turn_with_changes_sends_environment_context() { effort: Some(ReasoningEffort::High), summary: ReasoningSummary::Detailed, final_output_json_schema: None, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/read_file.rs b/codex-rs/core/tests/suite/read_file.rs index 293621b4c2..8ba7bccc84 100644 --- a/codex-rs/core/tests/suite/read_file.rs +++ b/codex-rs/core/tests/suite/read_file.rs @@ -1,11 +1,11 @@ #![cfg(not(target_os = "windows"))] use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -75,7 +75,7 @@ async fn read_file_tool_returns_requested_lines() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 8af9f90ff3..54b3bf8c24 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -12,11 +12,11 @@ use codex_core::config_types::McpServerTransportConfig; use codex_core::features::Feature; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::responses::mount_sse_once_match; @@ -111,7 +111,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -261,7 +261,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -443,7 +443,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index f07dc6f887..89cec293ee 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -4,11 +4,11 @@ use anyhow::Result; use codex_core::features::Feature; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::assert_regex_match; use core_test_support::responses::ev_apply_patch_function_call; @@ -46,7 +46,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str, sandbox_policy: SandboxPoli model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 277e677bb8..05923d1d9e 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -6,11 +6,11 @@ use assert_matches::assert_matches; use codex_core::features::Feature; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::plan_tool::StepStatus; use core_test_support::assert_regex_match; @@ -85,7 +85,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -155,7 +155,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -239,7 +239,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -338,7 +338,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -440,7 +440,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index 65f082cfd1..4120e73e3b 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -6,11 +6,11 @@ use std::time::Instant; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -39,7 +39,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index ee35f2515d..4e6b321bec 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -5,11 +5,12 @@ use anyhow::Result; use codex_core::features::Feature; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; +use codex_core::protocol::DisabledToolKind; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; @@ -49,7 +50,7 @@ async fn submit_turn( model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -410,6 +411,163 @@ async fn shell_timeout_includes_timeout_prefix_and_metadata() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_allowed_when_other_tool_disabled() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.model = "gpt-5".to_string(); + config.model_family = find_family_for_model("gpt-5").expect("gpt-5 family"); + config.features.enable(Feature::WebSearchRequest); + }); + let test = builder.build(&server).await?; + + test.codex + .submit(Op::UserTurn { + items: vec![InputItem::Text { + text: "hello codex".into(), + }], + final_output_json_schema: None, + cwd: test.cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + model: test.session_configured.model.clone(), + effort: None, + summary: ReasoningSummary::Auto, + disabled_tools: vec![DisabledToolKind::ViewImage.into()], + }) + .await?; + + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TaskComplete(_)) + }) + .await; + + let body = mock.single_request().body_json(); + let choice = body + .get("tool_choice") + .expect("tool_choice field should be present"); + assert!( + choice.is_object(), + "expected tool_choice to be an object when tools are restricted: {choice:?}" + ); + assert_eq!( + choice.get("type").and_then(Value::as_str), + Some("allowed_tools") + ); + assert_eq!(choice.get("mode").and_then(Value::as_str), Some("auto")); + + let allowed = choice + .get("tools") + .and_then(Value::as_array) + .cloned() + .expect("allowed tools array"); + assert!( + allowed + .iter() + .any(|tool| tool.get("name").and_then(Value::as_str) == Some("web_search")), + "expected web_search to remain allowed: {allowed:?}" + ); + assert!( + !allowed + .iter() + .any(|tool| tool.get("name").and_then(Value::as_str) == Some("view_image")), + "expected view_image to be excluded: {allowed:?}" + ); + let tools = tool_names(&body); + assert!( + tools.iter().any(|name| name == "web_search"), + "expected tools list to include web_search: {tools:?}" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn override_enables_web_search() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.model = "gpt-5".to_string(); + config.model_family = find_family_for_model("gpt-5").expect("gpt-5 family"); + config.features.enable(Feature::WebSearchRequest); + }); + let test = builder.build(&server).await?; + + test.codex + .submit(Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + model: None, + effort: None, + summary: None, + disabled_tools: Some(vec![]), + }) + .await?; + + test.codex + .submit(Op::UserTurn { + items: vec![InputItem::Text { + text: "hello after override".into(), + }], + final_output_json_schema: None, + cwd: test.cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + model: test.session_configured.model.clone(), + effort: None, + summary: ReasoningSummary::Auto, + disabled_tools: vec![], + }) + .await?; + + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TaskComplete(_)) + }) + .await; + + let body = mock.single_request().body_json(); + let choice = body + .get("tool_choice") + .expect("tool_choice field should be present"); + assert_eq!( + choice.as_str(), + Some("auto"), + "expected unrestricted tool choice to be auto: {choice:?}" + ); + + let tools = tool_names(&body); + assert!( + tools.iter().any(|name| name == "web_search"), + "expected tools list to include web_search: {tools:?}" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 230a88e992..17d1108f7c 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -5,11 +5,11 @@ use std::collections::HashMap; use anyhow::Result; use codex_core::features::Feature; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -129,7 +129,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -266,7 +266,7 @@ PY model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -373,7 +373,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 6749de3b92..160175f633 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -3,11 +3,11 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::default_disabled_tools; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -101,7 +101,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -201,7 +201,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; @@ -267,7 +267,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7d8466a8c8..dfdbf589b2 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -19,13 +19,13 @@ use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::git_info::get_git_repo_root; use codex_core::protocol::AskForApproval; +use codex_core::protocol::DisabledTool; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_core::protocol::TaskCompleteEvent; -use codex_core::protocol::default_disabled_tools; use codex_ollama::DEFAULT_OSS_MODEL; use codex_protocol::config_types::SandboxMode; use event_processor_with_human_output::EventProcessorWithHumanOutput; @@ -349,7 +349,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any effort: default_effort, summary: default_summary, final_output_json_schema: output_schema, - disabled_tools: default_disabled_tools(), + disabled_tools: DisabledTool::defaults(), }) .await?; info!("Sent prompt with event ID: {initial_prompt_task_id}"); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 8927dd2335..eae1c7b3e9 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -37,9 +37,67 @@ pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = ""; pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = ""; pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:"; -/// Default disabled tools used when clients do not explicitly supply one. -pub fn default_disabled_tools() -> Vec { - vec!["web_search".to_string()] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum DisabledToolKind { + WebSearch, + ViewImage, + UnifiedExec, + UpdatePlan, + Shell, + ApplyPatch, + GrepFiles, + ReadFile, + ListDir, + LocalShell, +} + +impl DisabledToolKind { + pub fn raw_name(self) -> &'static str { + match self { + DisabledToolKind::WebSearch => "web_search", + DisabledToolKind::ViewImage => "view_image", + DisabledToolKind::UnifiedExec => "unified_exec", + DisabledToolKind::UpdatePlan => "update_plan", + DisabledToolKind::Shell => "shell", + DisabledToolKind::ApplyPatch => "apply_patch", + DisabledToolKind::GrepFiles => "grep_files", + DisabledToolKind::ReadFile => "read_file", + DisabledToolKind::ListDir => "list_dir", + DisabledToolKind::LocalShell => "local_shell", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[serde(untagged)] +pub enum DisabledTool { + Known(DisabledToolKind), + Other(String), +} + +impl From for DisabledTool { + fn from(kind: DisabledToolKind) -> Self { + DisabledTool::Known(kind) + } +} + +impl DisabledTool { + /// Default disabled tools used when clients do not explicitly supply one. + pub fn defaults() -> Vec { + vec![DisabledToolKind::WebSearch.into()] + } + + pub fn raw_name(&self) -> &str { + match self { + DisabledTool::Known(kind) => kind.raw_name(), + DisabledTool::Other(name) => name.as_str(), + } + } + + pub fn matches_tool_name(&self, tool_name: &str) -> bool { + self.raw_name() == tool_name + } } /// Submission Queue Entry - requests from user @@ -96,8 +154,8 @@ pub enum Op { // The JSON schema to use for the final assistant message final_output_json_schema: Option, // disables tools - #[serde(default = "default_disabled_tools")] - disabled_tools: Vec, + #[serde(default = "DisabledTool::defaults")] + disabled_tools: Vec, }, /// Override parts of the persistent turn context for subsequent turns. @@ -136,7 +194,7 @@ pub enum Op { /// Updated disabled tools. #[serde(skip_serializing_if = "Option::is_none")] - disabled_tools: Option>, + disabled_tools: Option>, }, /// Approve a command execution