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 authored and copyberry committed 2026-08-20 17:46:05 +00:00
1 parent d0cc662b8c
commit 8a40095ea3
116 files changed
+1338 -4711

No files matched your search

+76
View File
@@ -6445,6 +6445,50 @@ async fn legacy_toggles_map_to_features() -> std::io::Result<()> {
Ok(())
}
#[tokio::test]
async fn legacy_unified_exec_disable_flags_do_not_disable_command_execution() -> std::io::Result<()>
{
for cfg in [
ConfigToml {
features: Some(FeaturesToml::from(BTreeMap::from([(
"unified_exec".to_string(),
false,
)]))),
..Default::default()
},
ConfigToml {
experimental_use_unified_exec_tool: Some(false),
..Default::default()
},
] {
let codex_home = TempDir::new()?;
let mut config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert!(config.features.enabled(Feature::UnifiedExec));
assert!(config.features.enabled(Feature::ShellTool));
config
.features
.disable(Feature::UnifiedExec)
.expect("legacy unified-exec toggle should normalize successfully");
assert!(config.features.enabled(Feature::UnifiedExec));
config
.features
.disable(Feature::ShellTool)
.expect("shell tool should remain independently configurable");
assert!(!config.features.enabled(Feature::ShellTool));
assert!(config.features.enabled(Feature::UnifiedExec));
}
Ok(())
}
#[tokio::test]
async fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> {
for feature_key in ["responses_websockets", "responses_websockets_v2"] {
@@ -10977,6 +11021,38 @@ shell_tool = false
Ok(())
}
#[tokio::test]
async fn feature_requirements_can_still_disable_unified_exec() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let mut config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
unified_exec = false
shell_tool = true
"#,
),
)
.build()
.await?;
assert!(!config.features.enabled(Feature::UnifiedExec));
assert!(config.features.enabled(Feature::ShellTool));
config
.features
.enable(Feature::UnifiedExec)
.expect("managed feature mutations should normalize successfully");
assert!(!config.features.enabled(Feature::UnifiedExec));
assert!(config.features.enabled(Feature::ShellTool));
Ok(())
}
#[tokio::test]
async fn feature_requirements_auto_review_disables_guardian_approval() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -152,6 +152,12 @@ fn normalize_candidate(
mut candidate: Features,
pinned_features: &BTreeMap<Feature, bool>,
) -> Features {
// Legacy user opt-outs selected the removed shell backend. Only managed
// requirements may disable the remaining unified-exec implementation.
if !pinned_features.contains_key(&Feature::UnifiedExec) {
candidate.enable(Feature::UnifiedExec);
}
for (feature, enabled) in pinned_features {
candidate.set_enabled(*feature, *enabled);
}