Route process tools to selected environments (#20647)

## Why
When a turn exposes multiple selected environments, shell-style tools
need a model-facing way to identify the intended target environment and
handlers need to resolve that target before parsing cwd-relative
permission fields or launching processes.

This PR scopes that rollout to process tools. Filesystem-oriented tools
such as `apply_patch`, `view_image`, and `list_dir` are intentionally
left for follow-up slices.

## What Changed
- Adds an `include_environment_id` option to shell-style tool schema
builders.
- Exposes optional `environment_id` on `shell`, `shell_command`, and
`exec_command` only when `ToolEnvironmentMode::Multiple` is active.
- Adds a shared handler helper that parses `environment_id` and
`workdir` from JSON function-call arguments and returns the selected
`Environment` plus effective absolute cwd.
- Uses that helper in `shell`, `shell_command`, and `exec_command`
handling so process execution uses the selected environment filesystem
and cwd.
- Changes `ExecCommandRequest` to carry a required resolved `cwd`,
removing the process-manager fallback to the primary turn cwd for new
exec commands.
- Leaves `write_stdin` unchanged because it targets an existing process
id, not a new environment.

## Testing
- Added unit coverage for process-tool schema exposure, selected
environment resolution, primary fallback, no-environment handling,
unknown environment ids, and resolving cwd-relative permission paths
against the selected environment cwd.
- Added a remote-suite e2e coverage case for `exec_command` routing
across explicit zero environments, one local environment, and
local+remote environments.
- Ran `just fmt` and `git diff --check`.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-05-05 12:12:03 -07:00
committed by GitHub
co-authored by Codex
parent fb7e1eb6fc
commit 78421face0
13 changed files with 313 additions and 91 deletions
+15
View File
@@ -17,6 +17,13 @@ pub struct ShellToolOptions {
}
pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec {
create_exec_command_tool_with_environment_id(options, /*include_environment_id*/ false)
}
pub(crate) fn create_exec_command_tool_with_environment_id(
options: CommandToolOptions,
include_environment_id: bool,
) -> ToolSpec {
let mut properties = BTreeMap::from([
(
"cmd".to_string(),
@@ -63,6 +70,14 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec {
)),
);
}
if include_environment_id {
properties.insert(
"environment_id".to_string(),
JsonSchema::string(Some(
"Optional environment id from the <environment_context> block. If omitted, uses the primary environment.".to_string(),
)),
);
}
properties.extend(create_approval_parameters(
options.exec_permission_approvals_enabled,
));
+11 -5
View File
@@ -7,6 +7,7 @@ use crate::ShellToolOptions;
use crate::SpawnAgentToolOptions;
use crate::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::TOOL_SEARCH_TOOL_NAME;
use crate::ToolEnvironmentMode;
use crate::ToolHandlerKind;
use crate::ToolName;
use crate::ToolRegistryPlan;
@@ -27,7 +28,6 @@ use crate::create_close_agent_tool_v1;
use crate::create_close_agent_tool_v2;
use crate::create_code_mode_tool;
use crate::create_create_goal_tool;
use crate::create_exec_command_tool;
use crate::create_followup_task_tool;
use crate::create_get_goal_tool;
use crate::create_image_generation_tool;
@@ -60,6 +60,7 @@ use crate::create_web_search_tool;
use crate::create_write_stdin_tool;
use crate::default_namespace_description;
use crate::dynamic_tool_to_loadable_tool_spec;
use crate::local_tool::create_exec_command_tool_with_environment_id;
use crate::mcp_tool_to_responses_api_tool;
use crate::request_permissions_tool_description;
use crate::request_user_input_tool_description;
@@ -135,6 +136,8 @@ pub fn build_tool_registry_plan(
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
match &config.shell_type {
ConfigShellToolType::Default => {
plan.push_spec(
@@ -154,10 +157,13 @@ pub fn build_tool_registry_plan(
}
ConfigShellToolType::UnifiedExec => {
plan.push_spec(
create_exec_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
create_exec_command_tool_with_environment_id(
CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
include_environment_id,
),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
@@ -19,6 +19,7 @@ use crate::ToolRegistryPlanDeferredTool;
use crate::ToolRegistryPlanMcpTool;
use crate::ToolsConfigParams;
use crate::WaitAgentTimeoutOptions;
use crate::create_exec_command_tool;
use crate::mcp_call_tool_result_output_schema;
use crate::request_user_input_available_modes;
use codex_app_server_protocol::AppInfo;
@@ -159,6 +160,49 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
}
}
#[test]
fn exec_command_spec_includes_environment_id_only_for_multiple_selected_environments() {
let model_info = model_info();
let available_models = Vec::new();
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
let 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,
});
let (single_environment_tools, _) = build_specs(
&config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_process_tool_environment_id(
&single_environment_tools,
"exec_command",
/*expected_present*/ false,
);
let multi_environment_config = config.with_environment_mode(ToolEnvironmentMode::Multiple);
let (multi_environment_tools, _) = build_specs(
&multi_environment_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
&[],
);
assert_process_tool_environment_id(
&multi_environment_tools,
"exec_command",
/*expected_present*/ true,
);
}
#[test]
fn test_build_specs_collab_tools_enabled() {
let model_info = model_info();
@@ -2424,6 +2468,23 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
}
fn assert_process_tool_environment_id(
tools: &[ConfiguredToolSpec],
expected_name: &str,
expected_present: bool,
) {
let tool = find_tool(tools, expected_name);
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &tool.spec else {
panic!("expected function tool {expected_name}");
};
let (properties, _) = expect_object_schema(parameters);
assert_eq!(
properties.contains_key("environment_id"),
expected_present,
"{expected_name} environment_id parameter presence"
);
}
fn find_namespace_function_tool<'a>(
tools: &'a [ConfiguredToolSpec],
expected_namespace: &str,