diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 79c86b94d1f0..76c1e4389c84 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -643,9 +643,12 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut planned_tools.add(PlanHandler); if turn_context.config.experimental_request_user_input_enabled { - planned_tools.add(RequestUserInputHandler { - available_modes: request_user_input_available_modes(features), - }); + planned_tools.add_with_exposure( + RequestUserInputHandler { + available_modes: request_user_input_available_modes(features), + }, + ToolExposure::DirectModelOnly, + ); } if features.enabled(Feature::RequestPermissionsTool) { diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 2f7f23947b44..3f9832c857c5 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -431,6 +431,10 @@ async fn request_user_input_tool_respects_experimental_config_gate() { let enabled = probe(|_| {}).await; enabled.assert_visible_contains(&["request_user_input"]); enabled.assert_registered_contains(&["request_user_input"]); + assert_eq!( + enabled.exposure("request_user_input"), + ToolExposure::DirectModelOnly + ); let disabled = probe(|turn| { update_config(turn, |config| { @@ -442,6 +446,30 @@ async fn request_user_input_tool_respects_experimental_config_gate() { disabled.assert_registered_lacks(&["request_user_input"]); } +#[tokio::test] +async fn request_user_input_stays_direct_in_code_mode_only() { + let plan = probe(|turn| { + set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]); + }) + .await; + + plan.assert_visible_contains(&[ + "request_user_input", + codex_code_mode::PUBLIC_TOOL_NAME, + codex_code_mode::WAIT_TOOL_NAME, + ]); + plan.assert_registered_contains(&["request_user_input"]); + assert_eq!( + plan.exposure("request_user_input"), + ToolExposure::DirectModelOnly + ); + + let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else { + panic!("expected code mode exec tool"); + }; + assert!(!exec.description.contains("request_user_input")); +} + #[tokio::test] async fn shell_family_registers_visible_unified_exec_and_hidden_legacy_shell() { let plan = probe(|turn| { @@ -1224,6 +1252,7 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() { vec![ "exec", "wait", + "request_user_input", "agents", // Hosted Responses tools. "web_search", @@ -1309,6 +1338,7 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() { // Code-mode entrypoints. codex_code_mode::PUBLIC_TOOL_NAME, codex_code_mode::WAIT_TOOL_NAME, + "request_user_input", // Multi-agent v2 tools. "spawn_agent", "send_message", diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 0f954bac7b06..a25b5bd4d712 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -502,6 +502,7 @@ async fn code_mode_only_restricts_prompt_tools() -> Result<()> { vec![ "exec".to_string(), "wait".to_string(), + "request_user_input".to_string(), "web_search".to_string() ] ); @@ -588,6 +589,7 @@ if (!tool) { vec![ "exec".to_string(), "wait".to_string(), + "request_user_input".to_string(), "web_search".to_string(), "image_generation".to_string() ] diff --git a/codex-rs/core/tests/suite/model_runtime_selectors.rs b/codex-rs/core/tests/suite/model_runtime_selectors.rs index 38c95394ea69..3ee758372d22 100644 --- a/codex-rs/core/tests/suite/model_runtime_selectors.rs +++ b/codex-rs/core/tests/suite/model_runtime_selectors.rs @@ -173,6 +173,7 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> { // Code-mode entrypoints. codex_code_mode::PUBLIC_TOOL_NAME.to_string(), codex_code_mode::WAIT_TOOL_NAME.to_string(), + "request_user_input".to_string(), // Hosted Responses tools. "web_search".to_string(), "image_generation".to_string(),