Standardize shell execution on unified exec (#39757)

## What changed

- Remove the legacy `shell_command` handler and runtime, leaving `exec_command`
  and `write_stdin` as the shell execution tools.
- Treat legacy `shell_command` model metadata as `unified_exec`, and normalize
  legacy user opt-outs so they do not disable command execution. Managed feature
  requirements and `shell_tool` can still disable it.
- Preserve shell approvals, sandboxing, zsh-fork support, and output truncation
  through the unified execution path.

## Testing

- Cover legacy configuration and model-metadata compatibility.
- Exercise unified shell execution, approvals, truncation, and `apply_patch`
  serialization across the app-server and core test suites.

GitOrigin-RevId: 5c2fd6164fc3519cdae4944cb9db276b8467311c
This commit is contained in:
jif
2026-08-20 17:21:19 +00:00
committed by copyberry
parent d0cc662b8c
commit 8a40095ea3
116 changed files with 1338 additions and 4711 deletions

View File

@@ -17,7 +17,6 @@ codex-extension-items = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-string = { workspace = true }
jsonptr = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [

View File

@@ -71,14 +71,12 @@ pub use tool_call::ToolCall;
pub use tool_call::ToolEnvironment;
pub use tool_call::TurnItemEmissionFuture;
pub use tool_call::TurnItemEmitter;
pub use tool_config::ShellCommandBackendConfig;
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_command_backend_for_features;
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;

View File

@@ -7,20 +7,10 @@ use codex_protocol::openai_models::ModelInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ShellCommandBackendConfig {
Classic,
ZshFork,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum UnifiedExecFeatureMode {
/// Unified exec should not be selected by this feature set.
///
/// This includes standalone `shell_zsh_fork`: until
/// `unified_exec_zsh_fork` is enabled too, `shell_zsh_fork` keeps using
/// the shell command backend instead of silently opting unified exec into
/// zsh-fork interception.
Disabled,
Direct,
ZshFork,
@@ -46,24 +36,12 @@ pub fn request_user_input_available_modes(features: &Features) -> Vec<ModeKind>
.collect()
}
pub fn shell_command_backend_for_features(features: &Features) -> ShellCommandBackendConfig {
if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
ShellCommandBackendConfig::ZshFork
} else {
ShellCommandBackendConfig::Classic
}
}
/// 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.
///
/// `unified_exec_zsh_fork` is only a composition gate. It does not enable
/// either underlying shell mode on its own, so disabling `unified_exec` or
/// `shell_zsh_fork` keeps those features independently off. This lets
/// enterprise deployments opt into, or out of, unified exec and zsh-fork
/// behavior separately; otherwise enabling the composition flag would silently
/// activate a shell backend that the configured feature set left disabled.
/// 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
@@ -71,7 +49,7 @@ pub fn unified_exec_feature_mode_for_features(features: &Features) -> UnifiedExe
if features.enabled(Feature::UnifiedExecZshFork) {
UnifiedExecFeatureMode::ZshFork
} else {
UnifiedExecFeatureMode::Disabled
UnifiedExecFeatureMode::Direct
}
} else {
UnifiedExecFeatureMode::Direct
@@ -82,36 +60,13 @@ pub fn shell_type_for_model_and_features(
model_info: &ModelInfo,
features: &Features,
) -> ConfigShellToolType {
let unified_exec_feature_mode = unified_exec_feature_mode_for_features(features);
let unified_exec_disabled =
matches!(unified_exec_feature_mode, UnifiedExecFeatureMode::Disabled);
let model_shell_type = match model_info.shell_type {
ConfigShellToolType::UnifiedExec if unified_exec_disabled => {
ConfigShellToolType::ShellCommand
}
ConfigShellToolType::Default | ConfigShellToolType::Local => {
ConfigShellToolType::ShellCommand
}
other => other,
};
let shell_command_type = match shell_command_backend_for_features(features) {
ShellCommandBackendConfig::Classic => model_shell_type,
ShellCommandBackendConfig::ZshFork => ConfigShellToolType::ShellCommand,
};
if !features.enabled(Feature::ShellTool) {
if !features.enabled(Feature::ShellTool)
|| !features.enabled(Feature::UnifiedExec)
|| matches!(model_info.shell_type, ConfigShellToolType::Disabled)
{
ConfigShellToolType::Disabled
} else {
match unified_exec_feature_mode {
UnifiedExecFeatureMode::Disabled => shell_command_type,
UnifiedExecFeatureMode::Direct | UnifiedExecFeatureMode::ZshFork => {
if codex_utils_pty::conpty_supported() {
ConfigShellToolType::UnifiedExec
} else {
ConfigShellToolType::ShellCommand
}
}
}
ConfigShellToolType::UnifiedExec
}
}

View File

@@ -69,59 +69,31 @@ fn shell_features() -> Features {
fn shell_type_is_derived_from_model_and_feature_gates() {
let model = model_with_shell_type(ConfigShellToolType::UnifiedExec);
let mut features = shell_features();
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.enable(Feature::UnifiedExec);
let expected_unified_exec = if codex_utils_pty::conpty_supported() {
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::UnifiedExec
} else {
ConfigShellToolType::ShellCommand
};
assert_eq!(
shell_type_for_model_and_features(&model, &features),
expected_unified_exec
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.enable(Feature::UnifiedExecZshFork);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
expected_unified_exec
);
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_command_backend_requires_both_shell_tool_and_zsh_fork() {
let mut features = shell_features();
fn shell_type_respects_disabled_model_capability() {
let model = model_with_shell_type(ConfigShellToolType::Disabled);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::ZshFork
);
features.disable(Feature::ShellTool);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
shell_type_for_model_and_features(&model, &shell_features()),
ConfigShellToolType::Disabled
);
}
@@ -149,7 +121,7 @@ fn unified_exec_feature_mode_follows_composition_dependencies() {
features.disable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
UnifiedExecFeatureMode::Direct
);
features.enable(Feature::UnifiedExecZshFork);