From cf2c04b325e1929732727e7776243226fbbe19bc Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Tue, 27 Oct 2026 23:06:56 +0000 Subject: [PATCH] Ship `codex-exec-server` in CLI and app-server packages (#57590) ## What changed - Build and bundle the standalone executor in `bin/` for both package variants, and advertise its path through `execServer` in `codex-package.json`. - Include the executor in Cargo and Bazel release packaging, macOS signing and notarization, and Windows signing verification. Preserve prebuilt signed binaries during packaging. - Make installers detect a missing advertised executor while continuing to accept older packages without it. ## Testing Add coverage for executor archive contents and executable permissions, signed binary preservation, and macOS entitlement verification. Add package smoke tests for initialization, version reporting, filesystem and process sandbox enforcement, and startup error parity with `codex exec-server`. GitOrigin-RevId: 5c0027fc19f16cf9c2a5bb52dbd065a9776bc2af --- .../actions/build-codex-packages/action.yml | 8 +- .../scripts/build-codex-package-archive.sh | 2 + .../codex-exec-server.entitlements.plist | 5 + .../provisioned_macos_cli_package.py | 2 +- .../macos-signing/sign_macos_cli_package.py | 7 + .../test_sign_macos_cli_package.py | 30 +- .github/workflows/rust-release-windows.yml | 30 +- .github/workflows/rust-release.yml | 62 +++-- codex-rs/BUILD.bazel | 3 + codex-rs/exec-server-cli/BUILD.bazel | 6 + codex-rs/exec-server/README.md | 2 + scripts/codex_package/README.md | 15 +- scripts/codex_package/cargo.py | 18 +- scripts/codex_package/cli.py | 12 + scripts/codex_package/layout.py | 8 + scripts/codex_package/smoke_tests/fixtures.py | 7 +- .../smoke_tests/test_codex_package.py | 260 +++++++++++++++++- scripts/codex_package/targets.py | 1 + scripts/codex_package/test_cargo.py | 8 +- scripts/codex_package/test_layout.py | 37 ++- scripts/install/install.ps1 | 10 + scripts/install/install.sh | 7 + scripts/install/test_install_sh.py | 12 +- 23 files changed, 497 insertions(+), 55 deletions(-) create mode 100644 .github/scripts/macos-signing/codex-exec-server.entitlements.plist diff --git a/.github/actions/build-codex-packages/action.yml b/.github/actions/build-codex-packages/action.yml index 86d9fdeae8..aa8a517679 100644 --- a/.github/actions/build-codex-packages/action.yml +++ b/.github/actions/build-codex-packages/action.yml @@ -23,7 +23,7 @@ runs: shell: bash working-directory: codex-rs env: - BASE_BINARIES: "codex codex-app-server codex-code-mode-host" + BASE_BINARIES: "codex codex-app-server codex-code-mode-host codex-exec-server" WINDOWS_ONLY_BINARIES: "codex-command-runner codex-windows-sandbox-setup" BUILD_MODE: ${{ inputs.build-mode }} BUILD_SYSTEM: ${{ inputs.build-system }} @@ -138,6 +138,7 @@ runs: package_args=( --target "$target" --code-mode-host-bin "${release_dir}/codex-code-mode-host${exe_suffix}" + --exec-server-bin "${release_dir}/codex-exec-server${exe_suffix}" --cargo-profile release --package-version "$CODEX_PACKAGE_VERSION" --force @@ -183,7 +184,6 @@ runs: mkdir -p "${GITHUB_WORKSPACE}/codex-rs/target" pytest_args=( -v - --basetemp "${GITHUB_WORKSPACE}/codex-rs/target/package-smoke-tmp" --package-target "$target" --symbols-archive "${GITHUB_WORKSPACE}/symbols-dist/${target}/codex-symbols-${target}.tar.gz" ) @@ -194,13 +194,17 @@ runs: exit 1 fi + # Windows setup helpers can finish background ACL work after pytest exits. + # Separate roots keep the next run from deleting their executables. uv run --frozen pytest "${pytest_args[@]}" \ + --basetemp "${GITHUB_WORKSPACE}/codex-rs/target/package-smoke-gzip-tmp" \ --compression gzip \ --cli-archive "${archive_dir}/codex-package-${target}.tar.gz" \ --app-server-archive "${archive_dir}/codex-app-server-package-${target}.tar.gz" \ test_codex_package.py uv run --frozen pytest "${pytest_args[@]}" \ + --basetemp "${GITHUB_WORKSPACE}/codex-rs/target/package-smoke-zstd-tmp" \ --compression zstd \ --cli-archive "${archive_dir}/codex-package-${target}.tar.zst" \ --app-server-archive "${archive_dir}/codex-app-server-package-${target}.tar.zst" \ diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh index 16db0342ae..20c12a3389 100644 --- a/.github/scripts/build-codex-package-archive.sh +++ b/.github/scripts/build-codex-package-archive.sh @@ -136,6 +136,8 @@ case "$target" in ;; esac +resource_args+=(--exec-server-bin "${entrypoint_dir%/}/codex-exec-server${exe_suffix}") + code_mode_host_bin="${entrypoint_dir%/}/codex-code-mode-host${exe_suffix}" if [[ "$code_mode_host_bin_provided" == "false" && -f "$code_mode_host_bin" ]]; then resource_args+=(--code-mode-host-bin "$code_mode_host_bin") diff --git a/.github/scripts/macos-signing/codex-exec-server.entitlements.plist b/.github/scripts/macos-signing/codex-exec-server.entitlements.plist new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/.github/scripts/macos-signing/codex-exec-server.entitlements.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/.github/scripts/macos-signing/provisioned_macos_cli_package.py b/.github/scripts/macos-signing/provisioned_macos_cli_package.py index e9bc091074..5266feabff 100644 --- a/.github/scripts/macos-signing/provisioned_macos_cli_package.py +++ b/.github/scripts/macos-signing/provisioned_macos_cli_package.py @@ -19,7 +19,7 @@ CODE_SIGNING_ID = "codex" APP = Path("CodexCLI.app") EXECUTABLE = APP / "Contents/MacOS/codex" SIGNING = Path(__file__).resolve().parent -HELPERS = ("bin/codex-code-mode-host", "codex-path/rg") +HELPERS = ("bin/codex-code-mode-host", "bin/codex-exec-server", "codex-path/rg") LAUNCHER = """#!/bin/sh set -eu entry="$0" diff --git a/.github/scripts/macos-signing/sign_macos_cli_package.py b/.github/scripts/macos-signing/sign_macos_cli_package.py index ac214160a1..8e3ef6726f 100644 --- a/.github/scripts/macos-signing/sign_macos_cli_package.py +++ b/.github/scripts/macos-signing/sign_macos_cli_package.py @@ -2,6 +2,7 @@ import argparse import os +import plistlib import subprocess import sys from pathlib import Path @@ -134,6 +135,12 @@ def main(configuration: bundle.ProfileConfiguration | None = None): check=True, ) if relative.startswith("bin/"): + # Signing tools may omit the blob when no entitlements are requested. + if ( + actual.stat().st_size == 0 + and plistlib.loads(entitlements.read_bytes()) == {} + ): + actual.write_bytes(entitlements.read_bytes()) expected = reports / f"{name}-expected.plist" subprocess.run( [ diff --git a/.github/scripts/macos-signing/test_sign_macos_cli_package.py b/.github/scripts/macos-signing/test_sign_macos_cli_package.py index 1b7ac328ee..5ed7349397 100644 --- a/.github/scripts/macos-signing/test_sign_macos_cli_package.py +++ b/.github/scripts/macos-signing/test_sign_macos_cli_package.py @@ -34,10 +34,15 @@ if name == "codesign": target = Path(args[-1]).name if target == "CodexCLI.app": source = Path(os.environ["CODEX_REPO_ROOT"]) / "signing-verification/codex-provisioned-entitlements.plist" - elif target in ("codex", "codex-code-mode-host"): + elif target in ("codex", "codex-code-mode-host", "codex-exec-server"): source = Path(os.environ["MOCK_SIGNING_SCRIPTS"]) / (target + ".entitlements.plist") else: sys.exit(0) + if target == "codex-exec-server" and os.environ.get("MOCK_EXECUTOR_ENTITLEMENTS"): + sys.stdout.buffer.write(plistlib.dumps({"unexpected": True})) + sys.exit(0) + if not plistlib.loads(source.read_bytes()) and os.environ.get("MOCK_OMIT_EMPTY_ENTITLEMENTS"): + sys.exit(0) if os.environ.get("MOCK_WRONG_ENTITLEMENTS"): sys.stdout.buffer.write(plistlib.dumps({"unexpected": True})) else: @@ -145,15 +150,21 @@ class MacosSigningTests(ProvisioningTestCase): ) def test_standard_and_provisioned_signing_and_verification(self): - for provisioned, target in itertools.product( - (False, True), ("aarch64-apple-darwin", "x86_64-apple-darwin") + for provisioned, target, omit_empty in itertools.product( + (False, True), + ("aarch64-apple-darwin", "x86_64-apple-darwin"), + (False, True), ): with ( - self.subTest(provisioned=provisioned, target=target), + self.subTest( + provisioned=provisioned, target=target, omit_empty=omit_empty + ), tempfile.TemporaryDirectory() as directory, ): root = Path(directory).resolve() package, driver, env = self.fixture(root, provisioned, target) + if omit_empty: + env["MOCK_OMIT_EMPTY_ENTITLEMENTS"] = "1" result = self.run_driver(driver, "sign", package, env) self.assertEqual(result.returncode, 0, result.stderr) result = self.run_driver(driver, "verify", package, env) @@ -189,13 +200,13 @@ class MacosSigningTests(ProvisioningTestCase): ) self.assertEqual( ["--entitlements" in call for call in signing], - [True, True, False], + [True, True, True, False], ) self.assertEqual( [call[0] for call in calls if call[0].startswith("notarize")], ["notarize_with_akv.py"] if provisioned - else ["notarize_macos_binary_with_akv.sh"] * 3, + else ["notarize_macos_binary_with_akv.sh"] * 4, ) self.assertEqual( [call[1:] for call in calls if call[0] == "lipo"], @@ -318,7 +329,12 @@ class MacosSigningTests(ProvisioningTestCase): def test_verification_rejects_wrong_signing_identity_and_entitlements(self): for provisioned, failure in itertools.product( - (False, True), ("MOCK_WRONG_SIGNING_IDENTITY", "MOCK_WRONG_ENTITLEMENTS") + (False, True), + ( + "MOCK_WRONG_SIGNING_IDENTITY", + "MOCK_WRONG_ENTITLEMENTS", + "MOCK_EXECUTOR_ENTITLEMENTS", + ), ): with ( self.subTest(provisioned=provisioned, failure=failure), diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index f80bcb34e7..ae435ae4af 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -4,7 +4,7 @@ on: workflow_call: env: - WINDOWS_BINARIES: "codex codex-code-mode-host codex-responses-api-proxy codex-windows-sandbox-setup codex-windows-sandbox-service codex-command-runner codex-app-server" + WINDOWS_BINARIES: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy codex-windows-sandbox-setup codex-windows-sandbox-service codex-command-runner codex-app-server" jobs: build-windows-binaries: @@ -39,14 +39,14 @@ jobs: - runner: windows-x64 target: x86_64-pc-windows-msvc bundle: primary - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-x64 - runner: windows-arm64 target: aarch64-pc-windows-msvc bundle: primary - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-arm64 @@ -67,14 +67,14 @@ jobs: - runner: windows-x64 target: x86_64-pc-windows-msvc bundle: app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-x64 - runner: windows-arm64 target: aarch64-pc-windows-msvc bundle: app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-arm64 @@ -549,6 +549,26 @@ jobs: --entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \ --archive-dir "dist/$target" + - name: Verify packaged executor signatures and bytes + shell: pwsh + run: | + $target = "${{ matrix.target }}" + $source = Join-Path $env:CARGO_TARGET_DIR "$target/release/codex-exec-server.exe" + foreach ($stem in @("codex-package", "codex-app-server-package")) { + $package = Join-Path $env:RUNNER_TEMP "verify-executor-$stem-$target" + New-Item -ItemType Directory -Path $package | Out-Null + tar -xzf "dist/$target/$stem-$target.tar.gz" -C $package + if ($LASTEXITCODE -ne 0) { throw "Cannot extract executor package" } + $binary = Join-Path $package "bin/codex-exec-server.exe" + if ((Get-FileHash $binary).Hash -ne (Get-FileHash $source).Hash) { + throw "Packaged executor differs from the signed input" + } + $signature = Get-AuthenticodeSignature $binary + if ($signature.Status -ne "Valid" -or $null -eq $signature.TimeStamperCertificate) { + throw "Invalid or untimestamped executor signature" + } + } + - name: Verify packaged Windows voice closure shell: pwsh run: | diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 7702e44a6c..6a4a2dcaca 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -96,50 +96,50 @@ jobs: target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" build_dmg: "true" - runner: macos-15-xlarge target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" - runner: macos-15-xlarge target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" build_dmg: "true" - runner: macos-15-xlarge target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" # Release artifacts intentionally ship MUSL-linked Linux binaries. - runner: ${{ github.event.repository.name }}-linux-x64-xl target: x86_64-unknown-linux-musl bundle: primary artifact_name: x86_64-unknown-linux-musl - binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy bwrap" build_dmg: "false" - runner: ${{ github.event.repository.name }}-linux-x64-xl target: x86_64-unknown-linux-musl bundle: app-server artifact_name: x86_64-unknown-linux-musl-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" - runner: ${{ github.event.repository.name }}-linux-arm64 target: aarch64-unknown-linux-musl bundle: primary artifact_name: aarch64-unknown-linux-musl - binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy bwrap" build_dmg: "false" - runner: ${{ github.event.repository.name }}-linux-arm64 target: aarch64-unknown-linux-musl bundle: app-server artifact_name: aarch64-unknown-linux-musl-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" steps: @@ -398,9 +398,9 @@ jobs: mkdir -p "$dest" for binary in ${{ matrix.binaries }}; do - # Both variants package the host, but only the primary bundle publishes + # Both variants package the host and executor; only the primary publishes # standalone binary archives to avoid duplicate release asset names. - if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then + if [[ "${{ matrix.bundle }}" == "app-server" && ( "$binary" == "codex-code-mode-host" || "$binary" == "codex-exec-server" ) ]]; then continue fi cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" @@ -662,19 +662,19 @@ jobs: - target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" - target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" - target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" - target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -907,22 +907,22 @@ jobs: - target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" build_dmg: "true" - target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" - target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" build_dmg: "true" - target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" build_dmg: "false" steps: @@ -1001,9 +1001,9 @@ jobs: mkdir -p "$dest" for binary in ${{ matrix.binaries }}; do - # Both variants package the host, but only the primary bundle publishes + # Both variants package the host and executor; only the primary publishes # standalone binary archives to avoid duplicate release asset names. - if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then + if [[ "${{ matrix.bundle }}" == "app-server" && ( "$binary" == "codex-code-mode-host" || "$binary" == "codex-exec-server" ) ]]; then continue fi cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" @@ -1300,22 +1300,22 @@ jobs: - target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" verify_dmg: "true" - target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" verify_dmg: "false" - target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-code-mode-host codex-exec-server codex-responses-api-proxy" verify_dmg: "true" - target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server - binaries: "codex-app-server codex-code-mode-host" + binaries: "codex-app-server codex-code-mode-host codex-exec-server" verify_dmg: "false" steps: @@ -1376,6 +1376,15 @@ jobs: normalized_actual="$(mktemp)" normalized_expected="$(mktemp)" codesign -d --entitlements :- "$path" >"$actual_entitlements" + # An omitted entitlement blob is valid only when the expected plist is empty. + python3 - "$expected_entitlements" "$actual_entitlements" <<'PY' + import plistlib + import sys + from pathlib import Path + expected, actual = map(Path, sys.argv[1:]) + if not actual.stat().st_size and plistlib.loads(expected.read_bytes()) == {}: + actual.write_bytes(expected.read_bytes()) + PY plutil -convert xml1 -o "$normalized_actual" "$actual_entitlements" plutil -convert xml1 -o "$normalized_expected" "$expected_entitlements" diff -u "$normalized_expected" "$normalized_actual" @@ -1386,9 +1395,9 @@ jobs: binary_path="${RUNNER_TEMP}/signed-binaries/${binary}" verify_signed_binary "$binary_path" "$binary" - # The app-server package contains the host, but its standalone archives + # The app-server package contains both helpers, but their standalone archives # are omitted above to avoid duplicate release asset names. - if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then + if [[ "${{ matrix.bundle }}" == "app-server" && ( "$binary" == "codex-code-mode-host" || "$binary" == "codex-exec-server" ) ]]; then continue fi @@ -1424,6 +1433,7 @@ jobs: tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir" verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint" verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host" + verify_signed_binary "${package_dir}/bin/codex-exec-server" "codex-exec-server" for resource in \ "${package_dir}/codex-path/rg" diff --git a/codex-rs/BUILD.bazel b/codex-rs/BUILD.bazel index e169894153..fc151fdc2e 100644 --- a/codex-rs/BUILD.bazel +++ b/codex-rs/BUILD.bazel @@ -53,6 +53,7 @@ runtime_archive( "//codex-rs/app-server:release_files", "//codex-rs/cli:release_files", "//codex-rs/code-mode-host:release_files", + "//codex-rs/exec-server-cli:release_files", ] + select({ "@platforms//os:linux": ["//codex-rs/bwrap:release_files"], "@platforms//os:windows": [ @@ -70,6 +71,7 @@ runtime_archive( srcs = [ "//codex-rs/cli:release_files", "//codex-rs/code-mode-host:release_files", + "//codex-rs/exec-server-cli:release_files", "//codex-rs/responses-api-proxy:release_files", ] + select({ "@platforms//os:linux": ["//codex-rs/bwrap:release_files"], @@ -84,6 +86,7 @@ runtime_archive( srcs = [ "//codex-rs/app-server:release_files", "//codex-rs/code-mode-host:release_files", + "//codex-rs/exec-server-cli:release_files", ] + select({ "@platforms//os:linux": ["//codex-rs/bwrap:release_files"], "//conditions:default": [], diff --git a/codex-rs/exec-server-cli/BUILD.bazel b/codex-rs/exec-server-cli/BUILD.bazel index 5d31d3ad0d..553d6aeda1 100644 --- a/codex-rs/exec-server-cli/BUILD.bazel +++ b/codex-rs/exec-server-cli/BUILD.bazel @@ -1,4 +1,6 @@ load("//:defs.bzl", "codex_rust_crate") +load("//bazel/platforms:release_binaries.bzl", "multiplatform_binaries") +load("//bazel/rules:runtime_package.bzl", "release_binary") codex_rust_crate( name = "exec-server-cli", @@ -9,3 +11,7 @@ codex_rust_crate( # The standalone integration test launches native OS sandboxes itself. test_tags = ["no-sandbox"], ) + +release_binary(binary = ":codex-exec-server") + +multiplatform_binaries(name = "codex-exec-server") diff --git a/codex-rs/exec-server/README.md b/codex-rs/exec-server/README.md index 13d6c80986..46a465a79d 100644 --- a/codex-rs/exec-server/README.md +++ b/codex-rs/exec-server/README.md @@ -17,6 +17,8 @@ use `codex-arg0` for hidden filesystem, process, and platform sandbox dispatch. The standalone executable accepts the same executor arguments and `-c` config overrides, including `--listen stdio`. Build it with `cargo build -p codex-exec-server-cli --bin codex-exec-server`. +Canonical packages place it in `bin/` beside the entrypoint, with platform +sandbox resources in `codex-resources/`. ## Command metadata diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index 6c6ba6a108..d5f829debb 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -19,7 +19,8 @@ The builder creates a canonical Codex package directory: ├── codex-package.json ├── bin │ ├── [.exe] -│ └── codex-code-mode-host[.exe] +│ ├── codex-code-mode-host[.exe] +│ └── codex-exec-server[.exe] ├── codex-resources │ ├── bwrap # Linux only │ ├── codex-command-runner.exe # Windows only @@ -30,6 +31,9 @@ The builder creates a canonical Codex package directory: The package directory is the primary artifact. Archive formats such as `.tar.gz`, `.tar.zst`, and `.zip` are serializations of that directory. +The `execServer` field in `codex-package.json` advertises the packaged executor. +Installers use its presence to repair a missing executor while accepting older +packages that predate the standalone binary. If `--target` is omitted, the builder uses the release target for the current host platform. On Linux, that default is a musl target to match Codex release @@ -50,6 +54,7 @@ override was provided: - all targets: the selected entrypoint, unless `--entrypoint-bin` is provided - all targets: `codex-code-mode-host`, unless `--code-mode-host-bin` is provided +- all targets: `codex-exec-server`, unless `--exec-server-bin` is provided - Linux targets: `bwrap`, unless `--bwrap-bin` is provided - Windows targets: `codex-command-runner` and `codex-windows-sandbox-setup`, unless the corresponding prebuilt helper flags are provided @@ -60,15 +65,15 @@ explicit target. Release jobs that already built and signed/notarized the entrypoint should pass `--entrypoint-bin` so the package contains that exact binary instead of rebuilding it. -Release jobs should likewise pass `--code-mode-host-bin` so the package contains -the signed host executable beside the signed entrypoint. +Release jobs should likewise pass `--code-mode-host-bin` and `--exec-server-bin` +so the package preserves the signed host and executor beside the entrypoint. On Linux and macOS, the builder strips symbols from the **package copies** of -source-built release-profile entrypoint and code-mode host binaries. Cargo outputs +source-built release-profile entrypoint, code-mode host, and executor binaries. Cargo outputs retain their symbols for debugging. The default `--strip auto` also preserves development/profiling builds and prebuilt inputs byte-for-byte, including signatures. Use `--strip all` to also strip prebuilt -entrypoint/host copies, or `--strip none` to keep symbols in every package copy. +first-party executable copies, or `--strip none` to keep symbols in every package copy. Strip before production signing; `--strip all` is not appropriate for inputs whose release signatures must be preserved. Windows MSVC symbols are separate PDB files and are not included in the package. diff --git a/scripts/codex_package/cargo.py b/scripts/codex_package/cargo.py index f759e44762..7f598256dd 100644 --- a/scripts/codex_package/cargo.py +++ b/scripts/codex_package/cargo.py @@ -18,6 +18,7 @@ CODEX_RS_ROOT = REPO_ROOT / "codex-rs" class SourceBuildOutputs: entrypoint_bin: Path code_mode_host_bin: Path + exec_server_bin: Path bwrap_bin: Path | None codex_command_runner_bin: Path | None codex_windows_sandbox_setup_bin: Path | None @@ -31,6 +32,7 @@ def build_source_binaries( profile: str, entrypoint_bin: Path | None, code_mode_host_bin: Path | None, + exec_server_bin: Path | None, bwrap_bin: Path | None, codex_command_runner_bin: Path | None, codex_windows_sandbox_setup_bin: Path | None, @@ -46,6 +48,7 @@ def build_source_binaries( variant, build_entrypoint=entrypoint_bin is None, build_code_mode_host=code_mode_host_bin is None, + build_exec_server=exec_server_bin is None, build_bwrap=spec.is_linux and bwrap_bin is None, build_codex_command_runner=spec.is_windows and codex_command_runner_bin is None, build_codex_windows_sandbox_setup=spec.is_windows @@ -64,7 +67,11 @@ def build_source_binaries( cmd.extend(["--bin", binary]) cargo_env = None - if entrypoint_bin is None or code_mode_host_bin is None: + if ( + entrypoint_bin is None + or code_mode_host_bin is None + or exec_server_bin is None + ): codex_v8_env = resolve_codex_v8_cargo_env(spec) if codex_v8_env: cargo_env = {**os.environ, **codex_v8_env} @@ -88,6 +95,11 @@ def build_source_binaries( if code_mode_host_bin is not None else output_dir / f"codex-code-mode-host{spec.exe_suffix}" ), + exec_server_bin=( + exec_server_bin.resolve() + if exec_server_bin is not None + else output_dir / f"codex-exec-server{spec.exe_suffix}" + ), bwrap_bin=resolve_output_path( bwrap_bin, output_dir / "bwrap" if spec.is_linux else None, @@ -111,6 +123,7 @@ def source_binaries_for_target( *, build_entrypoint: bool, build_code_mode_host: bool, + build_exec_server: bool, build_bwrap: bool, build_codex_command_runner: bool, build_codex_windows_sandbox_setup: bool, @@ -120,6 +133,8 @@ def source_binaries_for_target( binaries.append(variant.cargo_bin) if build_code_mode_host: binaries.append("codex-code-mode-host") + if build_exec_server: + binaries.append("codex-exec-server") if build_bwrap: binaries.append("bwrap") if build_codex_command_runner: @@ -186,6 +201,7 @@ def validate_source_outputs(outputs: SourceBuildOutputs) -> None: for path in [ outputs.entrypoint_bin, outputs.code_mode_host_bin, + outputs.exec_server_bin, outputs.bwrap_bin, outputs.codex_command_runner_bin, outputs.codex_windows_sandbox_setup_bin, diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index 6a99ccb91e..41e3caddc8 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -140,6 +140,11 @@ def create_parser() -> argparse.ArgumentParser: "the host is built with Cargo." ), ) + parser.add_argument( + "--exec-server-bin", + type=Path, + help="Optional prebuilt codex-exec-server executable. If omitted, build it with Cargo.", + ) parser.add_argument( "--bwrap-bin", type=Path, @@ -210,6 +215,11 @@ def assemble_package(args: argparse.Namespace) -> Path: "prebuilt code-mode host executable", "--code-mode-host-bin", ), + exec_server_bin=resolve_optional_input_path( + args.exec_server_bin, + "prebuilt executor executable", + "--exec-server-bin", + ), bwrap_bin=resolve_optional_input_path( args.bwrap_bin, "prebuilt Linux bwrap executable", @@ -229,6 +239,7 @@ def assemble_package(args: argparse.Namespace) -> Path: inputs = PackageInputs( entrypoint_bin=source_outputs.entrypoint_bin, code_mode_host_bin=source_outputs.code_mode_host_bin, + exec_server_bin=source_outputs.exec_server_bin, rg_bin=resolve_rg_bin(spec, args.rg_bin), bwrap_bin=source_outputs.bwrap_bin, codex_command_runner_bin=source_outputs.codex_command_runner_bin, @@ -239,6 +250,7 @@ def assemble_package(args: argparse.Namespace) -> Path: for filename, prebuilt in ( (variant.entrypoint_name(spec), args.entrypoint_bin), (f"codex-code-mode-host{spec.exe_suffix}", args.code_mode_host_bin), + (f"codex-exec-server{spec.exe_suffix}", args.exec_server_bin), ): if args.strip == "all" or ( args.strip == "auto" diff --git a/scripts/codex_package/layout.py b/scripts/codex_package/layout.py index f93d8da64c..ccb54313f4 100644 --- a/scripts/codex_package/layout.py +++ b/scripts/codex_package/layout.py @@ -55,6 +55,11 @@ def build_package_dir( bin_dir / f"codex-code-mode-host{spec.exe_suffix}", is_windows=spec.is_windows, ) + copy_executable( + inputs.exec_server_bin, + bin_dir / f"codex-exec-server{spec.exe_suffix}", + is_windows=spec.is_windows, + ) copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows) if inputs.bwrap_bin is not None: @@ -80,6 +85,7 @@ def build_package_dir( "target": spec.target, "variant": variant.name, "entrypoint": f"bin/{entrypoint_name}", + "execServer": f"bin/codex-exec-server{spec.exe_suffix}", "resourcesDir": "codex-resources", "pathDir": "codex-path", } @@ -113,6 +119,7 @@ def validate_package_dir( "target": spec.target, "variant": variant.name, "entrypoint": f"bin/{variant.entrypoint_name(spec)}", + "execServer": f"bin/codex-exec-server{spec.exe_suffix}", "resourcesDir": "codex-resources", "pathDir": "codex-path", } @@ -126,6 +133,7 @@ def validate_package_dir( required_files = [ Path("bin") / variant.entrypoint_name(spec), Path("bin") / f"codex-code-mode-host{spec.exe_suffix}", + Path("bin") / f"codex-exec-server{spec.exe_suffix}", Path("codex-path") / spec.rg_name, ] executable_files = list(required_files) diff --git a/scripts/codex_package/smoke_tests/fixtures.py b/scripts/codex_package/smoke_tests/fixtures.py index 52e8838c46..742b6c479f 100644 --- a/scripts/codex_package/smoke_tests/fixtures.py +++ b/scripts/codex_package/smoke_tests/fixtures.py @@ -208,7 +208,12 @@ def code_mode_host_debug_symbols( tmp_path_factory: pytest.TempPathFactory, ) -> Path: target = pytestconfig.getoption("package_target") - binaries = {"codex", "codex-app-server", "codex-code-mode-host"} + binaries = { + "codex", + "codex-app-server", + "codex-code-mode-host", + "codex-exec-server", + } if "windows" in target: binaries.update({"codex-command-runner", "codex-windows-sandbox-setup"}) diff --git a/scripts/codex_package/smoke_tests/test_codex_package.py b/scripts/codex_package/smoke_tests/test_codex_package.py index ee613a1290..0abd78cf5f 100644 --- a/scripts/codex_package/smoke_tests/test_codex_package.py +++ b/scripts/codex_package/smoke_tests/test_codex_package.py @@ -4,7 +4,8 @@ ROOT_OF_EXTRACTED_PACKAGE ├── bin │ ├── codex[.exe] # CLI package only │ ├── codex-app-server[.exe] # app-server package only -│ └── codex-code-mode-host[.exe] +│ ├── codex-code-mode-host[.exe] +│ └── codex-exec-server[.exe] ├── codex-package.json ├── codex-path │ └── rg[.exe] @@ -18,11 +19,13 @@ Each package contains one entrypoint, not both codex and codex-app-server. """ import json +import os import re import shutil import subprocess import sys import tomllib +from concurrent.futures import ThreadPoolExecutor from pathlib import Path import pytest @@ -67,6 +70,261 @@ def test_cli_version_matches_release_sources(package: SmokePackage) -> None: assert package.run("--version").stdout == f"codex-cli {version}\n" +@pytest.mark.parametrize("variant", ["cli_root", "app_server_root"]) +def test_packaged_executor_initializes_and_enforces_sandbox( + package: SmokePackage, variant: str +) -> None: + """Each package re-enters its executor helpers and enforces filesystem/process policy.""" + root = getattr(package, variant) + windows = "windows" in package.target + suffix = ".exe" if windows else "" + executor = root / "bin" / f"codex-exec-server{suffix}" + directory = package.directory / f"{variant}-executor-sandbox" + directory.mkdir() + workspace = directory / "workspace" + workspace.mkdir() + home = directory / "home" + home.mkdir() + blocked = directory / "blocked.txt" + blocked.write_text("unchanged") + environment = {**package.environment, "CODEX_HOME": str(home)} + if windows: + # Elevated mode must discover the setup/runner in this package's resources. + # Exclude build outputs and host Codex installations from helper fallback. + environment["PATH"] = str(Path(os.environ["SystemRoot"]) / "System32") + sandbox = { + "permissions": { + "type": "managed", + "file_system": { + "type": "restricted", + "entries": [ + { + "path": {"type": "special", "value": {"kind": "root"}}, + "access": "read", + }, + { + "path": {"type": "path", "path": workspace.as_uri()}, + "access": "write", + }, + ], + }, + "network": "restricted", + }, + "cwd": workspace.as_uri(), + "windowsSandboxLevel": "elevated" if windows else "disabled", + } + version = ( + subprocess.check_output( + [str(executor), "--version"], env=package.environment, text=True, timeout=45 + ) + .strip() + .split()[-1] + ) + assert version == package.run("--version").stdout.strip().split()[-1] + with ( + ThreadPoolExecutor(max_workers=1) as reader, + subprocess.Popen( + [str(executor), "--listen", "stdio"], + cwd=package.directory, + env=environment, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + ) as process, + ): + assert process.stdin is not None and process.stdout is not None + try: + process.stdin.write( + json.dumps( + { + "id": 1, + "method": "initialize", + "params": {"clientName": "package-smoke"}, + } + ) + + "\n" + ) + process.stdin.flush() + initialized = json.loads( + reader.submit(process.stdout.readline).result(timeout=45) + ) + assert initialized["id"] == 1 + assert initialized["result"]["sessionId"] + reported_environment = initialized["result"]["environmentInfo"] + metadata = json.loads((root / "codex-package.json").read_text()) + assert reported_environment["executorVersion"] == metadata["version"] + process.stdin.write('{"method":"initialized","params":{}}\n') + + # Restricted writes always launch the filesystem helper, even when allowed. + for request_id, path in [(2, workspace / "allowed.txt"), (3, blocked)]: + process.stdin.write( + json.dumps( + { + "id": request_id, + "method": "fs/writeFile", + "params": { + "path": path.as_uri(), + "dataBase64": "b2s=", + "sandbox": sandbox, + }, + } + ) + + "\n" + ) + process.stdin.flush() + # The first Windows helper can provision sandbox accounts before replying. + response = json.loads( + reader.submit(process.stdout.readline).result( + timeout=180 if windows and request_id == 2 else 45 + ) + ) + if request_id == 2: + assert response == {"id": 2, "result": {}}, response + assert path.read_text() == "ok" + else: + assert response["id"] == 3 and "error" in response, response + assert blocked.read_text() == "unchanged" + + command = ( + [ + str(Path(os.environ["SystemRoot"]) / "System32" / "cmd.exe"), + "/D", + "/S", + "/C", + r"echo process>process.txt & echo forbidden>..\blocked.txt & exit /b 0", + ] + if windows + else [ + "/bin/sh", + "-c", + "printf process >process.txt; printf forbidden >../blocked.txt; exit 0", + ] + ) + process.stdin.write( + json.dumps( + { + "id": 4, + "method": "process/start", + "params": { + "processId": "package-sandbox", + "argv": command, + "cwd": workspace.as_uri(), + "env": { + key: value + for key, value in environment.items() + if key.casefold() in {"path", "systemroot"} + }, + "sandbox": sandbox, + "tty": False, + "pipeStdin": False, + "arg0": None, + }, + } + ) + + "\n" + ) + process.stdin.flush() + started = False + exit_code = None + notifications = [] + while not started or exit_code is None: + response = json.loads( + reader.submit(process.stdout.readline).result(timeout=45) + ) + notifications.append(response) + if response.get("id") == 4: + expected_type = ( + "windowsRestrictedToken" + if windows + else "macosSeatbelt" + if "apple-darwin" in package.target + else "linuxSeccomp" + ) + assert response["result"]["sandboxType"] == expected_type, response + started = True + if response.get("method") == "process/exited": + exit_code = response["params"]["exitCode"] + assert exit_code == 0, notifications + assert (workspace / "process.txt").is_file(), notifications + assert (workspace / "process.txt").read_text().strip() == "process" + assert blocked.read_text() == "unchanged" + process.stdin.close() + assert process.wait(timeout=45) == 0 + finally: + if process.poll() is None: + process.kill() + + +def test_packaged_executor_matches_subcommand_startup( + package: SmokePackage, + tmp_path: Path, +) -> None: + """Both binaries reject the same invalid executor arguments and config.""" + home = tmp_path / "home" + temporary = tmp_path / "tmp" + home.mkdir() + temporary.mkdir() + (home / "config.toml").write_text("unknown_executor_option = true\n") + # Release builds require the helper directory to live outside their temp root. + environment = { + **package.environment, + "CODEX_HOME": str(home), + "TMPDIR": str(temporary), + "TEMP": str(temporary), + "TMP": str(temporary), + } + suffix = ".exe" if "windows" in package.target else "" + executor = package.cli_root / "bin" / f"codex-exec-server{suffix}" + for arguments in [ + ["--strict-config", "--listen", "stdio"], + ["-c", "unknown_executor_option=true", "--strict-config", "--listen", "stdio"], + ["--listen", "http://127.0.0.1:0"], + ["--concurrent-requests", "0"], + [ + "--aws-sigv4", + "--remote", + "https://example.openai.com", + "--environment-id", + "env-1", + ], + ["--remote-transport", "direct"], + ["forward", "--connect", "ws://127.0.0.1:0"], + [ + "-c", + "unknown_executor_option=true", + "forward", + "-c", + "analytics.enabled=false", + "--strict-config", + "--connect", + "ws://127.0.0.1:0", + "--remote", + "https://example.openai.com", + "--environment-id", + "env-1", + ], + ]: + outputs = [] + for command in ([str(package.cli), "exec-server"], [str(executor)]): + result = subprocess.run( + [*command, *arguments], + cwd=tmp_path, + env=environment, + capture_output=True, + text=True, + timeout=45, + ) + assert result.returncode != 0, arguments + outputs.append( + ( + result.returncode, + result.stdout, + result.stderr.replace("codex exec-server", "codex-exec-server"), + ) + ) + assert outputs[0] == outputs[1], arguments + + @pytest.mark.parametrize("entrypoint", ["codex", "codex-app-server"]) def test_app_server_runs_code_mode_through_python_sdk( package: SmokePackage, diff --git a/scripts/codex_package/targets.py b/scripts/codex_package/targets.py index eedf7b03b1..028f0724fc 100644 --- a/scripts/codex_package/targets.py +++ b/scripts/codex_package/targets.py @@ -45,6 +45,7 @@ class PackageVariant: class PackageInputs: entrypoint_bin: Path code_mode_host_bin: Path + exec_server_bin: Path rg_bin: Path bwrap_bin: Path | None codex_command_runner_bin: Path | None diff --git a/scripts/codex_package/test_cargo.py b/scripts/codex_package/test_cargo.py index 1a4a572b9b..2e52a9256d 100644 --- a/scripts/codex_package/test_cargo.py +++ b/scripts/codex_package/test_cargo.py @@ -21,6 +21,7 @@ class SourceBinariesForTargetTest(unittest.TestCase): PACKAGE_VARIANTS["codex"], build_entrypoint=False, build_code_mode_host=False, + build_exec_server=False, build_bwrap=False, build_codex_command_runner=False, build_codex_windows_sandbox_setup=False, @@ -37,6 +38,7 @@ class SourceBinariesForTargetTest(unittest.TestCase): PACKAGE_VARIANTS["codex"], build_entrypoint=False, build_code_mode_host=False, + build_exec_server=False, build_bwrap=False, build_codex_command_runner=False, build_codex_windows_sandbox_setup=False, @@ -53,6 +55,7 @@ class SourceBinariesForTargetTest(unittest.TestCase): PACKAGE_VARIANTS["codex"], build_entrypoint=False, build_code_mode_host=False, + build_exec_server=False, build_bwrap=False, build_codex_command_runner=False, build_codex_windows_sandbox_setup=False, @@ -67,6 +70,7 @@ class SourceBinariesForTargetTest(unittest.TestCase): PACKAGE_VARIANTS["codex"], build_entrypoint=False, build_code_mode_host=False, + build_exec_server=False, build_bwrap=False, build_codex_command_runner=True, build_codex_windows_sandbox_setup=True, @@ -81,11 +85,12 @@ class SourceBinariesForTargetTest(unittest.TestCase): PACKAGE_VARIANTS["codex-app-server"], build_entrypoint=False, build_code_mode_host=True, + build_exec_server=True, build_bwrap=False, build_codex_command_runner=False, build_codex_windows_sandbox_setup=False, ), - ["codex-code-mode-host"], + ["codex-code-mode-host", "codex-exec-server"], ) def test_build_uses_prebuilt_windows_helpers_without_running_cargo(self) -> None: @@ -103,6 +108,7 @@ class SourceBinariesForTargetTest(unittest.TestCase): profile="release", entrypoint_bin=entrypoint, code_mode_host_bin=code_mode_host, + exec_server_bin=code_mode_host, bwrap_bin=None, codex_command_runner_bin=command_runner, codex_windows_sandbox_setup_bin=sandbox_setup, diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py index 2682b8325d..dfa197227a 100644 --- a/scripts/codex_package/test_layout.py +++ b/scripts/codex_package/test_layout.py @@ -2,6 +2,8 @@ from pathlib import Path import hashlib +import io +import tarfile import json import shutil import subprocess @@ -12,6 +14,7 @@ import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from build_winget_package import prepare_winget_package +from codex_package.archive import write_archive, resolve_zstd_command from codex_package.layout import build_package_dir from codex_package.layout import validate_package_dir from codex_package.targets import PACKAGE_VARIANTS @@ -63,6 +66,7 @@ int main(void) { flags = [ "--entrypoint-bin", "--code-mode-host-bin", + "--exec-server-bin", "--rg-bin", ] if "linux" in target: @@ -73,7 +77,7 @@ int main(void) { # Preserved prebuilt inputs must not even need a strip tool. command.extend(("--strip-tool", str(root / "absent-strip"))) subprocess.run(command, check=True, capture_output=True) - for name in ("codex", "codex-code-mode-host"): + for name in ("codex", "codex-code-mode-host", "codex-exec-server"): executable = package / "bin" / name self.assertEqual( subprocess.check_output([str(executable)]), @@ -97,6 +101,7 @@ int main(void) { files = { "bin/codex.exe": b"signed CLI", "bin/codex-code-mode-host.exe": b"signed code mode host", + "bin/codex-exec-server.exe": b"signed executor", "codex-resources/codex-command-runner.exe": b"signed runner", "codex-resources/codex-windows-sandbox-setup.exe": b"signed setup", "codex-resources/voice/bin/codex-voice-host.exe": b"signed voice host", @@ -172,6 +177,7 @@ int main(void) { code_mode_host_bin=touch_executable( root / "codex-code-mode-host" ), + exec_server_bin=rg_bin, rg_bin=rg_bin, bwrap_bin=None, codex_command_runner_bin=None, @@ -180,13 +186,41 @@ int main(void) { build_package_dir(package_dir, "1.2.3", variant, spec, inputs) validate_package_dir(package_dir, variant, spec) + for suffix in (".tar.gz", ".tar.zst"): + archive_path = root / f"package{suffix}" + write_archive(package_dir, archive_path, force=False) + data = ( + subprocess.check_output( + [ + *resolve_zstd_command(), + "-d", + "--stdout", + str(archive_path), + ] + ) + if suffix == ".tar.zst" + else archive_path.read_bytes() + ) + with tarfile.open( + fileobj=io.BytesIO(data), mode="r:*" + ) as archive: + member = archive.getmember("bin/codex-exec-server") + self.assertTrue(member.mode & 0o111) + self.assertEqual( + archive.extractfile(member).read(), + rg_bin.read_bytes(), + ) self.assertEqual( { "rg": (package_dir / "codex-path" / "rg").read_bytes(), + "executor": ( + package_dir / "bin/codex-exec-server" + ).read_bytes(), }, { "rg": b"signed ripgrep binary", + "executor": b"signed ripgrep binary", }, ) @@ -198,6 +232,7 @@ int main(void) { inputs = PackageInputs( entrypoint_bin=touch_executable(root / "codex-app-server"), code_mode_host_bin=touch_executable(root / "codex-code-mode-host"), + exec_server_bin=touch_executable(root / "codex-exec-server"), rg_bin=touch_executable(root / "rg"), bwrap_bin=touch_executable(root / "bwrap"), codex_command_runner_bin=None, diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index e9d0bc8057..6ae0840bf5 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -772,6 +772,16 @@ function Test-PackageContentsAreComplete { "codex-resources\codex-command-runner.exe", "codex-resources\codex-windows-sandbox-setup.exe" ) + try { + $metadataPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Join-Path $PackageDir "codex-package.json")) + $metadata = [System.IO.File]::ReadAllText($metadataPath) + # Older canonical packages do not advertise or contain the executor. + if ([regex]::IsMatch($metadata, '"execServer"\s*:')) { + $expectedFiles += "bin\codex-exec-server.exe" + } + } catch { + return $false + } foreach ($name in $expectedFiles) { if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) { return $false diff --git a/scripts/install/install.sh b/scripts/install/install.sh index fff99229a5..b79970f9c1 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -1125,6 +1125,9 @@ install_package_release() { if [ -f "$stage_release/codex-resources/bwrap" ]; then chmod 0755 "$stage_release/codex-resources/bwrap" fi + if [ -f "$stage_release/bin/codex-exec-server" ]; then + chmod 0755 "$stage_release/bin/codex-exec-server" + fi ln -sf "bin/codex" "$stage_release/codex" if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then @@ -1178,6 +1181,10 @@ release_dir_is_complete() { [ -x "$release_dir/codex" ] && [ -x "$release_dir/codex-path/rg" ] || return 1 + # Older canonical packages do not advertise or contain the executor. + if grep -Eq '"execServer"[[:space:]]*:' "$release_dir/codex-package.json"; then + [ -x "$release_dir/bin/codex-exec-server" ] || return 1 + fi ;; legacy-platform-npm) [ -x "$release_dir/codex" ] && diff --git a/scripts/install/test_install_sh.py b/scripts/install/test_install_sh.py index 7cd18deb44..bac85ed277 100644 --- a/scripts/install/test_install_sh.py +++ b/scripts/install/test_install_sh.py @@ -995,6 +995,7 @@ def run_installer_in( root: Path, release: str, *, + installer_path: Path = INSTALL_SCRIPT, metadata_failure: bool = False, metadata_json: str | None = None, releases_metadata_json: str | None = None, @@ -1186,7 +1187,7 @@ def run_installer_in( "TRUE" if use_mirror else "false" ) result = subprocess.run( - ["/bin/sh", str(INSTALL_SCRIPT)], + ["/bin/sh", str(installer_path)], capture_output=True, check=False, env=env, @@ -1213,14 +1214,17 @@ def create_package_release( metadata_version: str = VERSION, binary_version: str = VERSION, target: str = "aarch64-apple-darwin", + include_exec_server: bool = False, ) -> tuple[Path, Path, str]: package_dir = root / "package" (package_dir / "bin").mkdir(parents=True) (package_dir / "codex-path").mkdir() + metadata = {"version": binary_version, "target": target, "entrypoint": "bin/codex"} + if include_exec_server: + metadata["execServer"] = "bin/codex-exec-server" + write_executable(package_dir / "bin/codex-exec-server", "#!/bin/sh\nexit 0\n") (package_dir / "codex-package.json").write_text( - json.dumps( - {"version": binary_version, "target": target, "entrypoint": "bin/codex"} - ), + json.dumps(metadata), encoding="utf-8", ) write_executable(