diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index cb894a51ab..7f0eed899a 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1050,6 +1050,7 @@ impl CodexMessageProcessor { effort, summary, final_output_json_schema: None, + disabled_tools: None, }) .await; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index a8dcb7259a..40f485d481 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -43,6 +43,7 @@ use crate::model_family::ModelFamily; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; use crate::openai_model_info::get_model_info; +use crate::openai_tools::create_allowed_tools_json_for_responses_api; use crate::openai_tools::create_tools_json_for_responses_api; use crate::protocol::RateLimitSnapshot; use crate::protocol::RateLimitWindow; @@ -54,6 +55,7 @@ use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ResponseItem; +use std::collections::HashSet; use std::sync::Arc; #[derive(Debug, Deserialize)] @@ -233,13 +235,39 @@ impl ModelClient { // // For Azure, we send `store: true` and preserve reasoning item IDs. let azure_workaround = self.provider.is_azure_responses_endpoint(); + let tool_choice = if let Some(disabled) = &prompt.disabled_tools { + if disabled.is_empty() { + serde_json::json!("auto") + } else { + let allowed = create_allowed_tools_json_for_responses_api(&prompt.tools, disabled); + let total_unique = prompt + .tools + .iter() + .map(super::client_common::tools::ToolSpec::name) + .collect::>() + .len(); + if allowed.is_empty() { + serde_json::json!("none") + } else if allowed.len() == total_unique { + serde_json::json!("auto") + } else { + serde_json::json!({ + "type": "allowed_tools", + "mode": "auto", + "tools": allowed, + }) + } + } + } else { + serde_json::json!("auto") + }; let payload = ResponsesApiRequest { model: &self.config.model, instructions: &full_instructions, input: &input_with_instructions, tools: &tools_json, - tool_choice: "auto", + 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 d30da5192a..3f2c9e5575 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -41,6 +41,9 @@ pub struct Prompt { /// Optional the output schema for the model's response. pub output_schema: Option, + + /// Optional list of tool names to disable for this prompt. + pub disabled_tools: Option>, } impl Prompt { @@ -268,7 +271,7 @@ pub(crate) struct ResponsesApiRequest<'a> { // separate enum for serialization. pub(crate) input: &'a Vec, pub(crate) tools: &'a [serde_json::Value], - pub(crate) tool_choice: &'static str, + pub(crate) tool_choice: Value, pub(crate) parallel_tool_calls: bool, pub(crate) reasoning: Option, pub(crate) store: bool, @@ -457,7 +460,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - tool_choice: "auto", + tool_choice: serde_json::json!("auto"), parallel_tool_calls: true, reasoning: None, store: false, @@ -498,7 +501,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - tool_choice: "auto", + tool_choice: serde_json::json!("auto"), parallel_tool_calls: true, reasoning: None, store: false, @@ -534,7 +537,7 @@ mod tests { instructions: "i", input: &input, tools: &tools, - tool_choice: "auto", + tool_choice: serde_json::json!("auto"), parallel_tool_calls: true, reasoning: None, store: false, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0441d3e4f7..14898295e0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -265,6 +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: Option>, } impl TurnContext { @@ -488,6 +489,7 @@ impl Session { cwd, is_review_mode: false, final_output_json_schema: None, + disabled_tools: None, }; let services = SessionServices { mcp_connection_manager, @@ -1171,6 +1173,7 @@ async fn submission_loop( model, effort, summary, + disabled_tools, } => { // Recalculate the persistent turn context with provided overrides. let prev = Arc::clone(&turn_context); @@ -1219,6 +1222,9 @@ async fn submission_loop( .clone() .unwrap_or(prev.sandbox_policy.clone()); let new_cwd = cwd.clone().unwrap_or_else(|| prev.cwd.clone()); + let new_disabled_tools = disabled_tools + .clone() + .or_else(|| prev.disabled_tools.clone()); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_family: &effective_family, @@ -1236,6 +1242,7 @@ async fn submission_loop( cwd: new_cwd.clone(), is_review_mode: false, final_output_json_schema: None, + disabled_tools: new_disabled_tools, }; // Install the new persistent context for subsequent tasks/turns. @@ -1274,6 +1281,7 @@ async fn submission_loop( effort, summary, final_output_json_schema, + disabled_tools, } => { turn_context .client @@ -1314,6 +1322,9 @@ async fn submission_loop( summary, sess.conversation_id, ); + let effective_disabled_tools = disabled_tools + .clone() + .or_else(|| turn_context.disabled_tools.clone()); let fresh_turn_context = TurnContext { client, @@ -1329,6 +1340,7 @@ async fn submission_loop( cwd, is_review_mode: false, final_output_json_schema, + disabled_tools: effective_disabled_tools.clone(), }; // if the environment context has changed, record it in the conversation history @@ -1608,6 +1620,7 @@ async fn spawn_review_thread( cwd: parent_turn_context.cwd.clone(), is_review_mode: true, final_output_json_schema: None, + disabled_tools: parent_turn_context.disabled_tools.clone(), }; // Seed the child task with the review prompt as the initial user message. @@ -1980,6 +1993,7 @@ async fn run_turn( parallel_tool_calls, base_instructions_override: turn_context.base_instructions.clone(), output_schema: turn_context.final_output_json_schema.clone(), + disabled_tools: turn_context.disabled_tools.clone(), }; let mut retries = 0; @@ -2787,6 +2801,7 @@ mod tests { tools_config, is_review_mode: false, final_output_json_schema: None, + disabled_tools: None, }; let services = SessionServices { mcp_connection_manager: McpConnectionManager::default(), @@ -2855,6 +2870,7 @@ mod tests { tools_config, is_review_mode: false, final_output_json_schema: None, + disabled_tools: None, }); let services = SessionServices { mcp_connection_manager: McpConnectionManager::default(), diff --git a/codex-rs/core/src/codex/compact.rs b/codex-rs/core/src/codex/compact.rs index 93bbfa79c6..9f59f01bc2 100644 --- a/codex-rs/core/src/codex/compact.rs +++ b/codex-rs/core/src/codex/compact.rs @@ -92,6 +92,7 @@ async fn run_compact_task_inner( loop { let prompt = Prompt { input: turn_input.clone(), + disabled_tools: turn_context.disabled_tools.clone(), ..Default::default() }; let attempt_result = diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index fa6e38a419..098f29e1d9 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -145,6 +145,22 @@ impl ToolRouter { let payload_outputs_custom = matches!(payload, ToolPayload::Custom { .. }); let failure_call_id = call_id.clone(); + if turn + .disabled_tools + .as_ref() + .map(|tools| tools.iter().any(|name| name == &tool_name)) + .unwrap_or(false) + { + let err = FunctionCallError::RespondToModel(format!( + "tool {tool_name} is disabled for this turn" + )); + return Ok(Self::failure_response( + failure_call_id, + payload_outputs_custom, + err, + )); + } + let invocation = ToolInvocation { session, turn, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index bb1df187c2..92eb878c7b 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -14,6 +14,7 @@ use serde_json::Value as JsonValue; use serde_json::json; use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; #[derive(Debug, Clone)] pub enum ConfigShellToolType { @@ -532,6 +533,40 @@ pub fn create_tools_json_for_responses_api( Ok(tools_json) } + +pub fn create_allowed_tools_json_for_responses_api( + tools: &[ToolSpec], + disabled_names: &[String], +) -> Vec { + let disabled_set: HashSet<&str> = disabled_names + .iter() + .map(std::string::String::as_str) + .collect(); + let mut seen = HashSet::new(); + let mut allowed = Vec::new(); + + for spec in tools { + let name = spec.name(); + if disabled_set.contains(name) || !seen.insert(name) { + continue; + } + let value = match spec { + ToolSpec::Function(tool) => json!({ + "type": "function", + "name": tool.name, + }), + ToolSpec::Freeform(tool) => json!({ + "type": "custom", + "name": tool.name, + }), + ToolSpec::LocalShell {} => json!({ "type": "local_shell" }), + ToolSpec::WebSearch {} => json!({ "type": "web_search" }), + }; + allowed.push(value); + } + + allowed +} /// Returns JSON values that are compatible with Function Calling in the /// Chat Completions API: /// https://platform.openai.com/docs/guides/function-calling?api-mode=chat @@ -1093,6 +1128,33 @@ mod tests { ); } + #[test] + fn create_allowed_tools_excludes_disabled_entries() { + let shell = super::create_shell_tool(); + let web_search = ToolSpec::WebSearch {}; + let view_image = super::create_view_image_tool(); + + let specs = vec![shell, web_search, view_image]; + + let allowed = super::create_allowed_tools_json_for_responses_api( + &specs, + &[String::from("web_search")], + ); + + assert_eq!(allowed.len(), 2); + assert!(allowed.iter().any(|tool| { + tool.get("type") == Some(&serde_json::Value::String("function".into())) + && tool.get("name") == Some(&serde_json::Value::String("shell".into())) + })); + assert!(allowed.iter().any(|tool| { + tool.get("type") == Some(&serde_json::Value::String("function".into())) + && tool.get("name") == Some(&serde_json::Value::String("view_image".into())) + })); + assert!(allowed.iter().all(|tool| { + tool.get("name") != Some(&serde_json::Value::String("web_search".into())) + })); + } + #[test] fn test_build_specs_mcp_tools_sorted_by_name() { let model_family = find_family_for_model("o3").expect("o3 should be a valid model family"); diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 8197bed10f..9adae36323 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -124,6 +124,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { .unwrap_or_default() .to_string(); let tool_calls = json!(requests[0]["tools"].as_array()); + let tool_choice_request_1 = requests[0]["tool_choice"].clone(); let prompt_cache_key = requests[0]["prompt_cache_key"] .as_str() .unwrap_or_default() @@ -132,6 +133,10 @@ async fn compact_resume_and_fork_preserve_model_history_view() { .as_str() .unwrap_or_default() .to_string(); + let tool_choice_compact_1 = requests[1]["tool_choice"].clone(); + let tool_choice_after_compact = requests[2]["tool_choice"].clone(); + let tool_choice_after_resume = requests[3]["tool_choice"].clone(); + let tool_choice_after_fork = requests[4]["tool_choice"].clone(); let expected_model = OPENAI_DEFAULT_MODEL; let user_turn_1 = json!( { @@ -170,7 +175,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { } ], "tools": tool_calls, - "tool_choice": "auto", + "tool_choice": tool_choice_request_1, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -239,7 +244,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { } ], "tools": [], - "tool_choice": "auto", + "tool_choice": tool_choice_compact_1, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -304,7 +309,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "tool_choice": "auto", + "tool_choice": tool_choice_after_compact, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -389,7 +394,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "tool_choice": "auto", + "tool_choice": tool_choice_after_resume, "parallel_tool_calls": false, "reasoning": { "summary": "auto" @@ -474,7 +479,7 @@ SUMMARY_ONLY_CONTEXT" } ], "tools": tool_calls, - "tool_choice": "auto", + "tool_choice": tool_choice_after_fork, "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 31195f7e3b..5242cf79e8 100644 --- a/codex-rs/core/tests/suite/grep_files.rs +++ b/codex-rs/core/tests/suite/grep_files.rs @@ -159,6 +159,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str) -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index e8d91fae8b..8409b45a96 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -84,6 +84,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/list_dir.rs b/codex-rs/core/tests/suite/list_dir.rs index 1aa5a6484b..3fb8f2b1d1 100644 --- a/codex-rs/core/tests/suite/list_dir.rs +++ b/codex-rs/core/tests/suite/list_dir.rs @@ -76,6 +76,7 @@ async fn list_dir_tool_returns_entries() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -181,6 +182,7 @@ async fn list_dir_tool_depth_one_omits_children() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -293,6 +295,7 @@ async fn list_dir_tool_depth_two_includes_children_only() -> anyhow::Result<()> model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -408,6 +411,7 @@ async fn list_dir_tool_depth_three_includes_grandchildren() -> anyhow::Result<() model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index a186c13ef3..7fef70d914 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -38,6 +38,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() { model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), summary: None, + disabled_tools: None, }) .await .expect("submit override"); @@ -78,6 +79,7 @@ async fn override_turn_context_does_not_create_config_file() { model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::Medium)), summary: None, + disabled_tools: None, }) .await .expect("submit override"); diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index dc000af449..1e57dfb267 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -443,6 +443,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() { model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), + disabled_tools: None, }) .await .unwrap(); @@ -577,6 +578,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: None, }) .await .unwrap(); @@ -688,6 +690,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: None, }) .await .unwrap(); @@ -705,6 +708,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: None, }) .await .unwrap(); @@ -802,6 +806,7 @@ async fn send_user_turn_with_changes_sends_environment_context() { effort: default_effort, summary: default_summary, final_output_json_schema: None, + disabled_tools: None, }) .await .unwrap(); @@ -819,6 +824,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: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/read_file.rs b/codex-rs/core/tests/suite/read_file.rs index fc5a94f91b..38e95506ac 100644 --- a/codex-rs/core/tests/suite/read_file.rs +++ b/codex-rs/core/tests/suite/read_file.rs @@ -74,6 +74,7 @@ async fn read_file_tool_returns_requested_lines() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 9dd921b91f..df0acc1ab1 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -110,6 +110,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -259,6 +260,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -440,6 +442,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 21141ec535..c5519bfb6d 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -45,6 +45,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str, sandbox_policy: SandboxPoli model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 68bd76bd8e..68f16df480 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -84,6 +84,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -153,6 +154,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -236,6 +238,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -334,6 +337,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -435,6 +439,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index b4e3d1c9ad..2b9ec91ed8 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -38,6 +38,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index ec07b0cdbd..e8201e9c95 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -48,6 +48,7 @@ async fn submit_turn( model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 6298ab06de..aa018a0845 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -128,6 +128,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -264,6 +265,7 @@ PY model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -370,6 +372,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index bdb67ad631..557436c10c 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -100,6 +100,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -199,6 +200,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; @@ -264,6 +266,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { model: session_model, effort: None, summary: ReasoningSummary::Auto, + disabled_tools: None, }) .await?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 967da52b8a..857fdd8eb7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -348,6 +348,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: None, }) .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 b73909473d..986b046cbe 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -90,6 +90,9 @@ pub enum Op { summary: ReasoningSummaryConfig, // The JSON schema to use for the final assistant message final_output_json_schema: Option, + /// Optional list of tool names to disable for this turn. + #[serde(skip_serializing_if = "Option::is_none")] + disabled_tools: Option>, }, /// Override parts of the persistent turn context for subsequent turns. @@ -125,6 +128,9 @@ pub enum Op { /// Updated reasoning summary preference (honored only for reasoning-capable models). #[serde(skip_serializing_if = "Option::is_none")] summary: Option, + /// Updated default disabled tool list for subsequent turns. + #[serde(skip_serializing_if = "Option::is_none")] + disabled_tools: Option>, }, /// Approve a command execution diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 787e098093..d5d87ab733 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1751,6 +1751,7 @@ impl ChatWidget { model: Some(model_for_action.clone()), effort: Some(effort_for_action), summary: None, + disabled_tools: None, })); tx.send(AppEvent::UpdateModel(model_for_action.clone())); tx.send(AppEvent::UpdateReasoningEffort(effort_for_action)); @@ -1807,6 +1808,7 @@ impl ChatWidget { model: None, effort: None, summary: None, + disabled_tools: None, })); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone()));