diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 2c3c831ac2..5ffa42dc8e 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -636,6 +636,7 @@ pub struct ToolsToml { )] pub web_search: Option, pub experimental_request_user_input: Option, + pub update_plan: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] @@ -645,6 +646,13 @@ pub struct ExperimentalRequestUserInput { pub enabled: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct UpdatePlanToolConfig { + #[serde(default = "default_true")] + pub enabled: bool, +} + #[derive(Deserialize)] #[serde(untagged)] enum WebSearchToolConfigInput { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 4f8b8ff392..39d0057456 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -3147,6 +3147,9 @@ "experimental_request_user_input": { "$ref": "#/definitions/ExperimentalRequestUserInput" }, + "update_plan": { + "$ref": "#/definitions/UpdatePlanToolConfig" + }, "web_search": { "allOf": [ { @@ -4648,6 +4651,16 @@ }, "type": "object" }, + "UpdatePlanToolConfig": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, "UriBasedFileOpener": { "oneOf": [ { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9d8b0ea097..f9a4dfc43c 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -361,6 +361,7 @@ web_search = true Some(ToolsToml { web_search: None, experimental_request_user_input: None, + update_plan: None, }) ); } @@ -380,6 +381,7 @@ web_search = false Some(ToolsToml { web_search: None, experimental_request_user_input: None, + update_plan: None, }) ); } @@ -398,6 +400,7 @@ fn tools_experimental_request_user_input_defaults_to_enabled() { Some(ToolsToml { web_search: None, experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: true }), + update_plan: None, }) ); } @@ -417,6 +420,7 @@ enabled = false Some(ToolsToml { web_search: None, experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: false }), + update_plan: None, }) ); } @@ -431,6 +435,7 @@ async fn load_config_resolves_experimental_request_user_input_enabled() -> std:: experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: false, }), + update_plan: None, }), ..ConfigToml::default() }, @@ -502,6 +507,27 @@ async fn load_config_resolves_non_prefixed_mcp_tool_servers() -> std::io::Result Ok(()) } +#[tokio::test] +async fn load_config_resolves_update_plan_enabled() -> std::io::Result<()> { + let codex_home = tempdir()?; + let config_toml = toml::from_str( + r#" +[tools.update_plan] +enabled = false +"#, + ) + .expect("TOML deserialization should succeed"); + let config = Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(!config.update_plan_enabled); + Ok(()) +} + #[tokio::test] async fn load_config_resolves_code_mode_config() -> std::io::Result<()> { let codex_home = tempdir()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e310bf7c42..6ef45cb91b 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1030,6 +1030,9 @@ pub struct Config { /// Whether to register the experimental request_user_input tool. pub experimental_request_user_input_enabled: bool, + /// Whether to register the update_plan tool. + pub update_plan_enabled: bool, + /// Configuration for the experimental code-mode tool surface. pub code_mode: CodeModeConfig, @@ -2537,6 +2540,14 @@ fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) -> .is_none_or(|config| config.enabled) } +fn resolve_update_plan_enabled(config_toml: &ConfigToml) -> bool { + config_toml + .tools + .as_ref() + .and_then(|tools| tools.update_plan.as_ref()) + .is_none_or(|config| config.enabled) +} + fn resolve_orchestrator_feature_enabled( feature: Option<&codex_config::config_toml::OrchestratorFeatureToml>, ) -> bool { @@ -3577,6 +3588,7 @@ impl Config { let web_search_config = resolve_web_search_config(&cfg); let experimental_request_user_input_enabled = resolve_experimental_request_user_input_enabled(&cfg); + let update_plan_enabled = resolve_update_plan_enabled(&cfg); let code_mode = resolve_code_mode_config(&cfg); let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg); let token_budget = resolve_token_budget_config(&cfg, &features)?; @@ -4108,6 +4120,7 @@ impl Config { web_search_mode: constrained_web_search_mode.value, web_search_config, experimental_request_user_input_enabled, + update_plan_enabled, code_mode, use_experimental_unified_exec_tool, background_terminal_max_timeout, diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 7bf871b561..0b6657d06d 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -713,7 +713,9 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut let features = turn_context.config.features.get(); let environment_mode = tool_environment_mode(context.environments); - planned_tools.add(PlanHandler); + if turn_context.config.update_plan_enabled { + planned_tools.add(PlanHandler); + } if features.enabled(Feature::DeferredExecutor) { planned_tools.add(WaitForEnvironmentHandler); diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index c6e9ad51ff..a0948b65fa 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -466,6 +466,22 @@ async fn request_user_input_tool_respects_experimental_config_gate() { disabled.assert_registered_lacks(&["request_user_input"]); } +#[tokio::test] +async fn update_plan_tool_respects_config_gate() { + let enabled = probe(|_| {}).await; + enabled.assert_visible_contains(&["update_plan"]); + enabled.assert_registered_contains(&["update_plan"]); + + let disabled = probe(|turn| { + update_config(turn, |config| { + config.update_plan_enabled = false; + }); + }) + .await; + disabled.assert_visible_lacks(&["update_plan"]); + disabled.assert_registered_lacks(&["update_plan"]); +} + #[tokio::test] async fn request_user_input_stays_direct_in_code_mode_only() { 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 ede1a8ef4b..22c5fc385c 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -294,6 +294,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R web_search_mode: Constrained::allow_any(WebSearchMode::Disabled), web_search_config: None, experimental_request_user_input_enabled: true, + update_plan_enabled: true, code_mode: Default::default(), use_experimental_unified_exec_tool: false, background_terminal_max_timeout: 300_000,