mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Backport MXC PowerShell fix and safe alpha publishing to 0.159.0-alpha.12 (#50061)
Backport two reviewed upstream fixes onto the exact content parent of 0.159.0-alpha.12 for the desktop 260930 train: - Public #49019 (internal #10589),4fd5745e84: reuse unpackaged PowerShell fallback for local MXC. Remote, unsandboxed, restricted-token handling and elevated-only -NoProfile behavior are preserved. - Public #49704,5602705c96: keep newer npm alpha pointers unchanged when publishing an older frozen hotfix. The existing OIDC publisher uses version-specific tags; registry lookup failure aborts publication. No new credentials or permission changes. Both patches apply cleanly and match their upstream patch IDs. Eight files total, +360/-71; no extra dependencies or unrelated product changes. Frozen base:1cc7e23612. Intended publication: 0.159.0-alpha.12.1 through the exact-SHA-guarded branch publisher. Validation: just fmt and whitespace checks passed; all 50 focused PowerShell library tests passed; all 17 release/tag-selection tests passed. Initial product-only head passed all three production build checks (Linux/macOS/Windows); updated-head checks are pending. Native MXC execution is not claimed by Linux validation. The included Windows integration test skips if MXC is unavailable. Initial CI manifest-policy failure reproduces on the exact frozen base; Codespell and cargo-shear failures concern unchanged files. No unrelated lint fixes included. --------- Co-authored-by: Ian MacLeod <imac@openai.com>
This commit is contained in:
co-authored by
Ian MacLeod
parent
1cc7e23612
commit
51215e9f88
@@ -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 -<platform> 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-<platform>, 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]))
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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<String> {
|
||||
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<codex_shell_command::shell_detect::DetectedShell>,
|
||||
) -> Vec<String> {
|
||||
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::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[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::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UnifiedExecRequest, UnifiedExecAttempt> 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) {
|
||||
|
||||
@@ -26,6 +26,9 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestBinaryDispatchGuard> = {
|
||||
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;
|
||||
|
||||
@@ -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::<u32>()?;
|
||||
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(())
|
||||
}
|
||||
@@ -132,7 +132,7 @@ fn file_exists(path: &std::path::Path) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<PathBuf> {
|
||||
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<DetectedShell> {
|
||||
}
|
||||
|
||||
/// 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<DetectedShell> {
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user