Disable env-bound tools when exec server is none (#16349)

## Summary
- make `CODEX_EXEC_SERVER_URL=none` map to an explicit disabled
environment mode instead of inferring from a missing URL
- expose environment capabilities (`exec_enabled`, `filesystem_enabled`)
so tool building can gate behavior explicitly and future
multi-environment work has a clearer seam
- suppress env-backed tools when the relevant capability is unavailable,
including exec tools, `js_repl`, `apply_patch`, `list_dir`, and
`view_image`
- keep handler/runtime backstops so disabled environments still reject
execution if a tool path somehow bypasses registration

## Testing
- `just fmt`
- `cargo test -p codex-exec-server`
- `cargo test -p codex-tools
disabled_environment_omits_environment_backed_tools`
- `cargo test -p codex-tools
environment_capabilities_gate_exec_and_filesystem_tools_independently`
- remote devbox Bazel build via `codex-applied-devbox`:
`//codex-rs/cli:cli`
This commit is contained in:
starr-openai
2026-04-06 17:22:06 -07:00
committed by GitHub
parent 9f737c28dd
commit a504d8f0fa
13 changed files with 258 additions and 119 deletions
+7
View File
@@ -86,6 +86,7 @@ pub struct ToolsConfig {
pub shell_type: ConfigShellToolType,
pub shell_command_backend: ShellCommandBackendConfig,
pub unified_exec_shell_mode: UnifiedExecShellMode,
pub has_environment: bool,
pub allow_login_shell: bool,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
@@ -200,6 +201,7 @@ impl ToolsConfig {
shell_type,
shell_command_backend,
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
has_environment: true,
allow_login_shell: true,
apply_patch_tool_type,
web_search_mode: *web_search_mode,
@@ -236,6 +238,11 @@ impl ToolsConfig {
self
}
pub fn with_has_environment(mut self, has_environment: bool) -> Self {
self.has_environment = has_environment;
self
}
pub fn with_unified_exec_shell_mode(
mut self,
unified_exec_shell_mode: UnifiedExecShellMode,
+66 -59
View File
@@ -107,54 +107,56 @@ pub fn build_tool_registry_plan(
);
}
match &config.shell_type {
ConfigShellToolType::Default => {
plan.push_spec(
create_shell_tool(ShellToolOptions {
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::Local => {
plan.push_spec(
create_local_shell_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::UnifiedExec => {
plan.push_spec(
create_exec_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.push_spec(
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec);
plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::ShellCommand => {
plan.push_spec(
create_shell_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
if config.has_environment {
match &config.shell_type {
ConfigShellToolType::Default => {
plan.push_spec(
create_shell_tool(ShellToolOptions {
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::Local => {
plan.push_spec(
create_local_shell_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::UnifiedExec => {
plan.push_spec(
create_exec_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.push_spec(
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec);
plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::ShellCommand => {
plan.push_spec(
create_shell_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
}
}
if config.shell_type != ConfigShellToolType::Disabled {
if config.has_environment && config.shell_type != ConfigShellToolType::Disabled {
plan.register_handler("shell", ToolHandlerKind::Shell);
plan.register_handler("container.exec", ToolHandlerKind::Shell);
plan.register_handler("local_shell", ToolHandlerKind::Shell);
@@ -189,7 +191,7 @@ pub fn build_tool_registry_plan(
);
plan.register_handler("update_plan", ToolHandlerKind::Plan);
if config.js_repl_enabled {
if config.has_environment && config.js_repl_enabled {
plan.push_spec(
create_js_repl_tool(),
/*supports_parallel_tool_calls*/ false,
@@ -265,7 +267,9 @@ pub fn build_tool_registry_plan(
plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest);
}
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
if config.has_environment
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
{
match apply_patch_tool_type {
ApplyPatchToolType::Freeform => {
plan.push_spec(
@@ -285,10 +289,11 @@ pub fn build_tool_registry_plan(
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
}
if config
.experimental_supported_tools
.iter()
.any(|tool| tool == "list_dir")
if config.has_environment
&& config
.experimental_supported_tools
.iter()
.any(|tool| tool == "list_dir")
{
plan.push_spec(
create_list_dir_tool(),
@@ -331,14 +336,16 @@ pub fn build_tool_registry_plan(
);
}
plan.push_spec(
create_view_image_tool(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("view_image", ToolHandlerKind::ViewImage);
if config.has_environment {
plan.push_spec(
create_view_image_tool(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("view_image", ToolHandlerKind::ViewImage);
}
if config.collab_tools {
if config.multi_agent_v2 {
@@ -453,6 +453,42 @@ fn view_image_tool_includes_detail_with_original_detail_feature() {
assert!(description.contains("omit this field for default resized behavior"));
}
#[test]
fn disabled_environment_omits_environment_backed_tools() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::JsRepl);
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_has_environment(/*has_environment*/ false);
tools_config
.experimental_supported_tools
.push("list_dir".to_string());
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
&[],
);
assert_lacks_tool_name(&tools, "exec_command");
assert_lacks_tool_name(&tools, "write_stdin");
assert_lacks_tool_name(&tools, "js_repl");
assert_lacks_tool_name(&tools, "js_repl_reset");
assert_lacks_tool_name(&tools, "apply_patch");
assert_lacks_tool_name(&tools, "list_dir");
assert_lacks_tool_name(&tools, VIEW_IMAGE_TOOL_NAME);
}
#[test]
fn test_build_specs_agent_job_worker_tools_enabled() {
let model_info = model_info();