Compare commits

...

6 Commits

Author SHA1 Message Date
Dylan Hurd
876029aff3 codex: preserve request-input mode ordering
Insert Default-mode availability ahead of Plan when feature overrides expand request_user_input support, keeping the generated tool description stable.

Co-authored-by: Codex <noreply@openai.com>
2026-05-12 21:45:54 -07:00
Dylan Hurd
48fff266b4 codex: preserve request-input modes on model override
Keep Default-mode request_user_input availability when turn-level feature overrides rebuild tool config after a model change.

Co-authored-by: Codex <noreply@openai.com>
2026-05-12 21:38:35 -07:00
Dylan Hurd
f84400da72 codex: fix CI failure on PR #22427
Co-authored-by: Codex <noreply@openai.com>
2026-05-12 21:29:45 -07:00
Dylan Hurd
88956cb633 codex: fix CI failure on PR #22427
Co-authored-by: Codex <noreply@openai.com>
2026-05-12 21:21:06 -07:00
Dylan Hurd
6fce878580 codex: fix CI failure on PR #22427
Co-authored-by: Codex <noreply@openai.com>
2026-05-12 21:12:10 -07:00
Dylan Hurd
65c9d24d19 chore(config) tools.request_user_input 2026-05-12 20:55:11 -07:00
14 changed files with 325 additions and 4 deletions

View File

@@ -38,6 +38,7 @@ use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use codex_model_provider_info::OPENAI_PROVIDER_ID;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
@@ -610,6 +611,22 @@ pub struct ToolsToml {
/// Enable the `view_image` tool that lets the agent attach local images.
#[serde(default)]
pub view_image: Option<bool>,
/// Configure registration and collaboration-mode availability for `request_user_input`.
#[serde(default)]
pub request_user_input: Option<RequestUserInputToolConfigToml>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct RequestUserInputToolConfigToml {
/// Whether to register the built-in `request_user_input` tool.
#[serde(default)]
pub enabled: Option<bool>,
/// Collaboration modes where `request_user_input` may be invoked.
#[serde(default)]
pub allowed_modes: Option<Vec<ModeKind>>,
}
#[derive(Deserialize)]

View File

@@ -59,6 +59,7 @@ pub use codex_protocol::ThreadId;
pub use codex_protocol::config_types::AltScreenMode;
pub use codex_protocol::config_types::ApprovalsReviewer;
pub use codex_protocol::config_types::CollaborationModeMask;
pub use codex_protocol::config_types::ModeKind;
pub use codex_protocol::config_types::ShellEnvironmentPolicy;
pub use codex_protocol::config_types::WebSearchMode;
pub use codex_protocol::dynamic_tools::DynamicToolSpec;

View File

@@ -1290,6 +1290,14 @@
},
"type": "object"
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"default"
],
"type": "string"
},
"ModelAvailabilityNuxConfig": {
"additionalProperties": {
"format": "uint32",
@@ -2276,6 +2284,25 @@
}
]
},
"RequestUserInputToolConfigToml": {
"additionalProperties": false,
"properties": {
"allowed_modes": {
"default": null,
"description": "Collaboration modes where `request_user_input` may be invoked.",
"items": {
"$ref": "#/definitions/ModeKind"
},
"type": "array"
},
"enabled": {
"default": null,
"description": "Whether to register the built-in `request_user_input` tool.",
"type": "boolean"
}
},
"type": "object"
},
"SandboxMode": {
"enum": [
"read-only",
@@ -2501,6 +2528,15 @@
"ToolsToml": {
"additionalProperties": false,
"properties": {
"request_user_input": {
"allOf": [
{
"$ref": "#/definitions/RequestUserInputToolConfigToml"
}
],
"default": null,
"description": "Configure registration and collaboration-mode availability for `request_user_input`."
},
"view_image": {
"default": null,
"description": "Enable the `view_image` tool that lets the agent attach local images.",

View File

@@ -61,11 +61,13 @@ use codex_config::types::WindowsToml;
use codex_core_plugins::PluginsManager;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use codex_model_provider_info::WireApi;
use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
@@ -381,6 +383,7 @@ web_search = true
Some(ToolsToml {
web_search: None,
view_image: None,
request_user_input: None,
})
);
}
@@ -400,10 +403,51 @@ web_search = false
Some(ToolsToml {
web_search: None,
view_image: None,
request_user_input: None,
})
);
}
#[test]
fn tools_request_user_input_deserializes_registration_and_modes() {
let cfg: ConfigToml = toml::from_str(
r#"
[tools.request_user_input]
enabled = false
allowed_modes = ["plan", "default"]
"#,
)
.expect("TOML deserialization should succeed");
let request_user_input = cfg
.tools
.and_then(|tools| tools.request_user_input)
.expect("request_user_input tool config should deserialize");
assert_eq!(request_user_input.enabled, Some(false));
assert_eq!(
request_user_input.allowed_modes,
Some(vec![ModeKind::Plan, ModeKind::Default])
);
}
#[test]
fn request_user_input_tool_config_defaults_to_plan_and_legacy_feature_adds_default() {
let cfg = ConfigToml::default();
let profile = ConfigProfile::default();
assert_eq!(
resolve_request_user_input_tool_config(&cfg, &profile, &Features::with_defaults()),
(true, vec![ModeKind::Plan])
);
let mut features = Features::with_defaults();
features.enable(Feature::DefaultModeRequestUserInput);
assert_eq!(
resolve_request_user_input_tool_config(&cfg, &profile, &features),
(true, vec![ModeKind::Plan, ModeKind::Default])
);
}
#[test]
fn rejects_provider_auth_with_env_key() {
let err = toml::from_str::<ConfigToml>(
@@ -7363,6 +7407,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
include_apply_patch_tool: true,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
request_user_input_tool_enabled: true,
request_user_input_allowed_modes: vec![ModeKind::Plan],
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -7810,6 +7856,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
include_apply_patch_tool: true,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
request_user_input_tool_enabled: true,
request_user_input_allowed_modes: vec![ModeKind::Plan],
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -7971,6 +8019,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
include_apply_patch_tool: true,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
request_user_input_tool_enabled: true,
request_user_input_allowed_modes: vec![ModeKind::Plan],
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -8117,6 +8167,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
include_apply_patch_tool: true,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
request_user_input_tool_enabled: true,
request_user_input_allowed_modes: vec![ModeKind::Plan],
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),

View File

@@ -78,6 +78,7 @@ use codex_model_provider_info::merge_configured_model_providers;
use codex_models_manager::ModelsManagerConfig;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
@@ -773,6 +774,12 @@ pub struct Config {
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// Whether to register the built-in request_user_input tool.
pub request_user_input_tool_enabled: bool,
/// Collaboration modes where request_user_input may be invoked.
pub request_user_input_allowed_modes: Vec<ModeKind>,
/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,
@@ -1957,6 +1964,38 @@ fn resolve_web_search_config(
}
}
fn resolve_request_user_input_tool_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
features: &Features,
) -> (bool, Vec<ModeKind>) {
let base = config_toml
.tools
.as_ref()
.and_then(|tools| tools.request_user_input.as_ref());
let profile = config_profile
.tools
.as_ref()
.and_then(|tools| tools.request_user_input.as_ref());
let enabled = profile
.and_then(|config| config.enabled)
.or_else(|| base.and_then(|config| config.enabled))
.unwrap_or(true);
let mut allowed_modes = profile
.and_then(|config| config.allowed_modes.clone())
.or_else(|| base.and_then(|config| config.allowed_modes.clone()))
.unwrap_or_else(|| vec![ModeKind::Plan]);
if features.enabled(Feature::DefaultModeRequestUserInput)
&& !allowed_modes.contains(&ModeKind::Default)
{
allowed_modes.push(ModeKind::Default);
}
(enabled, allowed_modes)
}
fn resolve_multi_agent_v2_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
@@ -2592,6 +2631,8 @@ impl Config {
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
let (request_user_input_tool_enabled, request_user_input_allowed_modes) =
resolve_request_user_input_tool_config(&cfg, &config_profile, &features);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) {
let base = apps_mcp_path_override_toml_config(cfg.features.as_ref());
@@ -3161,6 +3202,8 @@ impl Config {
include_apply_patch_tool: include_apply_patch_tool_flag,
web_search_mode: constrained_web_search_mode.value,
web_search_config,
request_user_input_tool_enabled,
request_user_input_allowed_modes,
use_experimental_unified_exec_tool,
background_terminal_max_timeout,
ghost_snapshot,

View File

@@ -51,6 +51,10 @@ pub(super) async fn spawn_review_thread(
sess.services.main_execve_wrapper_exe.as_ref(),
)
.with_web_search_config(/*web_search_config*/ None)
.with_request_user_input_config(
config.request_user_input_tool_enabled,
config.request_user_input_allowed_modes.clone(),
)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_environment_mode(parent_turn_context.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)

View File

@@ -5,6 +5,7 @@ use crate::environment_selection::ResolvedTurnEnvironments;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_protocol::SessionId;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -192,6 +193,13 @@ impl TurnContext {
/*developer_instructions*/ None,
);
let features = self.features.clone();
let mut request_user_input_available_modes =
self.tools_config.request_user_input_available_modes.clone();
if features.enabled(Feature::DefaultModeRequestUserInput)
&& !request_user_input_available_modes.contains(&ModeKind::Default)
{
request_user_input_available_modes.insert(0, ModeKind::Default);
}
let provider_capabilities = self.provider.capabilities();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
@@ -212,6 +220,10 @@ impl TurnContext {
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_request_user_input_config(
self.tools_config.request_user_input_tool_enabled,
request_user_input_available_modes,
)
.with_allow_login_shell(self.tools_config.allow_login_shell)
.with_environment_mode(self.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
@@ -472,6 +484,15 @@ impl Session {
let provider_for_context = create_model_provider(provider, auth_manager);
let provider_capabilities = provider_for_context.capabilities();
let session_telemetry_for_context = session_telemetry;
let mut request_user_input_allowed_modes =
per_turn_config.request_user_input_allowed_modes.clone();
if per_turn_config
.features
.enabled(Feature::DefaultModeRequestUserInput)
&& !request_user_input_allowed_modes.contains(&ModeKind::Default)
{
request_user_input_allowed_modes.insert(0, ModeKind::Default);
}
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager.try_list_models().unwrap_or_default(),
@@ -491,6 +512,10 @@ impl Session {
main_execve_wrapper_exe,
)
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_request_user_input_config(
per_turn_config.request_user_input_tool_enabled,
request_user_input_allowed_modes,
)
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_environment_mode(ToolEnvironmentMode::from_count(
environments.turn_environments.len(),

View File

@@ -332,9 +332,11 @@ fn collect_handler_tools(
handlers.push(Arc::new(UpdateGoalHandler));
}
handlers.push(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
if config.request_user_input_tool_enabled {
handlers.push(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
}
if config.request_permissions_tool_enabled {
handlers.push(Arc::new(RequestPermissionsHandler));

View File

@@ -868,6 +868,32 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
);
}
#[test]
fn request_user_input_registration_respects_tool_config() {
let model_info = model_info();
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_request_user_input_config(/*enabled*/ false, vec![ModeKind::Plan]);
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_lacks_tool_name(&tools, REQUEST_USER_INPUT_TOOL_NAME);
}
#[test]
fn request_permissions_requires_feature_flag() {
let model_info = model_info();

View File

@@ -203,3 +203,50 @@ async fn emits_deprecation_notice_for_use_legacy_landlock() -> anyhow::Result<()
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn emits_deprecation_notice_for_default_mode_request_user_input() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
let mut entries = BTreeMap::new();
entries.insert("default_mode_request_user_input".to_string(), true);
let mut features = config.features.get().clone();
features.apply_map(&entries);
config
.features
.set(features)
.expect("test config should allow managed feature map updates");
});
let TestCodex { codex, .. } = builder.build(&server).await?;
let notice = wait_for_event_match(&codex, |event| match event {
EventMsg::DeprecationNotice(ev)
if ev
.summary
.contains("[features].default_mode_request_user_input") =>
{
Some(ev.clone())
}
_ => None,
})
.await;
let DeprecationNoticeEvent { summary, details } = notice;
assert_eq!(
summary,
"`[features].default_mode_request_user_input` is deprecated and will be removed soon."
.to_string(),
);
assert_eq!(
details.as_deref(),
Some(
"Use `[tools.request_user_input].allowed_modes = [\"plan\", \"default\"]` in config.toml instead."
),
);
Ok(())
}

View File

@@ -437,6 +437,12 @@ impl Features {
Feature::UseLegacyLandlock,
);
}
"default_mode_request_user_input" => {
self.record_legacy_usage_force(
"features.default_mode_request_user_input",
Feature::DefaultModeRequestUserInput,
);
}
_ => {}
}
match feature_for_key(k) {
@@ -531,6 +537,17 @@ fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>
.to_string();
(summary, Some(details))
}
Feature::DefaultModeRequestUserInput => {
let label = match alias {
"features.default_mode_request_user_input" | "default_mode_request_user_input" => {
"[features].default_mode_request_user_input"
}
_ => alias,
};
let summary = format!("`{label}` is deprecated and will be removed soon.");
let details = "Use `[tools.request_user_input].allowed_modes = [\"plan\", \"default\"]` in config.toml instead.".to_string();
(summary, Some(details))
}
_ => {
let label = if alias.contains('.') || alias.starts_with('[') {
alias.to_string()
@@ -1062,7 +1079,7 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::DefaultModeRequestUserInput,
key: "default_mode_request_user_input",
stage: Stage::UnderDevelopment,
stage: Stage::Deprecated,
default_enabled: false,
},
FeatureSpec {

View File

@@ -119,6 +119,18 @@ fn request_permissions_tool_is_under_development() {
assert_eq!(Feature::RequestPermissionsTool.default_enabled(), false);
}
#[test]
fn default_mode_request_user_input_is_deprecated_and_disabled_by_default() {
assert_eq!(
Feature::DefaultModeRequestUserInput.stage(),
Stage::Deprecated
);
assert_eq!(
Feature::DefaultModeRequestUserInput.default_enabled(),
false
);
}
#[test]
fn remote_compaction_v2_is_under_development() {
assert_eq!(Feature::RemoteCompactionV2.stage(), Stage::UnderDevelopment);
@@ -248,6 +260,30 @@ fn use_legacy_landlock_config_records_deprecation_notice() {
);
}
#[test]
fn default_mode_request_user_input_config_records_deprecation_notice() {
let mut entries = BTreeMap::new();
entries.insert("default_mode_request_user_input".to_string(), true);
let mut features = Features::with_defaults();
features.apply_map(&entries);
let usages = features.legacy_feature_usages().collect::<Vec<_>>();
assert_eq!(usages.len(), 1);
assert_eq!(usages[0].alias, "features.default_mode_request_user_input");
assert_eq!(usages[0].feature, Feature::DefaultModeRequestUserInput);
assert_eq!(
usages[0].summary,
"`[features].default_mode_request_user_input` is deprecated and will be removed soon."
);
assert_eq!(
usages[0].details.as_deref(),
Some(
"Use `[tools.request_user_input].allowed_modes = [\"plan\", \"default\"]` in config.toml instead."
)
);
}
#[test]
fn image_detail_original_is_a_removed_feature_key() {
assert_eq!(

View File

@@ -26,6 +26,7 @@ use codex_core_api::Features;
use codex_core_api::GhostSnapshotConfig;
use codex_core_api::History;
use codex_core_api::MemoriesConfig;
use codex_core_api::ModeKind;
use codex_core_api::ModelAvailabilityNuxConfig;
use codex_core_api::MultiAgentV2Config;
use codex_core_api::NewThread;
@@ -265,6 +266,8 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
include_apply_patch_tool: false,
web_search_mode: Constrained::allow_any(WebSearchMode::Disabled),
web_search_config: None,
request_user_input_tool_enabled: true,
request_user_input_allowed_modes: vec![ModeKind::Plan],
use_experimental_unified_exec_tool: false,
background_terminal_max_timeout: 300_000,
ghost_snapshot: GhostSnapshotConfig::default(),

View File

@@ -122,6 +122,7 @@ pub struct ToolsConfig {
pub spawn_agent_usage_hint_text: Option<String>,
pub max_concurrent_threads_per_session: Option<usize>,
pub wait_agent_min_timeout_ms: Option<i64>,
pub request_user_input_tool_enabled: bool,
pub request_user_input_available_modes: Vec<ModeKind>,
pub experimental_supported_tools: Vec<String>,
pub agent_jobs_tools: bool,
@@ -259,6 +260,7 @@ impl ToolsConfig {
spawn_agent_usage_hint_text: None,
max_concurrent_threads_per_session: None,
wait_agent_min_timeout_ms: None,
request_user_input_tool_enabled: true,
request_user_input_available_modes: request_user_input_available_modes(features),
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
agent_jobs_tools: include_agent_jobs,
@@ -272,6 +274,16 @@ impl ToolsConfig {
self
}
pub fn with_request_user_input_config(
mut self,
enabled: bool,
available_modes: Vec<ModeKind>,
) -> Self {
self.request_user_input_tool_enabled = enabled;
self.request_user_input_available_modes = available_modes;
self
}
pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self {
if !namespace_tools {
self.namespace_tools = false;