mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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,
|
||||
));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user