Skip to content

Commit 335c7f8

Browse files
aibrahim-oaicopyberry
authored andcommitted
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
1 parent de3721a commit 335c7f8

16 files changed

Lines changed: 180 additions & 51 deletions

‎codex-rs/core/config.schema.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,9 @@
11511151
"sqlite": {
11521152
"type": "boolean"
11531153
},
1154+
"stable_environment_tools": {
1155+
"type": "boolean"
1156+
},
11541157
"standalone_web_search": {
11551158
"type": "boolean"
11561159
},
@@ -7318,6 +7321,9 @@
73187321
"sqlite": {
73197322
"type": "boolean"
73207323
},
7324+
"stable_environment_tools": {
7325+
"type": "boolean"
7326+
},
73217327
"standalone_web_search": {
73227328
"type": "boolean"
73237329
},

‎codex-rs/core/src/tools/handlers/apply_patch.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,9 @@ impl ApplyPatchHandler {
346346

347347
// Verify the parsed patch against the selected environment filesystem.
348348
let turn_environment = resolve_tool_environment(
349-
&step_context.environments,
349+
&step_context,
350350
selected_environment_id.as_deref(),
351+
"apply_patch is unavailable in this session",
351352
)?;
352353
let fs = turn_environment.environment.get_filesystem();
353354
let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None);

‎codex-rs/core/src/tools/handlers/mod.rs‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ mod view_image;
3636
pub(crate) mod view_image_spec;
3737
mod wait_for_environment;
3838

39+
use codex_features::Feature;
3940
use codex_file_system::FileSystemSandboxContext;
4041
use codex_sandboxing::policy_transforms::materialize_additional_permissions_with_context;
4142
use codex_sandboxing::policy_transforms::merge_permission_profiles;
@@ -47,7 +48,6 @@ use serde::Deserialize;
4748
use serde_json::Map;
4849
use serde_json::Value;
4950

50-
use crate::environment_selection::TurnEnvironmentSnapshot;
5151
use crate::function_tool::FunctionCallError;
5252
use crate::sandboxing::SandboxPermissions;
5353
use crate::session::step_context::StepContext;
@@ -158,9 +158,17 @@ where
158158
}
159159

160160
fn resolve_tool_environment<'a>(
161-
environments: &'a TurnEnvironmentSnapshot,
161+
step_context: &'a StepContext,
162162
environment_id: Option<&str>,
163+
legacy_unavailable_message: &'static str,
163164
) -> Result<&'a TurnEnvironment, FunctionCallError> {
165+
let environments = &step_context.environments;
166+
let stable_environment_tools = step_context
167+
.turn
168+
.config
169+
.features
170+
.get()
171+
.enabled(Feature::StableEnvironmentTools);
164172
environment_id.map_or_else(
165173
|| environments.primary(),
166174
|environment_id| {
@@ -170,19 +178,21 @@ fn resolve_tool_environment<'a>(
170178
},
171179
).ok_or_else(|| {
172180
if let Some(environment_id) = environment_id
173-
&& !environments
174-
.all_selections()
175-
.iter()
176-
.any(|selection| selection.environment_id == environment_id)
181+
&& (!stable_environment_tools
182+
|| !environments
183+
.all_selections()
184+
.iter()
185+
.any(|selection| selection.environment_id == environment_id))
177186
{
178187
return FunctionCallError::RespondToModel(format!(
179188
"unknown turn environment id `{environment_id}`"
180189
));
181190
}
182-
FunctionCallError::RespondToModel(
183-
"No usable execution environment is available. Wait for an environment to become available before using this tool."
184-
.to_string(),
185-
)
191+
FunctionCallError::RespondToModel(if stable_environment_tools {
192+
"No usable execution environment is available. Wait for an environment to become available before using this tool.".to_string()
193+
} else {
194+
legacy_unavailable_message.to_string()
195+
})
186196
})
187197
}
188198

‎codex-rs/core/src/tools/handlers/request_permissions.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ impl RequestPermissionsHandler {
6969

7070
let environment_args: RequestPermissionsEnvironmentArgs = parse_arguments(&arguments)?;
7171
let turn_environment = resolve_tool_environment(
72-
&step_context.environments,
72+
&step_context,
7373
environment_args.environment_id.as_deref(),
74+
"request_permissions requires a primary environment",
7475
)?;
7576
let sandbox_context =
7677
turn_environment.sandbox_context(/*additional_permissions*/ None);

‎codex-rs/core/src/tools/handlers/shell_spec.rs‎

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::collections::BTreeMap;
77

88
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99
pub struct CommandToolOptions {
10+
pub include_login_parameter: bool,
1011
pub exec_permission_approvals_enabled: bool,
1112
}
1213

@@ -15,13 +16,15 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec {
1516
create_exec_command_tool_with_environment_id(
1617
options,
1718
/*include_environment_id*/ false,
19+
/*include_shell_parameter*/ true,
1820
/*include_windows_shell_guidance*/ cfg!(windows),
1921
)
2022
}
2123

2224
pub(crate) fn create_exec_command_tool_with_environment_id(
2325
options: CommandToolOptions,
2426
include_environment_id: bool,
27+
include_shell_parameter: bool,
2528
include_windows_shell_guidance: bool,
2629
) -> ToolSpec {
2730
let yield_time_ms_description = if cfg!(windows) {
@@ -59,19 +62,23 @@ pub(crate) fn create_exec_command_tool_with_environment_id(
5962
)),
6063
),
6164
]);
62-
properties.insert(
63-
"shell".to_string(),
64-
JsonSchema::string(Some(
65-
"Shell binary to launch. Defaults to the user's default shell.".to_string(),
66-
)),
67-
);
68-
properties.insert(
69-
"login".to_string(),
70-
JsonSchema::boolean(Some(
71-
"True runs the shell with -l/-i semantics; false disables them. Defaults to true."
72-
.to_string(),
73-
)),
74-
);
65+
if include_shell_parameter {
66+
properties.insert(
67+
"shell".to_string(),
68+
JsonSchema::string(Some(
69+
"Shell binary to launch. Defaults to the user's default shell.".to_string(),
70+
)),
71+
);
72+
}
73+
if options.include_login_parameter {
74+
properties.insert(
75+
"login".to_string(),
76+
JsonSchema::boolean(Some(
77+
"True runs the shell with -l/-i semantics; false disables them. Defaults to true."
78+
.to_string(),
79+
)),
80+
);
81+
}
7582
if include_environment_id {
7683
properties.insert(
7784
"environment_id".to_string(),

‎codex-rs/core/src/tools/handlers/shell_spec_tests.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ fn windows_shell_guidance_description() -> String {
99
#[test]
1010
fn exec_command_tool_matches_expected_spec() {
1111
let tool = create_exec_command_tool(CommandToolOptions {
12+
include_login_parameter: true,
1213
exec_permission_approvals_enabled: false,
1314
});
1415

‎codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const EXEC_COMMAND_REJECTION_MAX_BYTES: usize = 900;
5858

5959
#[derive(Clone, Copy)]
6060
pub(crate) struct ExecCommandHandlerOptions {
61+
pub(crate) include_login_parameter: bool,
62+
pub(crate) include_shell_parameter: bool,
6163
pub(crate) allow_tty: bool,
6264
pub(crate) exec_permission_approvals_enabled: bool,
6365
pub(crate) include_environment_id: bool,
@@ -80,6 +82,8 @@ impl Default for ExecCommandHandler {
8082
Self {
8183
lifetime: ExecCommandLifetime::Interactive,
8284
options: ExecCommandHandlerOptions {
85+
include_login_parameter: false,
86+
include_shell_parameter: true,
8387
allow_tty: true,
8488
exec_permission_approvals_enabled: false,
8589
include_environment_id: false,
@@ -113,9 +117,11 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
113117
fn spec(&self) -> ToolSpec {
114118
let spec = create_exec_command_tool_with_environment_id(
115119
CommandToolOptions {
120+
include_login_parameter: self.options.include_login_parameter,
116121
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
117122
},
118123
self.options.include_environment_id,
124+
self.options.include_shell_parameter,
119125
self.options.include_windows_shell_guidance,
120126
);
121127
let mut spec = match self.lifetime {
@@ -179,8 +185,9 @@ impl ExecCommandHandler {
179185
);
180186
let environment_args: ExecCommandEnvironmentArgs = parse_arguments(&arguments)?;
181187
let turn_environment = resolve_tool_environment(
182-
&step_context.environments,
188+
&step_context,
183189
environment_args.environment_id.as_deref(),
190+
"unified exec is unavailable in this session",
184191
)?;
185192
let native_environment_cwd = turn_environment.cwd().clone();
186193
let cwd = environment_args

‎codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::function_tool::FunctionCallError;
2+
use codex_features::Feature;
3+
24
use crate::tools::context::ToolInvocation;
35
use crate::tools::context::ToolPayload;
46
use crate::tools::context::boxed_tool_output;
@@ -80,7 +82,18 @@ impl WriteStdinHandler {
8082
};
8183

8284
let args: WriteStdinArgs = parse_arguments(&arguments)?;
83-
resolve_tool_environment(&step_context.environments, /*environment_id*/ None)?;
85+
if turn
86+
.config
87+
.features
88+
.get()
89+
.enabled(Feature::StableEnvironmentTools)
90+
{
91+
resolve_tool_environment(
92+
&step_context,
93+
/*environment_id*/ None,
94+
"unified exec is unavailable in this session",
95+
)?;
96+
}
8497
let context =
8598
UnifiedExecContext::new(session.clone(), step_context, cancellation_token, call_id);
8699
let response = session

‎codex-rs/core/src/tools/handlers/view_image.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,11 @@ impl ViewImageHandler {
142142
}
143143
};
144144

145-
let turn_environment =
146-
resolve_tool_environment(&step_context.environments, environment_id.as_deref())?;
145+
let turn_environment = resolve_tool_environment(
146+
&step_context,
147+
environment_id.as_deref(),
148+
"view_image is unavailable in this session",
149+
)?;
147150
let path_uri = turn_environment.cwd().join(&path).map_err(|err| {
148151
FunctionCallError::RespondToModel(format!(
149152
"unable to resolve image path `{path}` against environment cwd `{}`: {err}",

‎codex-rs/core/src/tools/spec_plan.rs‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ use codex_tools::ToolExecutor;
8989
use codex_tools::ToolExposures;
9090
use codex_tools::ToolName;
9191
use codex_tools::ToolSpec;
92+
use codex_tools::UnifiedExecShellMode;
9293
use codex_tools::can_request_original_image_detail;
9394
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
9495
use codex_tools::collect_request_plugin_install_entries;
@@ -1079,9 +1080,20 @@ fn standalone_web_search_enabled(turn_context: &TurnContext, model_info: &ModelI
10791080
.enabled(Feature::StandaloneWebSearch))
10801081
}
10811082

1082-
fn tool_environment_mode(environments: &TurnEnvironmentSnapshot) -> ToolEnvironmentMode {
1083-
// Keep environment selectors stable as selected attachments change readiness.
1084-
ToolEnvironmentMode::from_count(environments.environments.len())
1083+
fn tool_environment_mode(context: &CoreToolPlanContext<'_>) -> ToolEnvironmentMode {
1084+
let count = if context
1085+
.turn_context
1086+
.config
1087+
.features
1088+
.get()
1089+
.enabled(Feature::StableEnvironmentTools)
1090+
{
1091+
// Keep selectors stable as selected attachments change readiness.
1092+
context.environments.environments.len()
1093+
} else {
1094+
context.environments.turn_environments().count()
1095+
};
1096+
ToolEnvironmentMode::from_count(count)
10851097
}
10861098

10871099
fn should_include_windows_shell_guidance(environments: &TurnEnvironmentSnapshot) -> bool {
@@ -1107,8 +1119,12 @@ fn should_include_windows_shell_guidance(environments: &TurnEnvironmentSnapshot)
11071119
fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) {
11081120
let turn_context = context.turn_context;
11091121
let features = turn_context.config.features.get();
1110-
let environment_mode = tool_environment_mode(context.environments);
1111-
if !features.enabled(Feature::ShellTool)
1122+
let environment_mode = tool_environment_mode(context);
1123+
let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools);
1124+
let advertise_environment_tools =
1125+
stable_environment_tools || environment_mode.has_environment();
1126+
if !advertise_environment_tools
1127+
|| !features.enabled(Feature::ShellTool)
11121128
|| matches!(context.model_info.shell_type, ConfigShellToolType::Disabled)
11131129
{
11141130
return;
@@ -1121,6 +1137,20 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistr
11211137
&& context.tool_policy.expose_additional_permissions;
11221138
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
11231139
let options = ExecCommandHandlerOptions {
1140+
include_login_parameter: stable_environment_tools
1141+
|| context
1142+
.environments
1143+
.turn_environments()
1144+
.any(|environment| environment.config().allow_login_shell),
1145+
include_shell_parameter: stable_environment_tools
1146+
|| !matches!(
1147+
&turn_context.unified_exec_shell_mode,
1148+
UnifiedExecShellMode::ZshFork(_)
1149+
)
1150+
|| context
1151+
.environments
1152+
.turn_environments()
1153+
.any(|environment| environment.environment.is_remote()),
11241154
allow_tty: features.enabled(Feature::UnifiedExecTty),
11251155
exec_permission_approvals_enabled,
11261156
include_environment_id,
@@ -1159,7 +1189,10 @@ fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
11591189
fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) {
11601190
let turn_context = context.turn_context;
11611191
let features = turn_context.config.features.get();
1162-
let environment_mode = tool_environment_mode(context.environments);
1192+
let environment_mode = tool_environment_mode(context);
1193+
let stable_environment_tools = features.enabled(Feature::StableEnvironmentTools);
1194+
let advertise_environment_tools =
1195+
stable_environment_tools || environment_mode.has_environment();
11631196

11641197
if turn_context.config.update_plan_enabled {
11651198
registry.add(PlanHandler);
@@ -1224,7 +1257,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
12241257
registry.add_with_exposure(SendMessageToUserAsyncHandler, ToolExposure::DirectModelOnly);
12251258
}
12261259

1227-
if features.enabled(Feature::RequestPermissionsTool) {
1260+
if advertise_environment_tools && features.enabled(Feature::RequestPermissionsTool) {
12281261
registry.add(RequestPermissionsHandler);
12291262
}
12301263

@@ -1277,7 +1310,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
12771310
));
12781311
}
12791312

1280-
if context.model_info.apply_patch_tool_type.is_some() {
1313+
if advertise_environment_tools && context.model_info.apply_patch_tool_type.is_some() {
12811314
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
12821315
registry.add(ApplyPatchHandler::new(include_environment_id));
12831316
}
@@ -1291,7 +1324,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
12911324
registry.add(TestSyncHandler);
12921325
}
12931326

1294-
if features.enabled(Feature::ViewImage) {
1327+
if advertise_environment_tools && features.enabled(Feature::ViewImage) {
12951328
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
12961329
registry.add(ViewImageHandler::new(ViewImageToolOptions {
12971330
can_request_original_image_detail: can_request_original_image_detail(

0 commit comments

Comments
 (0)