diff --git a/.github/scripts/npm_alpha_tag.py b/.github/scripts/npm_alpha_tag.py new file mode 100644 index 0000000000..cef206ff83 --- /dev/null +++ b/.github/scripts/npm_alpha_tag.py @@ -0,0 +1,43 @@ +"""Choose a forward-only npm alpha dist-tag under rust-release's concurrency lock.""" + +import json +from pathlib import Path +import subprocess +import sys +import tarfile + +from releases import should_update_version + + +def npm_alpha_tag(tarball: Path, version: str, tag: str) -> str: + # Read the actual package name: platform archives share @openai/codex, + # while the SDK and proxy have independent alpha pointers. + with tarfile.open(tarball, "r:gz") as archive: + with archive.extractfile("package/package.json") as manifest: + package = json.load(manifest)["name"] + + result = subprocess.run( + ["npm", "view", package, "dist-tags", "--json", "--prefer-online"], + check=True, + capture_output=True, + text=True, + ) + current = json.loads(result.stdout).get(tag, "") + if tag != "alpha": + # build_npm_package.py appends - to the release version. + current = current.removesuffix(f"-{tag.removeprefix('alpha-')}") + if should_update_version(version, current): + return tag + + # npm publish always writes a tag. Keep this version installable without + # changing alpha, alpha-, or latest. Prefix avoids semver tags, + # which npm rejects. Include the platform for versions of the same package. + print( + f"Keeping {package}@{tag} at {current}; publishing {version} separately", + file=sys.stderr, + ) + return f"release-{version}-{tag}" + + +if __name__ == "__main__": + print(npm_alpha_tag(Path(sys.argv[1]), sys.argv[2], sys.argv[3])) diff --git a/.github/scripts/test_releases.py b/.github/scripts/test_releases.py index f012a911d5..2133724e63 100644 --- a/.github/scripts/test_releases.py +++ b/.github/scripts/test_releases.py @@ -1,5 +1,13 @@ +import io +import json +from pathlib import Path +import subprocess +import tarfile +import tempfile import unittest +from unittest.mock import patch +from npm_alpha_tag import npm_alpha_tag from releases import is_valid_release_version, should_update_version @@ -52,5 +60,92 @@ class VersionComparisonTest(unittest.TestCase): self.assertRaises(ValueError, should_update_version, "0.123", "") +class NpmAlphaTagTest(unittest.TestCase): + def test_published_tags_across_packages_and_platforms(self) -> None: + # Each package/tag reads its own registry value, even after a partially + # published release. Also exercise a missing pointer and dotted hotfix. + cases = [ + ( + "@openai/codex", + "alpha", + "0.158.0-alpha.10", + "0.157.0-alpha.11.1", + "release-0.157.0-alpha.11.1-alpha", + ), + ( + "@openai/codex", + "alpha-linux-x64", + "0.158.0-alpha.10-linux-x64", + "0.157.0-alpha.11.1", + "release-0.157.0-alpha.11.1-alpha-linux-x64", + ), + ( + "@openai/codex", + "alpha-win32-arm64", + "0.158.0-alpha.9-win32-arm64", + "0.158.0-alpha.10", + "alpha-win32-arm64", + ), + ( + "@openai/codex-sdk", + "alpha", + "0.158.0-alpha.10", + "0.158.0-alpha.10.1", + "alpha", + ), + ( + "@openai/codex-responses-api-proxy", + "alpha", + "0.159.0-alpha.1", + "0.158.0-alpha.10.1", + "release-0.158.0-alpha.10.1-alpha", + ), + ("@openai/codex-sdk", "alpha", None, "0.158.0-alpha.10", "alpha"), + ] + with tempfile.TemporaryDirectory() as tmpdir: + tarball = Path(tmpdir) / "package.tgz" + for package, tag, current, version, expected in cases: + with self.subTest(package=package, tag=tag, version=version): + content = json.dumps({"name": package}).encode() + with tarfile.open(tarball, "w:gz") as archive: + info = tarfile.TarInfo("package/package.json") + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + tags = {tag: current} if current is not None else {} + result = subprocess.CompletedProcess([], 0, json.dumps(tags)) + with patch( + "npm_alpha_tag.subprocess.run", return_value=result + ) as run: + self.assertEqual(npm_alpha_tag(tarball, version, tag), expected) + run.assert_called_once_with( + [ + "npm", + "view", + package, + "dist-tags", + "--json", + "--prefer-online", + ], + check=True, + capture_output=True, + text=True, + ) + + def test_registry_failure_prevents_publish(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + tarball = Path(tmpdir) / "package.tgz" + content = b'{"name": "@openai/codex"}' + with tarfile.open(tarball, "w:gz") as archive: + info = tarfile.TarInfo("package/package.json") + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + with patch( + "npm_alpha_tag.subprocess.run", + side_effect=subprocess.CalledProcessError(1, "npm"), + ): + with self.assertRaises(subprocess.CalledProcessError): + npm_alpha_tag(tarball, "0.158.0-alpha.10", "alpha") + + if __name__ == "__main__": unittest.main() diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 26d4985a15..c542015cfd 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -1778,6 +1778,11 @@ jobs: contents: read steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -1898,6 +1903,12 @@ jobs: ;; esac + # The workflow concurrency group holds the lock from this lookup + # through publish, including concurrent alpha/hotfix release runs. + if [[ "${NPM_TAG}" == "alpha" ]]; then + tag="$(python3 .github/scripts/npm_alpha_tag.py "${tarball}" "${VERSION}" "${tag}")" + fi + publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}") if [[ -n "${tag}" ]]; then publish_cmd+=(--tag "${tag}") diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 727ff73917..7145665cc5 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -13,6 +13,7 @@ use crate::shell::Shell; use crate::shell::ShellType; use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR; use codex_core_plugins::PLUGIN_METRICS_OUTPUT_ENV_VAR; +use codex_file_system::WindowsSandboxSelection; #[cfg(unix)] use codex_install_context::InstallContext; #[cfg(target_os = "macos")] @@ -25,7 +26,6 @@ use codex_network_proxy::PROXY_ENV_KEYS; use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; pub(crate) use codex_network_proxy::is_managed_proxy_env_var; pub(crate) use codex_network_proxy::strip_managed_proxy_env; -use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::shell_environment::is_non_inheritable_env_var; use codex_shell_command::shell_snapshot::posix_env_path_expansion_function; use codex_utils_absolute_path::AbsolutePathBuf; @@ -152,38 +152,37 @@ pub(crate) fn apply_zsh_fork_path_prepend( runtime_path_prepends.prepend(env, zsh_bin_dir); } -pub(crate) fn prepare_powershell_command_for_elevated_windows_sandbox( +pub(crate) fn prepare_powershell_command_for_windows_sandbox( command: &[String], shell_type: Option<&ShellType>, sandbox_requested: bool, - windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox: WindowsSandboxSelection, environment_is_remote: bool, ) -> Vec { - prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + prepare_powershell_command_for_windows_sandbox_with_fallback( command, shell_type, sandbox_requested, - windows_sandbox_level, + windows_sandbox, environment_is_remote, - |path| { - codex_shell_command::shell_detect::fallback_powershell_shell_for_elevated_windows_sandbox( - path, - ) - }, + codex_shell_command::shell_detect::fallback_powershell_shell_for_windows_sandbox, ) } -fn prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( +fn prepare_powershell_command_for_windows_sandbox_with_fallback( command: &[String], shell_type: Option<&ShellType>, sandbox_requested: bool, - windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox: WindowsSandboxSelection, environment_is_remote: bool, find_fallback: impl FnOnce(&Path) -> Option, ) -> Vec { if shell_type != Some(&ShellType::PowerShell) || !sandbox_requested - || windows_sandbox_level != WindowsSandboxLevel::Elevated + || !matches!( + windows_sandbox, + WindowsSandboxSelection::Elevated | WindowsSandboxSelection::Mxc + ) || command.is_empty() { return command.to_vec(); @@ -194,9 +193,10 @@ fn prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( command[0] = fallback.shell_path.to_string_lossy().to_string(); } - if command[1..] - .iter() - .any(|arg| arg.eq_ignore_ascii_case("-NoProfile")) + if windows_sandbox != WindowsSandboxSelection::Elevated + || command[1..] + .iter() + .any(|arg| arg.eq_ignore_ascii_case("-NoProfile")) { return command; } @@ -716,11 +716,11 @@ mod prepare_powershell_command_tests { "Write-Output ok".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + WindowsSandboxSelection::Elevated, /*environment_is_remote*/ false, ); @@ -743,11 +743,11 @@ mod prepare_powershell_command_tests { "VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIABvAGsA".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + WindowsSandboxSelection::Elevated, /*environment_is_remote*/ false, ); @@ -762,8 +762,9 @@ mod prepare_powershell_command_tests { ); } - #[test] - fn preserves_existing_no_profile() { + #[test_case::test_case(WindowsSandboxSelection::Elevated; "elevated")] + #[test_case::test_case(WindowsSandboxSelection::Mxc; "mxc")] + fn preserves_existing_no_profile(windows_sandbox: WindowsSandboxSelection) { let command = vec![ "pwsh.exe".to_string(), "-NoProfile".to_string(), @@ -771,11 +772,11 @@ mod prepare_powershell_command_tests { "Write-Output ok".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + windows_sandbox, /*environment_is_remote*/ false, ); @@ -791,19 +792,20 @@ mod prepare_powershell_command_tests { "Write-Output ok".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::RestrictedToken, + WindowsSandboxSelection::RestrictedToken, /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); } - #[test] - fn leaves_unsandboxed_attempts_alone() { + #[test_case::test_case(WindowsSandboxSelection::Elevated; "elevated")] + #[test_case::test_case(WindowsSandboxSelection::Mxc; "mxc")] + fn leaves_unsandboxed_attempts_alone(windows_sandbox: WindowsSandboxSelection) { let command = vec![ r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" .to_string(), @@ -811,37 +813,42 @@ mod prepare_powershell_command_tests { "Write-Output ok".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ false, - WindowsSandboxLevel::Elevated, + windows_sandbox, /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); } - #[test] - fn leaves_non_powershell_alone() { + #[test_case::test_case(WindowsSandboxSelection::Elevated; "elevated")] + #[test_case::test_case(WindowsSandboxSelection::Mxc; "mxc")] + fn leaves_non_powershell_alone(windows_sandbox: WindowsSandboxSelection) { let command = vec![ "/bin/bash".to_string(), "-lc".to_string(), "echo ok".to_string(), ]; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_windows_sandbox( &command, Some(&ShellType::Bash), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + windows_sandbox, /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); } - #[test] - fn local_elevated_powershell_uses_discovered_fallback() { + #[test_case::test_case(WindowsSandboxSelection::Elevated, &["-NoProfile", "-Command", "Write-Output ok"]; "elevated")] + #[test_case::test_case(WindowsSandboxSelection::Mxc, &["-Command", "Write-Output ok"]; "mxc")] + fn local_sandboxed_powershell_uses_discovered_fallback( + windows_sandbox: WindowsSandboxSelection, + expected_args: &[&str], + ) { let command = vec![ r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" .to_string(), @@ -850,11 +857,11 @@ mod prepare_powershell_command_tests { ]; let fallback_path = std::path::PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"); - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + let rewritten = prepare_powershell_command_for_windows_sandbox_with_fallback( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + windows_sandbox, /*environment_is_remote*/ false, |_| { Some(codex_shell_command::shell_detect::DetectedShell { @@ -866,17 +873,19 @@ mod prepare_powershell_command_tests { assert_eq!( rewritten, - vec![ - fallback_path.to_string_lossy().to_string(), - "-NoProfile".to_string(), - "-Command".to_string(), - "Write-Output ok".to_string(), - ] + vec![fallback_path.to_string_lossy().to_string()] + .into_iter() + .chain(expected_args.iter().map(std::string::ToString::to_string)) + .collect::>() ); } - #[test] - fn remote_elevated_powershell_keeps_remote_store_path_and_no_profile() { + #[test_case::test_case(WindowsSandboxSelection::Elevated, &["-NoProfile", "-Command", "Write-Output ok"]; "elevated")] + #[test_case::test_case(WindowsSandboxSelection::Mxc, &["-Command", "Write-Output ok"]; "mxc")] + fn remote_sandboxed_powershell_keeps_remote_store_path( + windows_sandbox: WindowsSandboxSelection, + expected_args: &[&str], + ) { let command = vec![ r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" .to_string(), @@ -885,11 +894,11 @@ mod prepare_powershell_command_tests { ]; let mut discovery_called = false; - let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + let rewritten = prepare_powershell_command_for_windows_sandbox_with_fallback( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, - WindowsSandboxLevel::Elevated, + windows_sandbox, /*environment_is_remote*/ true, |_| { discovery_called = true; @@ -903,12 +912,10 @@ mod prepare_powershell_command_tests { assert!(!discovery_called); assert_eq!( rewritten, - vec![ - command[0].clone(), - "-NoProfile".to_string(), - "-Command".to_string(), - "Write-Output ok".to_string(), - ] + vec![command[0].clone()] + .into_iter() + .chain(expected_args.iter().map(std::string::ToString::to_string)) + .collect::>() ); } } diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 722b8dffc1..8e7c256b56 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -24,7 +24,7 @@ use crate::tools::runtimes::RuntimePathPrepends; use crate::tools::runtimes::apply_zsh_fork_path_prepend; use crate::tools::runtimes::exec_env_for_sandbox_permissions; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; -use crate::tools::runtimes::prepare_powershell_command_for_elevated_windows_sandbox; +use crate::tools::runtimes::prepare_powershell_command_for_windows_sandbox; use crate::tools::runtimes::zsh_fork; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalAction; @@ -34,6 +34,7 @@ use crate::tools::sandboxing::Sandboxable; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; +use crate::tools::sandboxing::executor_windows_sandbox_selection; use crate::tools::sandboxing::managed_network_for_sandbox_permissions; use crate::tools::sandboxing::sandbox_permissions_preserving_denied_reads; use crate::unified_exec::NoopSpawnLifecycle; @@ -571,11 +572,17 @@ impl<'a> ToolRuntime for UnifiedExecRunt { network.restore_and_disable_brokered_credentials(&mut env, &mut command); } - let command = prepare_powershell_command_for_elevated_windows_sandbox( + // The executor selection is Disabled for non-Windows target paths, so this + // preparation is a no-op for PowerShell on other platforms. + let command = prepare_powershell_command_for_windows_sandbox( &command, Some(&req.shell_type), attempt.sandbox_requested, - attempt.windows_sandbox_level, + executor_windows_sandbox_selection( + attempt.windows_sandbox_type, + attempt.windows_sandbox_level, + attempt.sandbox_cwd, + ), environment_is_remote, ); let command = if matches!(req.shell_type, ShellType::PowerShell) { diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 54e98b75cc..ada5d56124 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -26,6 +26,9 @@ pub static CODEX_ALIASES_TEMP_DIR: Option = { if argv1 == Some(CODEX_FS_HELPER_ARG1) { return TestBinaryDispatchMode::DispatchArg0Only; } + if argv1 == Some(codex_sandboxing::CODEX_WINDOWS_MXC_ARG1) { + return TestBinaryDispatchMode::DispatchArg0Only; + } if exe_name == CODEX_LINUX_SANDBOX_ARG0 { return TestBinaryDispatchMode::DispatchArg0Only; } @@ -218,6 +221,9 @@ mod turn_state; mod unified_exec; #[path = "unified_exec_launch_failure_tests.rs"] mod unified_exec_launch_failure; +#[cfg(windows)] +#[path = "unified_exec_mxc_powershell_tests.rs"] +mod unified_exec_mxc_powershell; mod unified_exec_process_events; mod unified_exec_stdin_approval; mod unified_exec_stdin_review_size; diff --git a/codex-rs/core/tests/suite/unified_exec_mxc_powershell_tests.rs b/codex-rs/core/tests/suite/unified_exec_mxc_powershell_tests.rs new file mode 100644 index 0000000000..42939e5ff3 --- /dev/null +++ b/codex-rs/core/tests/suite/unified_exec_mxc_powershell_tests.rs @@ -0,0 +1,120 @@ +//! Exercises local PowerShell discovery before launching commands in native MXC. + +use anyhow::Context; +use anyhow::Result; +use codex_protocol::models::PermissionProfile; +use codex_shell_command::shell_detect::DetectedShell; +use codex_shell_command::shell_detect::ShellType; +use core_test_support::responses::mount_function_call_agent_response; +use core_test_support::test_codex::TestCodexHarness; +use core_test_support::test_codex::test_codex; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::path::PathBuf; +use std::time::Duration; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn mxc_replaces_store_powershell_before_launch() -> Result<()> { + if !codex_sandboxing::windows_mxc_available() { + eprintln!("skipping test: native MXC is unavailable on this host"); + return Ok(()); + } + + // The Store executable need not exist: the fallback must replace it before launch. + let shell = DetectedShell { + shell_type: ShellType::PowerShell, + shell_path: PathBuf::from( + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_test\pwsh.exe", + ), + }; + let builder = test_codex() + .with_user_shell(shell.into()) + .with_pre_build_hook(|home| { + std::fs::write(home.join("config.toml"), "[windows]\nsandbox = \"mxc\"\n") + .expect("write MXC config"); + }) + .with_config(|config| { + config.codex_self_exe = Some(std::env::current_exe().expect("test executable")); + }); + // Discovery is intentionally host-local; remote execution must not use the host's shell. + let harness = TestCodexHarness::with_builder(builder).await?; + let mock = mount_function_call_agent_response( + harness.server(), + "mxc-powershell", + &json!({ + "cmd": "Set-Content -LiteralPath fallback.txt -Value mxc-powershell -NoNewline", + "login": false, + "yield_time_ms": 10_000, + }) + .to_string(), + "exec_command", + ) + .await; + harness + .submit_with_permission_profile( + "Write a file using PowerShell in the sandbox.", + PermissionProfile::workspace_write(), + ) + .await?; + + let request = mock.completion.single_request(); + let tool_output = request.function_call_output("mxc-powershell"); + let mut output = tool_output["output"] + .as_str() + .expect("command output") + .to_owned(); + // A slow MXC launch may outlive exec_command's yield window. + tokio::time::timeout(Duration::from_secs(120), async { + for poll in 0.. { + let Some(session_id) = output + .lines() + .find_map(|line| line.strip_prefix("Process running with session ID ")) + else { + break; + }; + let session_id = session_id.parse::()?; + let call_id = format!("mxc-powershell-poll-{poll}"); + let poll_mock = mount_function_call_agent_response( + harness.server(), + &call_id, + &json!({ + "session_id": session_id, + "chars": "", + "yield_time_ms": 1_000, + }) + .to_string(), + "write_stdin", + ) + .await; + harness + .submit_with_permission_profile( + "Wait for the PowerShell command to finish.", + PermissionProfile::workspace_write(), + ) + .await?; + let request = poll_mock.completion.single_request(); + let tool_output = request.function_call_output(&call_id); + output = tool_output["output"] + .as_str() + .expect("poll output") + .to_owned(); + } + Ok::<_, anyhow::Error>(()) + }) + .await + .context("timed out waiting for MXC PowerShell to exit")??; + assert!(output.contains("Process exited with code 0"), "{output}"); + assert_eq!( + std::fs::read_to_string(harness.path("fallback.txt"))?, + "mxc-powershell" + ); + let metadata: serde_json::Value = serde_json::from_str( + &mock + .function_call + .single_request() + .header("x-codex-turn-metadata") + .expect("turn metadata"), + )?; + assert_eq!(metadata["sandbox"], "windows_mxc"); + Ok(()) +} diff --git a/codex-rs/shell-command/src/shell_detect.rs b/codex-rs/shell-command/src/shell_detect.rs index c74532cee3..c82d2634f6 100644 --- a/codex-rs/shell-command/src/shell_detect.rs +++ b/codex-rs/shell-command/src/shell_detect.rs @@ -132,7 +132,7 @@ fn file_exists(path: &std::path::Path) -> Option { } } -// Store PowerShell can be inaccessible to the elevated sandbox account; +// Store PowerShell cannot run under MXC and can be inaccessible to the elevated sandbox account; // WindowsApps also contains valid Codex frameworks. fn is_inaccessible_windows_apps_powershell_path(path: &std::path::Path) -> bool { path.as_os_str() @@ -156,7 +156,7 @@ fn targets_inaccessible_windows_apps_powershell(path: &std::path::Path) -> bool .is_some_and(|resolved| is_inaccessible_windows_apps_powershell_path(&resolved)) } -fn is_elevated_sandbox_compatible_powershell_path(path: &std::path::Path) -> bool { +fn is_windows_sandbox_compatible_powershell_path(path: &std::path::Path) -> bool { if !path .extension() .is_some_and(|extension| extension.eq_ignore_ascii_case("exe")) @@ -167,19 +167,19 @@ fn is_elevated_sandbox_compatible_powershell_path(path: &std::path::Path) -> boo !targets_inaccessible_windows_apps_powershell(path) } -fn get_elevated_sandbox_compatible_powershell_path( +fn get_windows_sandbox_compatible_powershell_path( binary_name: &str, fallback_paths: &[&str], ) -> Option { if let Ok(mut paths) = which::which_all(binary_name) - && let Some(path) = paths.find(|path| is_elevated_sandbox_compatible_powershell_path(path)) + && let Some(path) = paths.find(|path| is_windows_sandbox_compatible_powershell_path(path)) { return Some(path); } for path in fallback_paths { let path = std::path::Path::new(path); - if is_elevated_sandbox_compatible_powershell_path(path) + if is_windows_sandbox_compatible_powershell_path(path) && let Some(path) = file_exists(path) { return Some(path); @@ -281,21 +281,21 @@ fn get_powershell_shell() -> Option { } /// Returns a replacement only when shell_path targets Store PowerShell and -/// an elevated sandbox-compatible PowerShell executable can be discovered. +/// an unpackaged PowerShell executable can be discovered. /// -/// The caller owns the elevated-sandbox policy decision. Normal shell discovery +/// The caller decides whether the sandbox backend requires a fallback. Normal shell discovery /// intentionally keeps the user's ordered PATH selection unchanged. -pub fn fallback_powershell_shell_for_elevated_windows_sandbox( +pub fn fallback_powershell_shell_for_windows_sandbox( shell_path: &std::path::Path, ) -> Option { if !cfg!(windows) || !targets_inaccessible_windows_apps_powershell(shell_path) { return None; } - let shell_path = get_elevated_sandbox_compatible_powershell_path("pwsh", PWSH_FALLBACK_PATHS) + let shell_path = get_windows_sandbox_compatible_powershell_path("pwsh", PWSH_FALLBACK_PATHS) .or_else(|| { - get_elevated_sandbox_compatible_powershell_path("powershell", POWERSHELL_FALLBACK_PATHS) - })?; + get_windows_sandbox_compatible_powershell_path("powershell", POWERSHELL_FALLBACK_PATHS) + })?; Some(DetectedShell { shell_type: ShellType::PowerShell, @@ -385,7 +385,7 @@ mod tests { r"C:\PROGRAM FILES\WINDOWSAPPS\MICROSOFT.POWERSHELL\PWSH.EXE", r"C:\portable\pwsh.cmd", ] { - assert!(!is_elevated_sandbox_compatible_powershell_path( + assert!(!is_windows_sandbox_compatible_powershell_path( std::path::Path::new(path) )); } @@ -400,7 +400,7 @@ mod tests { r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", r"C:\portable\NotWindowsApps\pwsh.EXE", ] { - assert!(is_elevated_sandbox_compatible_powershell_path( + assert!(is_windows_sandbox_compatible_powershell_path( std::path::Path::new(path) )); } @@ -421,7 +421,7 @@ mod tests { PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"), ] .into_iter() - .find(|path| is_elevated_sandbox_compatible_powershell_path(path)); + .find(|path| is_windows_sandbox_compatible_powershell_path(path)); assert_eq!(found, Some(portable)); }