Ship the standalone executor in release packages (#57837)

## What changed

- Include `codex-exec-server` in canonical CLI and app-server packages and advertise its path through `execServer` in `codex-package.json`.
- Build and sign the executor in release workflows, preserve signed prebuilt inputs, and verify packaged signatures. Use empty entitlements for the macOS executor.
- Have installers check for the executor when package metadata advertises it, while continuing to accept older packages.
- Omit the standalone executor and its manifest entry from npm packages to stay within npm's package size limit. Keep `codex exec-server` and its runtime helpers available.

## Testing

Add package smoke tests for executor initialization, version reporting, filesystem and process sandbox enforcement, and invalid-argument/configuration parity with `codex exec-server`. Extend archive, npm staging, and macOS signing tests to cover executor contents, executable permissions, preserved bytes, and entitlement verification.

GitOrigin-RevId: bc741fe7faa6fe631d076de18d448c55247c23c7
This commit is contained in:
Dmitriy Kovalenko
2026-10-28 21:09:05 +00:00
committed by copyberry
parent d359945843
commit b91f2515f5
26 changed files with 625 additions and 55 deletions
@@ -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" \
@@ -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")
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
@@ -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"
@@ -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(
[
@@ -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),
+25 -5
View File
@@ -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: |
+36 -26
View File
@@ -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"