diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 3dbf483c2..0765819fb 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -87,6 +87,10 @@ const fn default_hide_agent_reasoning() -> Option { Some(false) } +const fn default_true() -> bool { + true +} + /// Backward-compatible shape for ChatGPT workspace login restrictions in config.toml. #[derive(Serialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(untagged)] @@ -617,6 +621,14 @@ pub struct ToolsToml { deserialize_with = "deserialize_optional_web_search_tool_config" )] pub web_search: Option, + pub experimental_request_user_input: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ExperimentalRequestUserInput { + #[serde(default = "default_true")] + pub enabled: bool, } #[derive(Deserialize)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2d7fd818c..77d2c97d6 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -763,6 +763,16 @@ }, "type": "object" }, + "ExperimentalRequestUserInput": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, "ExternalConfigMigrationPrompts": { "additionalProperties": false, "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", @@ -2712,6 +2722,9 @@ "ToolsToml": { "additionalProperties": false, "properties": { + "experimental_request_user_input": { + "$ref": "#/definitions/ExperimentalRequestUserInput" + }, "web_search": { "allOf": [ { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index cb72404ed..fa024c0bc 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -12,6 +12,7 @@ use codex_config::config_toml::AgentRoleToml; use codex_config::config_toml::AgentsToml; use codex_config::config_toml::AutoReviewToml; use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ExperimentalRequestUserInput; use codex_config::config_toml::ProjectConfig; use codex_config::config_toml::RealtimeConfig; use codex_config::config_toml::RealtimeToml; @@ -389,7 +390,13 @@ web_search = true ) .expect("TOML deserialization should succeed"); - assert_eq!(cfg.tools, Some(ToolsToml { web_search: None })); + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: None, + }) + ); } #[test] @@ -402,7 +409,72 @@ web_search = false ) .expect("TOML deserialization should succeed"); - assert_eq!(cfg.tools, Some(ToolsToml { web_search: None })); + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: None, + }) + ); +} + +#[test] +fn tools_experimental_request_user_input_defaults_to_enabled() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools.experimental_request_user_input] +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: true }), + }) + ); +} + +#[test] +fn tools_experimental_request_user_input_can_be_disabled() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools.experimental_request_user_input] +enabled = false +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: false }), + }) + ); +} + +#[tokio::test] +async fn load_config_resolves_experimental_request_user_input_enabled() -> std::io::Result<()> { + let codex_home = tempdir()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + tools: Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { + enabled: false, + }), + }), + ..ConfigToml::default() + }, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(!config.experimental_request_user_input_enabled); + Ok(()) } #[test] diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 482159dfb..9624f42eb 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -939,6 +939,9 @@ pub struct Config { /// Additional parameters for the web search tool when it is enabled. pub web_search_config: Option, + /// Whether to register the experimental request_user_input tool. + pub experimental_request_user_input_enabled: bool, + /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, @@ -2205,6 +2208,14 @@ fn resolve_web_search_config(config_toml: &ConfigToml) -> Option bool { + config_toml + .tools + .as_ref() + .and_then(|tools| tools.experimental_request_user_input.as_ref()) + .is_none_or(|config| config.enabled) +} + fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config { let base = multi_agent_v2_toml_config(config_toml.features.as_ref()); let default = MultiAgentV2Config::default(); @@ -2916,6 +2927,8 @@ impl Config { let web_search_mode = resolve_web_search_mode(&cfg, &features).unwrap_or(WebSearchMode::Cached); let web_search_config = resolve_web_search_config(&cfg); + let experimental_request_user_input_enabled = + resolve_experimental_request_user_input_enabled(&cfg); let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg); let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) { let base = apps_mcp_path_override_toml_config(cfg.features.as_ref()); @@ -3472,6 +3485,7 @@ impl Config { forced_login_method, web_search_mode: constrained_web_search_mode.value, web_search_config, + experimental_request_user_input_enabled, use_experimental_unified_exec_tool, background_terminal_max_timeout, ghost_snapshot, diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index c6fd80c0a..3c8abf8f1 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -600,9 +600,11 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut planned_tools.add(UpdateGoalHandler); } - planned_tools.add(RequestUserInputHandler { - available_modes: request_user_input_available_modes(features), - }); + if turn_context.config.experimental_request_user_input_enabled { + planned_tools.add(RequestUserInputHandler { + available_modes: request_user_input_available_modes(features), + }); + } if features.enabled(Feature::RequestPermissionsTool) { planned_tools.add(RequestPermissionsHandler); diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index e059b5f4a..45b965763 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -377,6 +377,22 @@ fn apply_patch_accepts_environment_id(spec: &ToolSpec) -> bool { } } +#[tokio::test] +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"]); + + let disabled = probe(|turn| { + update_config(turn, |config| { + config.experimental_request_user_input_enabled = false; + }); + }) + .await; + disabled.assert_visible_lacks(&["request_user_input"]); + disabled.assert_registered_lacks(&["request_user_input"]); +} + #[tokio::test] async fn shell_family_registers_visible_unified_exec_and_hidden_legacy_shell() { let plan = probe(|turn| { diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index fb407de3e..9305a6f66 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -265,6 +265,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R forced_login_method: None, web_search_mode: Constrained::allow_any(WebSearchMode::Disabled), web_search_config: None, + experimental_request_user_input_enabled: true, use_experimental_unified_exec_tool: false, background_terminal_max_timeout: 300_000, ghost_snapshot: GhostSnapshotConfig::default(),