feat(config) experimental_request_user_input toggle (#24541)

## Summary
Experimental flag to allow toggling `request_user_input`:

```
tools.experimental_request_user_input = false
```

## Testing
- [x] Added unit tests
This commit is contained in:
Dylan Hurd
2026-05-29 21:35:53 -07:00
committed by GitHub
parent 00ca857d3f
commit e0435afb72
7 changed files with 135 additions and 5 deletions

View File

@@ -87,6 +87,10 @@ const fn default_hide_agent_reasoning() -> Option<bool> {
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<WebSearchToolConfig>,
pub experimental_request_user_input: Option<ExperimentalRequestUserInput>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ExperimentalRequestUserInput {
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Deserialize)]

View File

@@ -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": [
{

View File

@@ -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]

View File

@@ -939,6 +939,9 @@ pub struct Config {
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// 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<WebSearchConfig
.map(Into::into)
}
fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) -> 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,

View File

@@ -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);

View File

@@ -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| {

View File

@@ -265,6 +265,7 @@ fn new_config(model: Option<String>, 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(),