feat: gate unified exec zsh fork composition (#24979)

## Why

`shell_zsh_fork` and unified exec need to remain independently
controllable for enterprise rollouts, but we also need a third mode that
composes them. That composed mode is intended to preserve unified exec
command lifecycle support while letting the zsh fork provide more
accurate `execv(2)` interception.

Enabling `unified_exec_zsh_fork` by itself is intentionally not
sufficient. It is a composition gate, not a dependency-enabling
shortcut:

- `unified_exec` selects the PTY-backed unified exec tool.
- `shell_zsh_fork` opts into the zsh fork backend.
- `unified_exec_zsh_fork` only allows those two already-enabled modes to
be composed so local zsh unified exec commands can launch through the
zsh fork.

This separation is deliberate. Enterprises and staged rollouts must be
able to enable or disable unified exec and zsh-fork independently. If
`unified_exec_zsh_fork` implied either dependency, then enabling one
under-development composition flag would silently activate a shell
backend that the configured feature set left disabled.

This PR introduces only the configuration and planning gate for that
composition. Existing `shell_zsh_fork` behavior continues to use the
standalone shell tool unless the new composition feature is explicitly
enabled alongside both dependencies.

## What Changed

- Added the under-development feature flag `unified_exec_zsh_fork`.
- Added `UnifiedExecFeatureMode` so the three input feature flags
collapse into `Disabled`, `Direct`, or `ZshFork` mode before tool
planning.
- Updated tool selection so zsh-fork composition requires
`unified_exec`, `shell_zsh_fork`, and `unified_exec_zsh_fork`.
- Kept the existing standalone zsh-fork shell tool behavior when only
`shell_zsh_fork` is enabled.
- Updated config schema output for the new feature flag.

## Verification

- Added feature and tool-config coverage for the new gate.
- Added planner coverage proving `shell_zsh_fork` remains standalone
until composition is explicitly enabled.
- Ran focused tests for `codex-features`, `codex-tools`, and the
affected `codex-core` planner case.





---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/24979).
* #24982
* #24981
* #24980
* __->__ #24979
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-06-01 13:01:36 -07:00
1 parent 009e6c4817
commit d6748f741a
9 files changed
+168 -21

No files matched your search

+57 -13
View File
@@ -13,6 +13,19 @@ pub enum ShellCommandBackendConfig {
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,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolUserShellType {
Zsh,
@@ -41,13 +54,39 @@ pub fn shell_command_backend_for_features(features: &Features) -> ShellCommandBa
}
}
/// 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.
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::Disabled
}
} else {
UnifiedExecFeatureMode::Direct
}
}
pub fn shell_type_for_model_and_features(
model_info: &ModelInfo,
features: &Features,
) -> ConfigShellToolType {
let unified_exec_enabled = features.enabled(Feature::UnifiedExec);
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_enabled => {
ConfigShellToolType::UnifiedExec if unified_exec_disabled => {
ConfigShellToolType::ShellCommand
}
ConfigShellToolType::Default | ConfigShellToolType::Local => {
@@ -55,19 +94,24 @@ pub fn shell_type_for_model_and_features(
}
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) {
ConfigShellToolType::Disabled
} else if features.enabled(Feature::ShellZshFork) {
ConfigShellToolType::ShellCommand
} else if unified_exec_enabled {
if codex_utils_pty::conpty_supported() {
ConfigShellToolType::UnifiedExec
} else {
ConfigShellToolType::ShellCommand
}
} else {
model_shell_type
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
}
}
}
}
}
@@ -85,13 +129,13 @@ pub struct ZshForkConfig {
impl UnifiedExecShellMode {
pub fn for_session(
shell_command_backend: ShellCommandBackendConfig,
feature_mode: UnifiedExecFeatureMode,
user_shell_type: ToolUserShellType,
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
) -> Self {
if cfg!(unix)
&& shell_command_backend == ShellCommandBackendConfig::ZshFork
&& matches!(feature_mode, UnifiedExecFeatureMode::ZshFork)
&& matches!(user_shell_type, ToolUserShellType::Zsh)
&& let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) =
(shell_zsh_path, main_execve_wrapper_exe)