From 335c7f8ecab29c4462a553bbf174276d2c67a766 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sun, 4 Oct 2026 20:58:36 +0000 Subject: [PATCH] Gate stable environment tool exposure behind a feature flag (#50962) ## What changed Add the default-off `stable_environment_tools` feature flag. When enabled, advertise environment-backed tools before an executor is ready, keep environment selectors stable as readiness changes, and include the `shell` and `login` parameters. Execution still requires a usable environment and its policy. When disabled, expose environment-backed tools only when a usable environment exists, derive selectors from usable environments, and condition shell parameters on the execution configuration. Restore tool-specific unavailable-environment errors and skip the environment readiness check for `write_stdin`. ## Testing Parameterize the empty-environment tool availability test for both flag states, check that commands require a ready transport by default, and explicitly opt tests that rely on stable tool exposure into the feature. GitOrigin-RevId: 68c6240fb3adf85d7f9b407804d4823c515abfdc --- codex-rs/core/config.schema.json | 6 +++ .../core/src/tools/handlers/apply_patch.rs | 3 +- codex-rs/core/src/tools/handlers/mod.rs | 30 +++++++---- .../src/tools/handlers/request_permissions.rs | 3 +- .../core/src/tools/handlers/shell_spec.rs | 33 +++++++----- .../src/tools/handlers/shell_spec_tests.rs | 1 + .../handlers/unified_exec/exec_command.rs | 9 +++- .../handlers/unified_exec/write_stdin.rs | 15 +++++- .../core/src/tools/handlers/view_image.rs | 7 ++- codex-rs/core/src/tools/spec_plan.rs | 51 +++++++++++++++---- codex-rs/core/tests/suite/remote_env.rs | 14 +++-- codex-rs/core/tests/suite/scenarios.rs | 11 ++-- codex-rs/core/tests/suite/skills_extension.rs | 4 ++ ...catalog_dedup__dedup_before_budgeting.snap | 4 +- codex-rs/core/tests/suite/tools.rs | 31 +++++++++-- codex-rs/features/src/lib.rs | 9 ++++ 16 files changed, 180 insertions(+), 51 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index cced3a9174..b832da2f80 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1151,6 +1151,9 @@ "sqlite": { "type": "boolean" }, + "stable_environment_tools": { + "type": "boolean" + }, "standalone_web_search": { "type": "boolean" }, @@ -7318,6 +7321,9 @@ "sqlite": { "type": "boolean" }, + "stable_environment_tools": { + "type": "boolean" + }, "standalone_web_search": { "type": "boolean" }, diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 5b0bea21c3..800358c92a 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -346,8 +346,9 @@ impl ApplyPatchHandler { // Verify the parsed patch against the selected environment filesystem. let turn_environment = resolve_tool_environment( - &step_context.environments, + &step_context, selected_environment_id.as_deref(), + "apply_patch is unavailable in this session", )?; let fs = turn_environment.environment.get_filesystem(); let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None); diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index a4d92223c3..710c089b8a 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -36,6 +36,7 @@ mod view_image; pub(crate) mod view_image_spec; mod wait_for_environment; +use codex_features::Feature; use codex_file_system::FileSystemSandboxContext; use codex_sandboxing::policy_transforms::materialize_additional_permissions_with_context; use codex_sandboxing::policy_transforms::merge_permission_profiles; @@ -47,7 +48,6 @@ use serde::Deserialize; use serde_json::Map; use serde_json::Value; -use crate::environment_selection::TurnEnvironmentSnapshot; use crate::function_tool::FunctionCallError; use crate::sandboxing::SandboxPermissions; use crate::session::step_context::StepContext; @@ -158,9 +158,17 @@ where } fn resolve_tool_environment<'a>( - environments: &'a TurnEnvironmentSnapshot, + step_context: &'a StepContext, environment_id: Option<&str>, + legacy_unavailable_message: &'static str, ) -> Result<&'a TurnEnvironment, FunctionCallError> { + let environments = &step_context.environments; + let stable_environment_tools = step_context + .turn + .config + .features + .get() + .enabled(Feature::StableEnvironmentTools); environment_id.map_or_else( || environments.primary(), |environment_id| { @@ -170,19 +178,21 @@ fn resolve_tool_environment<'a>( }, ).ok_or_else(|| { if let Some(environment_id) = environment_id - && !environments - .all_selections() - .iter() - .any(|selection| selection.environment_id == environment_id) + && (!stable_environment_tools + || !environments + .all_selections() + .iter() + .any(|selection| selection.environment_id == environment_id)) { return FunctionCallError::RespondToModel(format!( "unknown turn environment id `{environment_id}`" )); } - FunctionCallError::RespondToModel( - "No usable execution environment is available. Wait for an environment to become available before using this tool." - .to_string(), - ) + FunctionCallError::RespondToModel(if stable_environment_tools { + "No usable execution environment is available. Wait for an environment to become available before using this tool.".to_string() + } else { + legacy_unavailable_message.to_string() + }) }) } diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index b393766fb1..1c776a2546 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -69,8 +69,9 @@ impl RequestPermissionsHandler { let environment_args: RequestPermissionsEnvironmentArgs = parse_arguments(&arguments)?; let turn_environment = resolve_tool_environment( - &step_context.environments, + &step_context, environment_args.environment_id.as_deref(), + "request_permissions requires a primary environment", )?; let sandbox_context = turn_environment.sandbox_context(/*additional_permissions*/ None); diff --git a/codex-rs/core/src/tools/handlers/shell_spec.rs b/codex-rs/core/src/tools/handlers/shell_spec.rs index ca48b21220..ac3dcf9926 100644 --- a/codex-rs/core/src/tools/handlers/shell_spec.rs +++ b/codex-rs/core/src/tools/handlers/shell_spec.rs @@ -7,6 +7,7 @@ use std::collections::BTreeMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CommandToolOptions { + pub include_login_parameter: bool, pub exec_permission_approvals_enabled: bool, } @@ -15,6 +16,7 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec { create_exec_command_tool_with_environment_id( options, /*include_environment_id*/ false, + /*include_shell_parameter*/ true, /*include_windows_shell_guidance*/ cfg!(windows), ) } @@ -22,6 +24,7 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec { pub(crate) fn create_exec_command_tool_with_environment_id( options: CommandToolOptions, include_environment_id: bool, + include_shell_parameter: bool, include_windows_shell_guidance: bool, ) -> ToolSpec { let yield_time_ms_description = if cfg!(windows) { @@ -59,19 +62,23 @@ pub(crate) fn create_exec_command_tool_with_environment_id( )), ), ]); - properties.insert( - "shell".to_string(), - JsonSchema::string(Some( - "Shell binary to launch. Defaults to the user's default shell.".to_string(), - )), - ); - properties.insert( - "login".to_string(), - JsonSchema::boolean(Some( - "True runs the shell with -l/-i semantics; false disables them. Defaults to true." - .to_string(), - )), - ); + if include_shell_parameter { + properties.insert( + "shell".to_string(), + JsonSchema::string(Some( + "Shell binary to launch. Defaults to the user's default shell.".to_string(), + )), + ); + } + if options.include_login_parameter { + properties.insert( + "login".to_string(), + JsonSchema::boolean(Some( + "True runs the shell with -l/-i semantics; false disables them. Defaults to true." + .to_string(), + )), + ); + } if include_environment_id { properties.insert( "environment_id".to_string(), diff --git a/codex-rs/core/src/tools/handlers/shell_spec_tests.rs b/codex-rs/core/src/tools/handlers/shell_spec_tests.rs index 7f8007b17f..c3d1afffff 100644 --- a/codex-rs/core/src/tools/handlers/shell_spec_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_spec_tests.rs @@ -9,6 +9,7 @@ fn windows_shell_guidance_description() -> String { #[test] fn exec_command_tool_matches_expected_spec() { let tool = create_exec_command_tool(CommandToolOptions { + include_login_parameter: true, exec_permission_approvals_enabled: false, }); diff --git a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs index 56e5aff362..faf8b90848 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs @@ -58,6 +58,8 @@ const EXEC_COMMAND_REJECTION_MAX_BYTES: usize = 900; #[derive(Clone, Copy)] pub(crate) struct ExecCommandHandlerOptions { + pub(crate) include_login_parameter: bool, + pub(crate) include_shell_parameter: bool, pub(crate) allow_tty: bool, pub(crate) exec_permission_approvals_enabled: bool, pub(crate) include_environment_id: bool, @@ -80,6 +82,8 @@ impl Default for ExecCommandHandler { Self { lifetime: ExecCommandLifetime::Interactive, options: ExecCommandHandlerOptions { + include_login_parameter: false, + include_shell_parameter: true, allow_tty: true, exec_permission_approvals_enabled: false, include_environment_id: false, @@ -113,9 +117,11 @@ impl ToolExecutor for ExecCommandHandler { fn spec(&self) -> ToolSpec { let spec = create_exec_command_tool_with_environment_id( CommandToolOptions { + include_login_parameter: self.options.include_login_parameter, exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled, }, self.options.include_environment_id, + self.options.include_shell_parameter, self.options.include_windows_shell_guidance, ); let mut spec = match self.lifetime { @@ -179,8 +185,9 @@ impl ExecCommandHandler { ); let environment_args: ExecCommandEnvironmentArgs = parse_arguments(&arguments)?; let turn_environment = resolve_tool_environment( - &step_context.environments, + &step_context, environment_args.environment_id.as_deref(), + "unified exec is unavailable in this session", )?; let native_environment_cwd = turn_environment.cwd().clone(); let cwd = environment_args diff --git a/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs b/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs index 1fecb98b04..3a6aa8d96f 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs @@ -1,4 +1,6 @@ use crate::function_tool::FunctionCallError; +use codex_features::Feature; + use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::context::boxed_tool_output; @@ -80,7 +82,18 @@ impl WriteStdinHandler { }; let args: WriteStdinArgs = parse_arguments(&arguments)?; - resolve_tool_environment(&step_context.environments, /*environment_id*/ None)?; + if turn + .config + .features + .get() + .enabled(Feature::StableEnvironmentTools) + { + resolve_tool_environment( + &step_context, + /*environment_id*/ None, + "unified exec is unavailable in this session", + )?; + } let context = UnifiedExecContext::new(session.clone(), step_context, cancellation_token, call_id); let response = session diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 903df019f9..25468f3749 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -142,8 +142,11 @@ impl ViewImageHandler { } }; - let turn_environment = - resolve_tool_environment(&step_context.environments, environment_id.as_deref())?; + let turn_environment = resolve_tool_environment( + &step_context, + environment_id.as_deref(), + "view_image is unavailable in this session", + )?; let path_uri = turn_environment.cwd().join(&path).map_err(|err| { FunctionCallError::RespondToModel(format!( "unable to resolve image path `{path}` against environment cwd `{}`: {err}", diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 7b95414c05..e5ab1500ee 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -89,6 +89,7 @@ use codex_tools::ToolExecutor; use codex_tools::ToolExposures; use codex_tools::ToolName; use codex_tools::ToolSpec; +use codex_tools::UnifiedExecShellMode; use codex_tools::can_request_original_image_detail; use codex_tools::collect_code_mode_exec_prompt_tool_definitions; use codex_tools::collect_request_plugin_install_entries; @@ -1079,9 +1080,20 @@ fn standalone_web_search_enabled(turn_context: &TurnContext, model_info: &ModelI .enabled(Feature::StandaloneWebSearch)) } -fn tool_environment_mode(environments: &TurnEnvironmentSnapshot) -> ToolEnvironmentMode { - // Keep environment selectors stable as selected attachments change readiness. - ToolEnvironmentMode::from_count(environments.environments.len()) +fn tool_environment_mode(context: &CoreToolPlanContext<'_>) -> ToolEnvironmentMode { + let count = if context + .turn_context + .config + .features + .get() + .enabled(Feature::StableEnvironmentTools) + { + // Keep selectors stable as selected attachments change readiness. + context.environments.environments.len() + } else { + context.environments.turn_environments().count() + }; + ToolEnvironmentMode::from_count(count) } fn should_include_windows_shell_guidance(environments: &TurnEnvironmentSnapshot) -> bool { @@ -1107,8 +1119,12 @@ fn should_include_windows_shell_guidance(environments: &TurnEnvironmentSnapshot) fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { let turn_context = context.turn_context; let features = turn_context.config.features.get(); - let environment_mode = tool_environment_mode(context.environments); - if !features.enabled(Feature::ShellTool) + let environment_mode = tool_environment_mode(context); + let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools); + let advertise_environment_tools = + stable_environment_tools || environment_mode.has_environment(); + if !advertise_environment_tools + || !features.enabled(Feature::ShellTool) || matches!(context.model_info.shell_type, ConfigShellToolType::Disabled) { return; @@ -1121,6 +1137,20 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistr && context.tool_policy.expose_additional_permissions; let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); let options = ExecCommandHandlerOptions { + include_login_parameter: stable_environment_tools + || context + .environments + .turn_environments() + .any(|environment| environment.config().allow_login_shell), + include_shell_parameter: stable_environment_tools + || !matches!( + &turn_context.unified_exec_shell_mode, + UnifiedExecShellMode::ZshFork(_) + ) + || context + .environments + .turn_environments() + .any(|environment| environment.environment.is_remote()), allow_tty: features.enabled(Feature::UnifiedExecTty), exec_permission_approvals_enabled, include_environment_id, @@ -1159,7 +1189,10 @@ fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { let turn_context = context.turn_context; let features = turn_context.config.features.get(); - let environment_mode = tool_environment_mode(context.environments); + let environment_mode = tool_environment_mode(context); + let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools); + let advertise_environment_tools = + stable_environment_tools || environment_mode.has_environment(); if turn_context.config.update_plan_enabled { registry.add(PlanHandler); @@ -1224,7 +1257,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool registry.add_with_exposure(SendMessageToUserAsyncHandler, ToolExposure::DirectModelOnly); } - if features.enabled(Feature::RequestPermissionsTool) { + if advertise_environment_tools && features.enabled(Feature::RequestPermissionsTool) { registry.add(RequestPermissionsHandler); } @@ -1277,7 +1310,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool )); } - if context.model_info.apply_patch_tool_type.is_some() { + if advertise_environment_tools && context.model_info.apply_patch_tool_type.is_some() { let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); registry.add(ApplyPatchHandler::new(include_environment_id)); } @@ -1291,7 +1324,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool registry.add(TestSyncHandler); } - if features.enabled(Feature::ViewImage) { + if advertise_environment_tools && features.enabled(Feature::ViewImage) { let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); registry.add(ViewImageHandler::new(ViewImageToolOptions { can_request_original_image_detail: can_request_original_image_detail( diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 31cca21194..a60c2e5d6f 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -1907,6 +1907,10 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes() let mut builder = test_codex() .with_extensions(Arc::new(extensions.build())) .with_config(|config| { + config + .features + .enable(Feature::StableEnvironmentTools) + .expect("enable stable environment tools"); assert!(config.features.enable(Feature::DeferredExecutor).is_ok()); if cfg!(unix) { assert!(config.features.enable(Feature::ShellZshFork).is_ok()); @@ -2830,7 +2834,6 @@ async fn ready_before_selection_resolves_resumed_thread_capability_root_after_wa // The first request may legally see either Starting or Ready; the wait makes step two ready. let first_tools = tool_names(&requests[0].body_json()); assert!(first_tools.contains(&"wait_for_environment".to_string())); - assert!(first_tools.contains(&"exec_command".to_string())); let first_user_context = requests[0].message_input_texts("user"); let first_environment_context = first_user_context .iter() @@ -2839,8 +2842,13 @@ async fn ready_before_selection_resolves_resumed_thread_capability_root_after_wa let first_has_ready_root = first_user_context .iter() .any(|text| text.contains("ready-first-root")); - // Tool availability no longer distinguishes Starting from Ready. - if first_environment_context.contains("starting") { + let first_is_starting = first_environment_context.contains("starting"); + assert_eq!( + first_tools.contains(&"exec_command".to_string()), + !first_is_starting, + "without stable_environment_tools, commands require a ready transport" + ); + if first_is_starting { assert!(!first_has_ready_root); } else { assert!(first_environment_context.contains("")); diff --git a/codex-rs/core/tests/suite/scenarios.rs b/codex-rs/core/tests/suite/scenarios.rs index 13080f7daa..7b69faff01 100644 --- a/codex-rs/core/tests/suite/scenarios.rs +++ b/codex-rs/core/tests/suite/scenarios.rs @@ -1710,9 +1710,14 @@ async fn subagent_waits_for_its_inherited_environment_configuration() -> Result< use super::remote_env::spawn_tests::PendingSpawnCase; use super::remote_env::spawn_tests::pending_subagent_scenario; - let requests = - pending_subagent_scenario(PendingSpawnCase::TurnSettings, configure_scenario_catalog) - .await?; + let requests = pending_subagent_scenario(PendingSpawnCase::TurnSettings, |config| { + configure_scenario_catalog(config); + config + .features + .enable(Feature::StableEnvironmentTools) + .expect("enable stable environment tools"); + }) + .await?; let labels = [ "parent delegates while the environment is starting", "parent waits for the worker", diff --git a/codex-rs/core/tests/suite/skills_extension.rs b/codex-rs/core/tests/suite/skills_extension.rs index 0385577e82..be32ec9232 100644 --- a/codex-rs/core/tests/suite/skills_extension.rs +++ b/codex-rs/core/tests/suite/skills_extension.rs @@ -3062,6 +3062,10 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r model_info.max_context_window = None; }) .with_config(|config| { + config + .features + .enable(Feature::StableEnvironmentTools) + .expect("enable stable environment tools"); configure_catalog_test(config); config.cloud_skill_enabled = true; config.model_provider.name = "Skills compaction test".to_string(); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_before_budgeting.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_before_budgeting.snap index f03b58dab6..449e2a34b6 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_before_budgeting.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__skill_catalog_dedup__dedup_before_budgeting.snap @@ -16,8 +16,8 @@ Settings: text: {"verbosity":"low"} tool_choice: "auto" -- request 1 (turn) -- -00:additional_tools/developer (3; hash=91d402a1b9d55c34): - - namespace/functions; hash=7d26e086c1dbba28 +00:additional_tools/developer (3; hash=b6ebf543280ec271): + - namespace/functions; hash=bd9e182b4235cdc1 - custom/exec - function/wait - function/request_user_input diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index a963ebdc60..62ab267fa3 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -244,8 +244,12 @@ async fn strict_tool_collisions_do_not_duplicate_unrelated_compaction_errors() - Ok(()) } +#[test_case::test_case(false; "default_off")] +#[test_case::test_case(true; "opted_in")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn empty_turn_environments_keeps_environment_backed_tools() -> Result<()> { +async fn empty_turn_environments_gate_environment_backed_tools( + stable_environment_tools: bool, +) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -259,7 +263,17 @@ async fn empty_turn_environments_keeps_environment_backed_tools() -> Result<()> ) .await; - let mut builder = test_codex().with_config(|config| { + let mut builder = test_codex().with_config(move |config| { + if stable_environment_tools { + config + .features + .enable(Feature::StableEnvironmentTools) + .expect("enable stable environment tools"); + } + config + .features + .enable(Feature::RequestPermissionsTool) + .expect("enable permissions tool"); config.update_plan_enabled = true; config .features @@ -276,10 +290,17 @@ async fn empty_turn_environments_keeps_environment_backed_tools() -> Result<()> tools.contains(&"update_plan".to_string()), "non-environment tool should remain available; got {tools:?}" ); - for environment_tool in ["exec_command", "write_stdin", "apply_patch", "view_image"] { - assert!( + for environment_tool in [ + "exec_command", + "write_stdin", + "apply_patch", + "view_image", + "request_permissions", + ] { + assert_eq!( tools.contains(&environment_tool.to_string()), - "{environment_tool} should remain available for explicit empty turn environments; got {tools:?}" + stable_environment_tools, + "unexpected {environment_tool} availability for empty environments; got {tools:?}" ); } diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 8d7fa099ad..118852725e 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -116,6 +116,9 @@ pub enum Feature { DaemonAutoStart, // Experimental + /// Advertise enabled environment-backed tools and their shell parameters before the + /// executor is ready. Actual execution still requires a usable environment and its policy. + StableEnvironmentTools, /// Send per-content-entry classifications in internal Responses metadata. ContentItemKinds, /// Record model-attempted tool calls in internal Responses metadata. @@ -1095,6 +1098,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::StableEnvironmentTools, + key: "stable_environment_tools", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CodeMode, key: "code_mode",