Standardize shell execution on unified exec (#39772)

## What changed

- Use `exec_command` and `write_stdin` as the shell tool surface.
- Treat legacy `default`, `local`, and `shell_command` model metadata as `unified_exec`.
- Remove obsolete shell-selection configuration and runtime paths while preserving the feature and policy gates for zsh fork execution.

GitOrigin-RevId: d743cbe598630d73052f1fecad680c4cde17977d
This commit is contained in:
jif
2026-08-20 18:29:35 +00:00
committed by copyberry
parent 5bcd7b0fbc
commit bce5f2fcfc
40 changed files with 189 additions and 992 deletions
-3
View File
@@ -73,12 +73,9 @@ pub use tool_call::TurnItemEmissionFuture;
pub use tool_call::TurnItemEmitter;
pub use tool_config::ToolEnvironmentMode;
pub use tool_config::ToolUserShellType;
pub use tool_config::UnifiedExecFeatureMode;
pub use tool_config::UnifiedExecShellMode;
pub use tool_config::ZshForkConfig;
pub use tool_config::request_user_input_available_modes;
pub use tool_config::shell_type_for_model_and_features;
pub use tool_config::unified_exec_feature_mode_for_features;
pub use tool_definition::ToolDefinition;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
+5 -47
View File
@@ -2,20 +2,9 @@ use codex_features::Feature;
use codex_features::Features;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum UnifiedExecFeatureMode {
/// Unified exec should not be selected by this feature set.
///
Disabled,
Direct,
ZshFork,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolUserShellType {
Zsh,
@@ -36,40 +25,6 @@ pub fn request_user_input_available_modes(features: &Features) -> Vec<ModeKind>
.collect()
}
/// Returns the unified-exec mode requested by feature policy, before runtime
/// session inputs such as platform, user shell, and zsh-fork binary paths are
/// resolved.
///
/// Disabling unified exec keeps command execution disabled. The legacy
/// composition flag can still disable zsh-fork interception independently.
pub fn unified_exec_feature_mode_for_features(features: &Features) -> UnifiedExecFeatureMode {
if !features.enabled(Feature::ShellTool) || !features.enabled(Feature::UnifiedExec) {
UnifiedExecFeatureMode::Disabled
} else if features.enabled(Feature::ShellZshFork) {
if features.enabled(Feature::UnifiedExecZshFork) {
UnifiedExecFeatureMode::ZshFork
} else {
UnifiedExecFeatureMode::Direct
}
} else {
UnifiedExecFeatureMode::Direct
}
}
pub fn shell_type_for_model_and_features(
model_info: &ModelInfo,
features: &Features,
) -> ConfigShellToolType {
if !features.enabled(Feature::ShellTool)
|| !features.enabled(Feature::UnifiedExec)
|| matches!(model_info.shell_type, ConfigShellToolType::Disabled)
{
ConfigShellToolType::Disabled
} else {
ConfigShellToolType::UnifiedExec
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum UnifiedExecShellMode {
Direct,
@@ -84,13 +39,16 @@ pub struct ZshForkConfig {
impl UnifiedExecShellMode {
pub fn for_session(
feature_mode: UnifiedExecFeatureMode,
features: &Features,
user_shell_type: ToolUserShellType,
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
) -> Self {
if cfg!(unix)
&& matches!(feature_mode, UnifiedExecFeatureMode::ZshFork)
&& features.enabled(Feature::ShellTool)
&& features.enabled(Feature::UnifiedExec)
&& features.enabled(Feature::ShellZshFork)
&& features.enabled(Feature::UnifiedExecZshFork)
&& matches!(user_shell_type, ToolUserShellType::Zsh)
&& let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) =
(shell_zsh_path, main_execve_wrapper_exe)
+43 -136
View File
@@ -1,142 +1,10 @@
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationPolicyConfig;
use pretty_assertions::assert_eq;
use super::*;
fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
ModelInfo {
slug: "test-model".to_string(),
display_name: "Test Model".to_string(),
description: None,
default_reasoning_level: None,
supported_reasoning_levels: Vec::new(),
shell_type,
visibility: ModelVisibility::List,
supported_in_api: true,
priority: 0,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
default_service_tier: None,
availability_nux: None,
upgrade: None,
model_messages: None,
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
include_apps_usage_instructions: false,
supports_reasoning_summary_parameter: true,
default_reasoning_summary: Default::default(),
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::tokens(/*limit*/ 1024),
supports_image_detail_original: false,
context_window: None,
max_context_window: None,
auto_compact_token_limit: None,
comp_hash: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
input_modalities: codex_protocol::openai_models::default_input_modalities(),
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
multi_agent_version: None,
}
}
fn shell_features() -> Features {
let mut features = Features::with_defaults();
features.enable(Feature::ShellTool);
features.disable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExec);
features.disable(Feature::UnifiedExecZshFork);
features
}
#[test]
fn shell_type_is_derived_from_model_and_feature_gates() {
let model = model_with_shell_type(ConfigShellToolType::UnifiedExec);
let mut features = shell_features();
features.enable(Feature::UnifiedExec);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::UnifiedExec
);
features.disable(Feature::ShellTool);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::Disabled
);
features.enable(Feature::ShellTool);
features.disable(Feature::UnifiedExec);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::Disabled
);
}
#[test]
fn shell_type_respects_disabled_model_capability() {
let model = model_with_shell_type(ConfigShellToolType::Disabled);
assert_eq!(
shell_type_for_model_and_features(&model, &shell_features()),
ConfigShellToolType::Disabled
);
}
#[test]
fn unified_exec_feature_mode_follows_composition_dependencies() {
let mut features = shell_features();
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
);
features.enable(Feature::UnifiedExec);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Direct
);
features.enable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Direct
);
features.enable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Direct
);
features.enable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::ZshFork
);
features.disable(Feature::ShellTool);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
);
}
#[test]
fn request_user_input_modes_follow_default_mode_feature() {
let mut features = Features::with_defaults();
@@ -154,12 +22,14 @@ fn request_user_input_modes_follow_default_mode_feature() {
}
#[test]
fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() {
fn unified_exec_shell_mode_respects_feature_and_policy_gates() {
let exe = std::env::current_exe().expect("current exe path");
let shell = exe.clone();
let mut features = Features::with_defaults();
features.enable(Feature::ShellTool);
features.enable(Feature::ShellZshFork);
let mode = UnifiedExecShellMode::for_session(
UnifiedExecFeatureMode::ZshFork,
&features,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
@@ -170,9 +40,46 @@ fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() {
assert_eq!(mode, UnifiedExecShellMode::Direct);
}
features.disable(Feature::ShellZshFork);
assert_eq!(
UnifiedExecShellMode::for_session(
UnifiedExecFeatureMode::Direct,
&features,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
),
UnifiedExecShellMode::Direct
);
features.enable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExecZshFork);
assert_eq!(
UnifiedExecShellMode::for_session(
&features,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
),
UnifiedExecShellMode::Direct
);
features.enable(Feature::UnifiedExecZshFork);
features.disable(Feature::UnifiedExec);
assert_eq!(
UnifiedExecShellMode::for_session(
&features,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
),
UnifiedExecShellMode::Direct
);
features.enable(Feature::UnifiedExec);
features.disable(Feature::ShellTool);
assert_eq!(
UnifiedExecShellMode::for_session(
&features,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),