mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
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:
committed by
copyberry
parent
d359945843
commit
b91f2515f5
@@ -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),
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -18,6 +18,12 @@ When `--package codex` is provided, the staging helper builds the lightweight
|
||||
`@openai/codex` meta package plus all platform-native `@openai/codex` variants
|
||||
that are later published under platform-specific dist-tags.
|
||||
|
||||
The npm variants omit the standalone `codex-exec-server` executable and its
|
||||
`execServer` manifest entry to keep uploads within npm's package size limit.
|
||||
They retain `codex exec-server` through the CLI, the code-mode host, and the
|
||||
platform sandbox helpers. Canonical release archives retain the standalone
|
||||
executor; npm staging changes only the package copy.
|
||||
|
||||
Direct `build_npm_package.py` invocations are still useful for package-specific
|
||||
debugging, but native packages expect `--vendor-src` to point at a prehydrated
|
||||
`vendor/` tree. Release packaging should use `scripts/stage_npm_packages.py`
|
||||
|
||||
@@ -391,6 +391,16 @@ def copy_native_binaries(
|
||||
if dest_target_dir.exists():
|
||||
shutil.rmtree(dest_target_dir)
|
||||
shutil.copytree(target_dir, dest_target_dir)
|
||||
# standalone exec-server is a binary that is equal to codex exec-server, do not publish it to npm
|
||||
for name in ("codex-exec-server", "codex-exec-server.exe"):
|
||||
(dest_target_dir / "bin" / name).unlink(missing_ok=True)
|
||||
metadata_path = dest_target_dir / "codex-package.json"
|
||||
if metadata_path.is_file():
|
||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
||||
metadata.pop("execServer", None)
|
||||
metadata_path.write_text(
|
||||
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
else:
|
||||
dest_target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Check the contents of npm archives staged from canonical release packages."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT / "codex-cli" / "scripts"))
|
||||
|
||||
from build_npm_package import CODEX_PLATFORM_PACKAGES
|
||||
|
||||
|
||||
class NpmPackageTest(unittest.TestCase):
|
||||
def test_npm_archives_preserve_cli_runtime_without_standalone_executor(
|
||||
self,
|
||||
) -> None:
|
||||
# Use an installed executable as prebuilt input: packaging must preserve its
|
||||
# bytes even when staging a package for another operating system.
|
||||
executable = shutil.which("whoami") or sys.executable
|
||||
for package, platform in CODEX_PLATFORM_PACKAGES.items():
|
||||
with self.subTest(package=package), tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
target = platform["target_triple"]
|
||||
source = root / "vendor" / target
|
||||
suffix = ".exe" if platform["os"] == "win32" else ""
|
||||
metadata = {
|
||||
"layoutVersion": 1,
|
||||
"variant": "codex",
|
||||
"version": "1.2.3",
|
||||
"target": target,
|
||||
"entrypoint": f"bin/codex{suffix}",
|
||||
"execServer": f"bin/codex-exec-server{suffix}",
|
||||
"resourcesDir": "codex-resources",
|
||||
"pathDir": "codex-path",
|
||||
}
|
||||
files = [
|
||||
metadata["entrypoint"],
|
||||
metadata["execServer"],
|
||||
f"bin/codex-code-mode-host{suffix}",
|
||||
f"codex-path/rg{suffix}",
|
||||
]
|
||||
if platform["os"] == "linux":
|
||||
files.append("codex-resources/bwrap")
|
||||
elif platform["os"] == "win32":
|
||||
files.extend(
|
||||
[
|
||||
"codex-resources/codex-command-runner.exe",
|
||||
"codex-resources/codex-windows-sandbox-setup.exe",
|
||||
]
|
||||
)
|
||||
for name in files:
|
||||
destination = source / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(executable, destination)
|
||||
(source / "codex-package.json").write_text(json.dumps(metadata))
|
||||
original = {
|
||||
path.relative_to(source).as_posix(): path.read_bytes()
|
||||
for path in source.rglob("*")
|
||||
if path.is_file()
|
||||
}
|
||||
archive_path = root / "package.tgz"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(
|
||||
REPO_ROOT / "codex-cli" / "scripts" / "build_npm_package.py"
|
||||
),
|
||||
"--package",
|
||||
package,
|
||||
"--version",
|
||||
"1.2.3",
|
||||
"--vendor-src",
|
||||
str(source.parent),
|
||||
"--staging-dir",
|
||||
str(root / "staged"),
|
||||
"--pack-output",
|
||||
str(archive_path),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
with tarfile.open(archive_path, "r:gz") as archive:
|
||||
prefix = f"package/vendor/{target}/"
|
||||
actual = {
|
||||
member.name.removeprefix(prefix): archive.extractfile(
|
||||
member
|
||||
).read()
|
||||
for member in archive.getmembers()
|
||||
if member.isfile() and member.name.startswith(prefix)
|
||||
}
|
||||
expected = original.copy()
|
||||
metadata = json.loads(expected.pop("codex-package.json"))
|
||||
expected.pop(metadata.pop("execServer"))
|
||||
self.assertEqual(json.loads(actual.pop("codex-package.json")), metadata)
|
||||
self.assertEqual(actual, expected)
|
||||
self.assertEqual(
|
||||
{
|
||||
path.relative_to(source).as_posix(): path.read_bytes()
|
||||
for path in source.rglob("*")
|
||||
if path.is_file()
|
||||
},
|
||||
original,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ The builder creates a canonical Codex package directory:
|
||||
├── codex-package.json
|
||||
├── bin
|
||||
│ ├── <entrypoint>[.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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" ] &&
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user