Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ has matching runner capacity, secrets, and branch-protection expectations.
- `codex-lab-app`

The runner must have Rust, Python 3, Xcode command line tools, and macOS
`ditto` available. The generated Codex Lab app artifact is currently unsigned.
`ditto` available. Release runs also require the keychain identity
`Developer ID Application: Shiny Computers Leasing LLC (MM5YXC7T6E)`; the
workflow checks for that exact identity before signing. The generated Codex Lab
app artifact is currently unsigned.

`exec-harness.yml` expects a self-hosted Linux x64 runner with these labels:

Expand Down Expand Up @@ -93,35 +96,47 @@ default target directory when no artifact root is configured or available.

- `codex-lab-app-aarch64-apple-darwin.zip`
- `codex-lab-shim-aarch64-apple-darwin.zip`
- `codex-lab-engine-aarch64-apple-darwin.zip`
- `SHA256SUMS`
- `codex-lab-distribution.json`

The distribution manifest is the contract for future installers and updaters.
It marks the app zip as the canonical app update unit, the shim zip as a
companion wrapper, and records supported layouts for extracted sibling installs,
`CODEX_LAB_APP_PATH` overrides, and `/Applications` installs. Artifacts remain
`signed: false` and `notarized: false` until the signing pipeline exists.
companion wrapper, and the engine zip as the managed supervisor execution unit.
It also records supported layouts for extracted sibling installs,
`CODEX_LAB_APP_PATH` overrides, and `/Applications` installs. Pull-request app
artifacts keep all three payloads unsigned so untrusted changes never receive
signing credentials; their manifest is packaging-validation metadata, not a
publishable installer manifest.

## Codex Lab Release Publication

`codex-lab-release.yml` builds the same macOS ARM64 distribution files and
stages them for GitHub Releases. It separates trust boundaries deliberately:

- the self-hosted macOS runner builds and uploads a workflow artifact with
`contents: read` permissions;
`codex-lab-release.yml` builds the macOS ARM64 app, shim, and engine, then signs
and verifies the engine before staging the final distribution for GitHub
Releases. It separates trust boundaries deliberately:

- the self-hosted macOS runner builds the app and shim, copies the release
engine, and signs it with the runner's Shiny Developer ID identity while the
job retains only `contents: read` permissions;
- that same job applies hardened runtime plus
`com.apple.security.cs.allow-jit`, then validates the signature,
TeamIdentifier, entitlement, executable digest, source commit, and version
before archiving the engine;
- an `ubuntu-latest` validation job downloads the staged artifact, verifies
checksums, and checks that the manifest has release metadata and download
URLs. This validates internal consistency, not artifact provenance;
- a separate `ubuntu-latest` publish job has `contents: write` and creates a
public prerelease only for explicit manual dispatches with `publish: true`.

Manual dispatch with `publish: false` is the dry-run path: it builds and
validates the release artifact set, including checking that the release tag is
available, without creating a GitHub Release. Publishing is restricted to manual
dispatches from the repository default branch. Published Codex Lab releases are
public prereleases and are not marked as latest while the artifacts remain
unsigned and unnotarized. Public prereleases are used so manifest `downloadUrl`
entries are immediately usable by installers and updaters.
Manual dispatch with `publish: false` is the dry-run path: it builds, signs, and
validates the release artifact set, including checking that the
release tag is available, without creating a GitHub Release. Publishing is
restricted to manual dispatches from the repository default branch. Published
Codex Lab releases remain public prereleases and are not marked as latest. The
app and shim are unsigned Lab launch surfaces; the managed engine is the signed
execution boundary whose digest, source commit, version, stable identifier,
TeamIdentifier, and JIT entitlement are pinned by the installer and LaunchAgent
supervisor.

Release IDs use this namespace:

Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/codex-lab-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,28 @@ jobs:
mkdir -p "$dist_dir"
app_zip="${dist_dir}/codex-lab-app-aarch64-apple-darwin.zip"
shim_zip="${dist_dir}/codex-lab-shim-aarch64-apple-darwin.zip"
engine_zip="${dist_dir}/codex-lab-engine-aarch64-apple-darwin.zip"
engine_dir="${output_root}/engine"
source_commit="$(git rev-parse HEAD)"
mkdir -p "$engine_dir"
cp "$CODEX_LAB_BIN" "${engine_dir}/codex"
chmod 0755 "${engine_dir}/codex"
start=$SECONDS
ditto -c -k --norsrc --keepParent "${output_root}/Codex Lab.app" "$app_zip"
app_zip_seconds=$((SECONDS - start))
start=$SECONDS
ditto -c -k --norsrc --keepParent "${output_root}/bin/codex-lab" "$shim_zip"
shim_zip_seconds=$((SECONDS - start))
start=$SECONDS
ditto -c -k --norsrc "${engine_dir}/codex" "$engine_zip"
engine_zip_seconds=$((SECONDS - start))
app_zip_bytes="$(wc -c < "$app_zip" | tr -d '[:space:]')"
shim_zip_bytes="$(wc -c < "$shim_zip" | tr -d '[:space:]')"
engine_zip_bytes="$(wc -c < "$engine_zip" | tr -d '[:space:]')"
unzip -l "$app_zip" | grep -F "Codex Lab.app/Contents/Resources/codex-lab"
unzip -l "$shim_zip" | grep -F "bin/codex-lab"
if { unzip -l "$app_zip"; unzip -l "$shim_zip"; } | grep -E '/\._|(^|[[:space:]])\._'; then
unzip -l "$engine_zip" | grep -F "codex"
if { unzip -l "$app_zip"; unzip -l "$shim_zip"; unzip -l "$engine_zip"; } | grep -E '/\._|(^|[[:space:]])\._'; then
echo "unexpected AppleDouble file in archive" >&2
exit 1
fi
Expand All @@ -181,9 +191,11 @@ jobs:
{
echo "- App zip seconds: \`$app_zip_seconds\`"
echo "- Shim zip seconds: \`$shim_zip_seconds\`"
echo "- Engine zip seconds: \`$engine_zip_seconds\`"
echo "- Manifest/SHA seconds: \`$manifest_seconds\`"
echo "- App zip bytes: \`$app_zip_bytes\`"
echo "- Shim zip bytes: \`$shim_zip_bytes\`"
echo "- Engine zip bytes: \`$engine_zip_bytes\`"
} >> "$GITHUB_STEP_SUMMARY"
echo "dist_dir=$dist_dir" >> "$GITHUB_OUTPUT"

Expand Down
89 changes: 85 additions & 4 deletions .github/workflows/codex-lab-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,56 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
echo "output_root=$output_root" >> "$GITHUB_OUTPUT"

- name: Sign and verify managed Codex Lab engine
id: engine
shell: bash
run: |
set -euo pipefail
engine_dir="${RUNNER_TEMP}/codex-lab-signed-engine"
engine_path="${engine_dir}/codex"
engine_identifier="$(PYTHONPATH=scripts python3 -c 'from codex_lab_package.engine_contract import ENGINE_SIGNING_IDENTIFIER; print(ENGINE_SIGNING_IDENTIFIER)')"
engine_team_identifier="$(PYTHONPATH=scripts python3 -c 'from codex_lab_package.engine_contract import ENGINE_TEAM_IDENTIFIER; print(ENGINE_TEAM_IDENTIFIER)')"
signing_identity="Developer ID Application: Shiny Computers Leasing LLC (${engine_team_identifier})"
signing_keychain="$HOME/Library/Keychains/login.keychain-db"
rm -rf "$engine_dir"
mkdir -p "$engine_dir"
cp "$CODEX_LAB_BIN" "$engine_path"
chmod 0755 "$engine_path"
security unlock-keychain -p "" "$signing_keychain"
security set-keychain-settings -lut 21600 "$signing_keychain"
security find-identity -v -p codesigning | grep -F "$signing_identity"
.github/scripts/macos-signing/sign_macos_code.sh \
--target "$engine_path" \
--identity "$signing_identity" \
--deep false \
--identifier "$engine_identifier" \
--options runtime \
--timestamp true \
--entitlements .github/scripts/macos-signing/codex.entitlements.plist
PYTHONPATH=scripts python3 - "$engine_path" "$(git rev-parse HEAD)" <<'PY'
from pathlib import Path
import sys

from codex_lab_package.engine_contract import ENGINE_SIGNING_IDENTIFIER
from codex_lab_package.engine_contract import ENGINE_TEAM_IDENTIFIER
from codex_lab_package.supervisor import inspect_engine
from codex_package.version import read_workspace_version

identity = inspect_engine(Path(sys.argv[1]))
expected_source_commit = sys.argv[2]
expected_version = read_workspace_version()
if identity.signing_identifier != ENGINE_SIGNING_IDENTIFIER:
raise ValueError(f"unexpected engine signing identifier: {identity}")
if identity.team_identifier != ENGINE_TEAM_IDENTIFIER:
raise ValueError(f"unexpected engine TeamIdentifier: {identity}")
if identity.source_commit != expected_source_commit:
raise ValueError(f"unexpected engine source commit: {identity}")
if identity.version != expected_version:
raise ValueError(f"unexpected engine version: {identity}")
print(identity)
PY
echo "engine_path=$engine_path" >> "$GITHUB_OUTPUT"

- name: Archive Codex Lab release artifacts
id: archive
env:
Expand All @@ -166,27 +216,45 @@ jobs:
run: |
set -euo pipefail
output_root="${{ steps.package.outputs.output_root }}"
signed_engine="${{ steps.engine.outputs.engine_path }}"
release_tag="${RELEASE_TAG:?missing release tag}"
dist_dir="${RUNNER_TEMP}/codex-lab-dist"
engine_dir="${RUNNER_TEMP}/codex-lab-engine-archive"
mkdir -p "$dist_dir"
app_zip="${dist_dir}/codex-lab-app-aarch64-apple-darwin.zip"
shim_zip="${dist_dir}/codex-lab-shim-aarch64-apple-darwin.zip"
engine_zip="${dist_dir}/codex-lab-engine-aarch64-apple-darwin.zip"
source_commit="$(git rev-parse HEAD)"
download_base_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${release_tag}"
rm -rf "$engine_dir"
mkdir -p "$engine_dir"
cp "$signed_engine" "${engine_dir}/codex"
start=$SECONDS
ditto -c -k --norsrc --keepParent "${output_root}/Codex Lab.app" "$app_zip"
app_zip_seconds=$((SECONDS - start))
start=$SECONDS
ditto -c -k --norsrc --keepParent "${output_root}/bin/codex-lab" "$shim_zip"
shim_zip_seconds=$((SECONDS - start))
start=$SECONDS
ditto -c -k --norsrc "${engine_dir}/codex" "$engine_zip"
engine_zip_seconds=$((SECONDS - start))
app_zip_bytes="$(wc -c < "$app_zip" | tr -d '[:space:]')"
shim_zip_bytes="$(wc -c < "$shim_zip" | tr -d '[:space:]')"
engine_zip_bytes="$(wc -c < "$engine_zip" | tr -d '[:space:]')"
unzip -l "$app_zip" | grep -F "Codex Lab.app/Contents/Resources/codex-lab"
unzip -l "$shim_zip" | grep -F "bin/codex-lab"
if { unzip -l "$app_zip"; unzip -l "$shim_zip"; } | grep -E '/\._|(^|[[:space:]])\._'; then
unzip -l "$engine_zip" | grep -F "codex"
if { unzip -l "$app_zip"; unzip -l "$shim_zip"; unzip -l "$engine_zip"; } | grep -E '/\._|(^|[[:space:]])\._'; then
echo "unexpected AppleDouble file in archive" >&2
exit 1
fi
engine_validation_dir="${RUNNER_TEMP}/codex-lab-engine-archive-validation"
rm -rf "$engine_validation_dir"
mkdir -p "$engine_validation_dir"
ditto -x -k "$engine_zip" "$engine_validation_dir"
PYTHONPATH=scripts python3 -c \
'from pathlib import Path; import sys; from codex_lab_package.supervisor import inspect_engine; print(inspect_engine(Path(sys.argv[1])))' \
"${engine_validation_dir}/codex"
start=$SECONDS
(cd "$dist_dir" && shasum -a 256 -- *.zip > SHA256SUMS)
python3 scripts/build_codex_lab_distribution_manifest.py generate \
Expand All @@ -199,7 +267,8 @@ jobs:
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--release-tag "$release_tag" \
--download-base-url "$download_base_url"
--download-base-url "$download_base_url" \
--engine-signed
python3 scripts/build_codex_lab_distribution_manifest.py validate \
"${dist_dir}/codex-lab-distribution.json" \
--dist-dir "$dist_dir" \
Expand All @@ -210,9 +279,11 @@ jobs:
{
echo "- App zip seconds: \`$app_zip_seconds\`"
echo "- Shim zip seconds: \`$shim_zip_seconds\`"
echo "- Engine zip seconds: \`$engine_zip_seconds\`"
echo "- Manifest/SHA seconds: \`$manifest_seconds\`"
echo "- App zip bytes: \`$app_zip_bytes\`"
echo "- Shim zip bytes: \`$shim_zip_bytes\`"
echo "- Engine zip bytes: \`$engine_zip_bytes\`"
} >> "$GITHUB_STEP_SUMMARY"
echo "dist_dir=$dist_dir" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -242,12 +313,18 @@ jobs:
set -euo pipefail
test -f dist/codex-lab-app-aarch64-apple-darwin.zip
test -f dist/codex-lab-shim-aarch64-apple-darwin.zip
test -f dist/codex-lab-engine-aarch64-apple-darwin.zip
test -f dist/SHA256SUMS
test -f dist/codex-lab-distribution.json
(cd dist && sha256sum -c SHA256SUMS)
jq -e \
--arg tag "${{ needs.build-macos-aarch64.outputs.release_tag }}" \
'.release.tag == $tag and .artifacts.appZip.downloadUrl and .artifacts.shimZip.downloadUrl' \
'.release.tag == $tag
and .artifacts.appZip.downloadUrl
and .artifacts.shimZip.downloadUrl
and .artifacts.engineZip.downloadUrl
and .artifacts.engineZip.signed == true
and .managedEngine.sha256' \
dist/codex-lab-distribution.json

- name: Verify release tag is available
Expand Down Expand Up @@ -300,11 +377,14 @@ jobs:
cat > release-notes.md <<'EOF'
Codex Lab macOS ARM64 distribution artifact.

This prerelease is unsigned and not notarized. It is intended for Codex Lab validation before the signing and updater pipeline exists.
The managed Codex Lab engine is individually Developer ID signed and
carries the required V8 JIT entitlement. The app and companion shim
remain unsigned Lab launch surfaces.

Assets:
- codex-lab-app-aarch64-apple-darwin.zip
- codex-lab-shim-aarch64-apple-darwin.zip
- codex-lab-engine-aarch64-apple-darwin.zip
- codex-lab-distribution.json
- SHA256SUMS
EOF
Expand Down Expand Up @@ -350,5 +430,6 @@ jobs:
--latest=false \
dist/codex-lab-app-aarch64-apple-darwin.zip \
dist/codex-lab-shim-aarch64-apple-darwin.zip \
dist/codex-lab-engine-aarch64-apple-darwin.zip \
dist/codex-lab-distribution.json \
dist/SHA256SUMS
57 changes: 35 additions & 22 deletions scripts/codex_lab_package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,23 @@ GUI is running beside the launchd-supervised websocket app-server. The embedded
and managed CLI builds must have matching fixed source/build provenance.

The GitHub workflow uploads `codex-lab-distribution.json` beside the app zip,
shim zip, and `SHA256SUMS`. The manifest records artifact roles, sizes,
checksums, source workflow metadata, supported install layouts, release tags,
download URLs when published, and the current signing state. Codex Lab artifacts
are currently marked `signed: false` and `notarized: false` until a later
signing/notarization stage is implemented.
shim zip, managed-engine zip, and `SHA256SUMS`. The manifest records artifact
roles, sizes, checksums, source workflow metadata, supported install layouts,
release tags, download URLs when published, and the managed engine's binary
digest, Developer ID identifier, TeamIdentifier, version, source commit, and
required JIT entitlement. PR app artifacts carry an unsigned engine for package
validation; published release manifests require the engine artifact to be
individually Developer ID signed.

Packaging workflows bind the static smoke to the expected source commit before
the interactive GUI smoke is performed.

## Installing a published release

The current published-release installer installs only the app and optional shim.
It does not yet provision the individually signed managed engine or its user
LaunchAgent. Until signed engine provisioning is added to the release path, the
launcher intentionally fails closed unless that matching supervisor has already
been installed by the Codex Lab canary workflow.
The published-release installer provisions the app, optional shim, individually
signed managed engine, and the `dev.everycode.codex-lab.app-server.v1` user
LaunchAgent as one rollback-aware transaction. No manual canary provisioning is
required for a supported release.

Use `scripts/install_codex_lab.py` to install or manually update Codex Lab from a
published release manifest:
Expand Down Expand Up @@ -122,15 +123,27 @@ scripts/install_codex_lab.py --update
```

`--update` reads the recorded install state, preserves the installed app path and
shim path, and replaces only when a newer published Lab release is available.

The installer downloads the manifest, `SHA256SUMS`, app zip, and shim zip into a
temporary staging directory. It validates the manifest shape, requires artifact
URLs to be siblings of the manifest URL, checks artifact sizes and SHA-256
hashes, rejects unsafe zip members, smoke-checks the staged app and shim, then
replaces the requested install paths. Existing targets are refused unless
`--force` is supplied.

Codex Lab release artifacts are currently unsigned and unnotarized. This
installer is a manual Lab installer/update path; silent automatic updates should
wait for signed or notarized artifacts, or a signed manifest.
shim path, installs the matching engine, and restarts the pinned supervisor only
when a newer published Lab release is available. It does not enable the upstream
standalone updater.

To remove the recorded install and restore any managed engine that predated the
first supported installer run, use:

```shell
scripts/install_codex_lab.py --uninstall
```

The installer downloads the manifest, `SHA256SUMS`, app zip, shim zip, and engine
zip into a temporary staging directory. It validates release URLs, sizes, and
SHA-256 hashes; rejects unsafe zip members; smoke-checks the app and shim; and
uses macOS code-signing inspection plus engine provenance to require the exact
binary digest, source commit, version, stable identifier, TeamIdentifier, and V8
JIT entitlement from the release metadata. It then replaces the engine, app,
shim, and state as a rollback set before installing and health-checking the
LaunchAgent. A provisioning failure restores the prior files and the
supervisor's own rollback restores its prior runner, plist, and load state.
Existing targets are refused unless `--force` is supplied.

The app and shim remain unsigned Lab launch surfaces. The managed engine is the
individually Developer ID signed execution boundary pinned by the supervisor.
Loading
Loading