fix: assemble verified host runner assets before release

Reuse release verification producers for the supported native hosts. Validate source-bound data before publication and probe the installed Codex command in the existing offline consumer.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 07:02:16 -05:00
1 parent 5fa4abaead
commit f8e2bee66e
10 files changed
+1061 -19

No files matched your search

+190
View File
@@ -7,18 +7,28 @@ on:
description: Commit SHA, branch, or tag to verify
required: true
type: string
runner_assets_only:
description: Build only the release daemon/provider-pack data after the caller's source gate
type: boolean
default: false
outputs:
runner_assets_artifact:
description: Run-bound verified release daemon/provider-pack data
value: ${{ jobs.runner_release_assemble.outputs.artifact_name }}
# Caller-provided refs may name unmerged PR code. AWS is eligible only when
# the caller runs on canonical master and verifies that event's exact SHA.
# The organization group also restricts these workflow files to master.
jobs:
runner_chaos_evals:
if: ${{ !inputs.runner_assets_only }}
name: Pre-release Runner chaos evals
uses: ./.github/workflows/runner-chaos-evals.yml
with:
ref: ${{ inputs.ref }}
typecheck:
if: ${{ !inputs.runner_assets_only }}
name: Typecheck
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -97,6 +107,7 @@ jobs:
run: pnpm -r typecheck
general_tests:
if: ${{ !inputs.runner_assets_only }}
name: General tests (${{ matrix.group_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -204,6 +215,7 @@ jobs:
fi
serialized_tests:
if: ${{ !inputs.runner_assets_only }}
name: Serialized tests (${{ matrix.shard_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -253,6 +265,7 @@ jobs:
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
runner_workflow_evals:
if: ${{ !inputs.runner_assets_only }}
name: Runner workflow eval scorer contract
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 10
@@ -283,6 +296,7 @@ jobs:
run: pnpm test:runner-workflow-evals
verify_paperclip_runner:
if: ${{ !inputs.runner_assets_only }}
name: Verify Paperclip Runner (${{ matrix.lane }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -396,6 +410,7 @@ jobs:
run: pnpm --filter @paperclipai/paperclip-runner build:rust
build:
if: ${{ !inputs.runner_assets_only }}
name: Build
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
@@ -424,3 +439,178 @@ jobs:
- name: Build
run: pnpm build
# Only the missing release producers. Source verification stays with the
# caller; these unprivileged jobs neither publish nor receive provider secrets.
runner_release_binaries:
needs: runner_release_request
if: inputs.runner_assets_only
name: Release daemon (${{ matrix.target }})
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
runner: macos-15
- target: darwin-x64
runner: macos-15-intel
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- name: Build the exact native release daemon
working-directory: packages/paperclip-runner
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
rustup show
cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd
- name: Record native architecture, checksum and executable metadata
env:
SOURCE_SHA: ${{ inputs.ref }}
TARGET: ${{ matrix.target }}
run: |
node scripts/release-runner-artifacts.mjs record-binary "$SOURCE_SHA" "$TARGET" \
"$GITHUB_WORKSPACE/packages/paperclip-runner/runner/target/release/paperclip-runnerd" \
"$RUNNER_TEMP/runner-release/$TARGET"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.target }}
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 7
runner_release_pack:
needs: runner_release_request
if: inputs.runner_assets_only
name: Actual release image Linux daemon and provider-pack identity
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
run_install: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Extract the actual exact-source image provider-pack
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
mkdir -p "$RUNNER_TEMP/runner-release-input"
owner="paperclip-release-pack-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
trap 'docker rm -f "$owner" >/dev/null 2>&1 || true' EXIT
# Canary images publish after npm. Build the existing image target
# without registry credentials and consume its actual source bytes.
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
image="$owner"
docker buildx build --platform linux/amd64 --target runner-provider-pack \
--build-arg "PAPERCLIP_BUILD_COMMIT=$SOURCE_SHA" \
--label "org.opencontainers.image.revision=$SOURCE_SHA" \
--cache-from type=registry,ref=ghcr.io/paperclipai/paperclip:buildcache-amd64 \
--tag "$image" --load .
pack_path=/provider-pack/provider-pack.json
docker image inspect "$image" > "$RUNNER_TEMP/runner-release-input/image.json"
daemon_path=/app/server/dist/vendor/paperclip-runner/bin/paperclip-runnerd
docker create --name "$owner" --network none --user 1000:1000 --read-only \
--cap-drop ALL --security-opt no-new-privileges \
--entrypoint "$daemon_path" "$image" --build-metadata >/dev/null
docker cp "$owner:$pack_path" "$RUNNER_TEMP/runner-release-input/provider-pack.json"
docker cp "$owner:$daemon_path" "$RUNNER_TEMP/runner-release-input/paperclip-runnerd"
# Execute only inside the source image. Its libc may be newer than
# ubuntu-latest; host execution would not prove this image artifact.
timeout 30s docker start --attach "$owner" > "$RUNNER_TEMP/runner-release-input/daemon-metadata.json"
test "$(docker inspect --format '{{.State.ExitCode}}' "$owner")" = 0
node scripts/release-runner-artifacts.mjs record-image-binary "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-input/paperclip-runnerd" "$RUNNER_TEMP/runner-release-input/daemon-metadata.json" \
"$RUNNER_TEMP/runner-release-input/image.json" "$RUNNER_TEMP/runner-release/linux-x64"
node scripts/release-runner-artifacts.mjs record-pack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-input/provider-pack.json" "$RUNNER_TEMP/runner-release-input/image.json" \
"$RUNNER_TEMP/runner-release/provider-pack"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-provider-pack
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 7
runner_release_assemble:
if: inputs.runner_assets_only
name: Assemble verified release runner data
needs: [runner_release_binaries, runner_release_pack]
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
artifact_name: ${{ steps.identity.outputs.artifact_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: runner-release-${{ inputs.ref }}-${{ github.run_id }}-${{ github.run_attempt }}-*
path: ${{ runner.temp }}/runner-release-inputs
merge-multiple: true
- name: Run the sole source-aware assembler and bound its data envelope
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node scripts/release-runner-artifacts.mjs assemble "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-inputs" "$RUNNER_TEMP/runner-release-assembled"
- name: Record run-bound assembled artifact
id: identity
env:
SOURCE_SHA: ${{ inputs.ref }}
run: echo "artifact_name=runner-release-assembled-$SOURCE_SHA-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.identity.outputs.artifact_name }}
path: |
${{ runner.temp }}/runner-release-assembled/runner-release-assets.tar.gz
${{ runner.temp }}/runner-release-assembled/runner-release-assets.tar.gz.sha256
if-no-files-found: error
retention-days: 7
runner_release_request:
if: inputs.runner_assets_only
name: Validate immutable runner producer inputs
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Reject mutable sources before any producer checkout
env:
SOURCE_SHA: ${{ inputs.ref }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
+236 -8
View File
@@ -337,8 +337,11 @@ jobs:
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
publish_canary:
if: github.event_name == 'push'
needs: verify_canary
if: >-
!cancelled() && github.event_name == 'push' && needs.verify_canary.result == 'success' &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [verify_canary, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-canary
@@ -350,10 +353,14 @@ jobs:
# For the explicit docker.yml dispatch below.
actions: write
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
- name: Setup pnpm
@@ -366,6 +373,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -587,11 +621,13 @@ jobs:
artifact_name: nightly-release-smoke
publish_nightly:
needs: [select_nightly, smoke_nightly]
needs: [select_nightly, smoke_nightly, pin_runner_release_source, release_runner_assets]
# Publish when smoke passed, or when smoke was deliberately skipped by a
# dry-run dispatch (the publish itself is a dry-run in that case).
if: >-
!cancelled() &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success') &&
needs.select_nightly.outputs.proceed == 'true' &&
(needs.smoke_nightly.result == 'success' ||
(needs.smoke_nightly.result == 'skipped' && github.event_name == 'workflow_dispatch' && inputs.dry_run))
@@ -611,10 +647,14 @@ jobs:
id-token: write
actions: write
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
ref: ${{ needs.select_nightly.outputs.sha }}
fetch-depth: 0
@@ -628,6 +668,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -855,9 +922,11 @@ jobs:
ref: ${{ needs.select_beta.outputs.sha }}
publish_beta:
needs: [select_beta, verify_beta_candidate]
needs: [select_beta, verify_beta_candidate, pin_runner_release_source, release_runner_assets]
if: >-
!cancelled() &&
needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success') &&
needs.select_beta.result == 'success' &&
(needs.verify_beta_candidate.result == 'success' ||
(needs.verify_beta_candidate.result == 'skipped' && needs.select_beta.outputs.mode == 'promote'))
@@ -877,10 +946,14 @@ jobs:
outputs:
beta_version: ${{ steps.result.outputs.beta_version }}
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
ref: ${{ needs.select_beta.outputs.sha }}
fetch-depth: 0
@@ -894,6 +967,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -1240,17 +1340,24 @@ jobs:
ref: ${{ needs.preflight_stable.outputs.sha }}
preview_stable:
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.dry_run
needs: [preflight_stable, verify_stable]
if: >-
!cancelled() && github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.dry_run &&
needs.verify_stable.result == 'success' && needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [preflight_stable, verify_stable, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
@@ -1264,6 +1371,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
@@ -1282,8 +1416,11 @@ jobs:
./scripts/release.sh "${args[@]}"
publish_stable:
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.dry_run
needs: [preflight_stable, verify_stable]
if: >-
!cancelled() && github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.dry_run &&
needs.verify_stable.result == 'success' && needs.pin_runner_release_source.result == 'success' &&
(needs.pin_runner_release_source.outputs.required != 'true' || needs.release_runner_assets.result == 'success')
needs: [preflight_stable, verify_stable, pin_runner_release_source, release_runner_assets]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-stable
@@ -1294,10 +1431,14 @@ jobs:
outputs:
stable_version: ${{ steps.tag.outputs.version }}
defaults:
run:
working-directory: source
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
path: source
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
@@ -1311,6 +1452,33 @@ jobs:
with:
node-version: 24
cache: pnpm
cache-dependency-path: source/pnpm-lock.yaml
- name: Checkout trusted release transfer tools
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.sha }}
path: trusted
persist-credentials: false
- name: Download this run's verified runner data
if: needs.pin_runner_release_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.release_runner_assets.outputs.runner_assets_artifact }}
path: ${{ runner.temp }}/runner-release-archive
- name: Validate runner data against the selected source before publishing
if: needs.pin_runner_release_source.outputs.required == 'true'
env:
SOURCE_SHA: ${{ needs.pin_runner_release_source.outputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
node "$GITHUB_WORKSPACE/trusted/scripts/release-runner-artifacts.mjs" unpack "$SOURCE_SHA" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz" \
"$RUNNER_TEMP/runner-release-archive/runner-release-assets.tar.gz.sha256" \
"$RUNNER_TEMP/runner-release-assets" "$GITHUB_WORKSPACE/source"
echo "PAPERCLIP_RELEASE_RUNNER_ASSETS=$RUNNER_TEMP/runner-release-assets" >> "$GITHUB_ENV"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -1477,3 +1645,63 @@ jobs:
echo "- Open and merge the PR (a human opens it so CI runs):"
echo " https://github.com/${GITHUB_REPOSITORY}/compare/master...${branch}?expand=1"
} >> "$GITHUB_STEP_SUMMARY"
# Resolve one applicable release channel after its existing gate. Optional
# channels are deliberately skipped, so use explicit results, never an
# implicit all-needs-success dependency that would suppress promotions.
pin_runner_release_source:
needs: [verify_canary, select_nightly, select_beta, verify_beta_candidate, preflight_stable, verify_stable]
if: >-
!cancelled() &&
((github.event_name == 'push' && needs.verify_canary.result == 'success') ||
(needs.select_nightly.result == 'success' && needs.select_nightly.outputs.proceed == 'true') ||
(needs.select_beta.result == 'success' &&
(needs.verify_beta_candidate.result == 'success' || needs.select_beta.outputs.mode == 'promote')) ||
(needs.preflight_stable.result == 'success' && needs.verify_stable.result == 'success'))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
source_sha: ${{ steps.pin.outputs.source_sha }}
required: ${{ steps.pin.outputs.required }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- name: Pin the gated source and identify its release assembly contract
id: pin
env:
EVENT_NAME: ${{ github.event_name }}
CANARY_SHA: ${{ github.sha }}
NIGHTLY_SHA: ${{ needs.select_nightly.outputs.sha }}
BETA_SHA: ${{ needs.select_beta.outputs.sha }}
STABLE_SHA: ${{ needs.preflight_stable.outputs.sha }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == push ]]; then source_sha="$CANARY_SHA";
elif [[ -n "$NIGHTLY_SHA" ]]; then source_sha="$NIGHTLY_SHA";
elif [[ -n "$BETA_SHA" ]]; then source_sha="$BETA_SHA";
else source_sha="$STABLE_SHA"; fi
[[ "$source_sha" =~ ^[a-f0-9]{40}$ ]]
git cat-file -e "$source_sha^{commit}"
required=false
# Promoted older sources retain their original contract. New sources
# opt in with the checked-in hook and may never publish partial assets.
if git show "$source_sha:scripts/release.sh" | grep -F 'PAPERCLIP_RELEASE_RUNNER_ASSETS' >/dev/null; then
git cat-file -e "$source_sha:scripts/release-runner-artifacts.mjs"
required=true
fi
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "required=$required" >> "$GITHUB_OUTPUT"
release_runner_assets:
needs: pin_runner_release_source
if: needs.pin_runner_release_source.outputs.required == 'true'
permissions:
contents: read
uses: ./.github/workflows/release-verify.yml
with:
ref: ${{ needs.pin_runner_release_source.outputs.source_sha }}
runner_assets_only: true
@@ -7,7 +7,14 @@ const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(
: JSON.stringify(value);
/** Release assembly must reject a self-consistent pack built for another release. */
export function verifyReleaseProviderPack(identity, sourceRevision) {
export function verifyReleaseProviderPack(identity, sourceRevision, sourceProfiles = profiles, sourceDistributions = distributions) {
if (sourceProfiles?.schema !== "paperclip.acpx-profiles.v1" || typeof sourceProfiles.acpxVersion !== "string" || !sourceProfiles.acpxVersion
|| ["grok", "claude", "codex", "cursor"].some(agent => !/^sha256:[a-f0-9]{64}$/.test(sourceProfiles?.profiles?.[agent]?.commandDigest ?? ""))
|| typeof sourceProfiles?.profiles?.cursor?.agentServerVersion !== "string" || !sourceProfiles.profiles.cursor.agentServerVersion
|| sourceDistributions?.schema !== "paperclip.cursor_distribution.v1"
|| !/^[a-f0-9]{64}$/.test(sourceDistributions?.platforms?.["linux-x64"]?.closureSha256 ?? "")) {
throw new Error("Invalid release qualification snapshots");
}
const payload = identity?.payload;
if (identity?.schema !== "paperclip-runner/remote-provider-pack/v1" || !payload
|| payload.target?.platform !== "linux" || payload.target?.architecture !== "x64"
@@ -15,14 +22,14 @@ export function verifyReleaseProviderPack(identity, sourceRevision) {
if (identity.digest !== `sha256:${createHash("sha256").update(canonical(payload)).digest("hex")}`) {
throw new Error("Remote provider-pack payload digest mismatch");
}
if (payload.pins?.acpx !== profiles.acpxVersion
|| ["grok", "claude", "codex"].some(agent => payload.acpxProfileDigests?.[agent] !== profiles.profiles[agent].commandDigest)) {
if (payload.pins?.acpx !== sourceProfiles.acpxVersion
|| ["grok", "claude", "codex"].some(agent => payload.acpxProfileDigests?.[agent] !== sourceProfiles.profiles[agent].commandDigest)) {
throw new Error("Remote provider-pack ACPX profiles do not match this release");
}
const current = profiles.profiles.cursor;
const current = sourceProfiles.profiles.cursor;
for (const cursor of [payload.providers?.cursor, ...(Object.hasOwn(payload.candidateProviders ?? {}, "cursor") ? [payload.candidateProviders.cursor] : [])]) {
if (!cursor || cursor.version !== current.agentServerVersion || cursor.profileDigest !== current.commandDigest
|| cursor.closureDigest !== `sha256:${distributions.platforms["linux-x64"].closureSha256}`
|| cursor.closureDigest !== `sha256:${sourceDistributions.platforms["linux-x64"].closureSha256}`
|| cursor.qualification !== "qualified" || cursor.path !== "provider-assets/cursor/linux-x64"
|| !/^sha256:[a-f0-9]{64}$/.test(cursor.sha256 ?? "")) {
throw new Error("Remote provider-pack Cursor identity does not match this release");
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -9,6 +9,8 @@ import test from "node:test";
import profiles from "../acpx-profiles.json" with { type: "json" };
import distributions from "../cursor-distributions.json" with { type: "json" };
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
import { makeRunnerReleaseArchive, readRunnerReleaseArchive, verifyReleaseDaemonMetadata, verifyRunnerReleaseData } from "../../../scripts/release-runner-artifacts.mjs";
import { gzipSync, gunzipSync } from "node:zlib";
const revision = "a".repeat(40);
const digest = "sha256:" + "b".repeat(64);
@@ -26,6 +28,162 @@ function fixture() {
function seal(payload) { return { schema: "paperclip-runner/remote-provider-pack/v1", payload,
digest: `sha256:${createHash("sha256").update(canonical(payload)).digest("hex")}` }; }
function daemonMetadata() { return { schema: 'paperclip-runner/runnerd-build-metadata/v1', binaryName: 'paperclip-runnerd',
packageName: '@paperclipai/paperclip-runner', packageVersion: '0.0.0', binaryContractVersion: 2, nativeExecutionVersion: 1,
harnessDriverVersion: 1, prp: { name: 'paperclip.runner', minimumVersion: 1, maximumVersion: 2 },
durableSessionCapabilities: ['unlimited_runtime', 'connection_lease_renewal'],
prpTransportModes: ['dial_ws_loopback', 'dial_wss', 'listen_ws'] }; }
test('release daemon compatibility rejects lookalikes, incompatible contracts and missing production capabilities', () => {
assert.equal(verifyReleaseDaemonMetadata(daemonMetadata()).binaryContractVersion, 2);
for (const mismatch of [
{ schema: 'other' }, { binaryName: 'other' }, { packageName: 'other' }, { packageVersion: '' },
{ binaryContractVersion: 999 }, { nativeExecutionVersion: 2 }, { harnessDriverVersion: 2 },
{ prp: { name: 'other', minimumVersion: 1, maximumVersion: 2 } },
{ prp: { name: 'paperclip.runner', minimumVersion: 2, maximumVersion: 2 } },
{ prp: { name: 'paperclip.runner', minimumVersion: 1, maximumVersion: 1 } },
{ durableSessionCapabilities: ['unlimited_runtime'] }, { prpTransportModes: ['dial_ws_loopback'] },
]) assert.throws(() => verifyReleaseDaemonMetadata({ ...daemonMetadata(), ...mismatch }));
});
function releaseData() {
const files = new Map(), platforms = {};
for (const target of ["darwin-arm64", "darwin-x64", "linux-x64"]) {
const bytes = Buffer.alloc(64);
if (target === "linux-x64") { bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(62, 18); }
else { bytes.writeUInt32LE(0xfeedfacf, 0); bytes.writeUInt32LE(target === "darwin-arm64" ? 0x0100000c : 0x01000007, 4); }
files.set(`bin/${target}/paperclip-runnerd`, bytes);
platforms[target] = { path: `${target}/paperclip-runnerd`, sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}` };
}
const pack = seal(fixture());
files.set("remote-provider-packs/linux-x64/provider-pack.json", Buffer.from(JSON.stringify(pack)));
files.set("bin/release-manifest.json", Buffer.from(JSON.stringify({ schema: "paperclip.runner.release-binaries.v1", sourceRevision: revision,
platforms, remoteProviderPack: { target: "linux-x64", sourceRevision: revision, digest: pack.digest } })));
files.set("assembly-receipt.json", Buffer.from(JSON.stringify({ schema: "paperclip.runner.release-assembly.v1", sourceRevision: revision,
sourcePins: { profiles: `sha256:${createHash("sha256").update(readFileSync(new URL('../acpx-profiles.json', import.meta.url))).digest('hex')}`,
distributions: `sha256:${createHash("sha256").update(readFileSync(new URL('../cursor-distributions.json', import.meta.url))).digest('hex')}` },
providerImage: { sourceRevision: revision, platform: "linux/amd64", imageId: digest },
linuxDaemonImage: { sourceRevision: revision, platform: "linux/amd64", imageId: digest },
daemonMetadata: Object.fromEntries(Object.keys(platforms).map(target => [target, daemonMetadata()])),
files: Object.fromEntries([...files].map(([path, bytes]) => [path, `sha256:${createHash("sha256").update(bytes).digest("hex")}`])) })));
return files;
}
test("release transfer preserves only the assembled daemon and actual provider-pack data", () => {
const files = releaseData(), archive = makeRunnerReleaseArchive(files, revision);
const checksum = `sha256:${createHash("sha256").update(archive).digest("hex")}`;
assert.deepEqual(readRunnerReleaseArchive(archive, revision, checksum), files);
assert.throws(() => readRunnerReleaseArchive(archive, "c".repeat(40), checksum), /source mismatch/);
assert.throws(() => readRunnerReleaseArchive(archive, revision, digest), /archive checksum mismatch/);
});
test('the real transfer CLI validates before extraction, restores executable modes and refuses existing output', () => {
const root = mkdtempSync(join(tmpdir(), 'runner-release-transfer-'));
const helper = fileURLToPath(new URL('../../../scripts/release-runner-artifacts.mjs', import.meta.url));
try {
const archive = makeRunnerReleaseArchive(releaseData(), revision), archivePath = join(root, 'assets.tar.gz');
const checksumPath = join(root, 'assets.sha256'), output = join(root, 'output');
writeFileSync(archivePath, archive); writeFileSync(checksumPath, digest);
const invoke = (...args) => spawnSync(process.execPath, [helper, ...args], { encoding: 'utf8' });
let result = invoke('unpack', revision, archivePath, checksumPath, output);
assert.equal(result.status, 1); assert.match(result.stderr, /archive checksum mismatch/);
assert.equal(existsSync(output), false, 'No data can be written before the entire envelope passes');
writeFileSync(checksumPath, `sha256:${createHash('sha256').update(archive).digest('hex')}`);
result = invoke('unpack', revision, archivePath, checksumPath, output);
assert.equal(result.status, 0, result.stderr);
assert.equal(lstatSync(join(output, 'bin/darwin-arm64/paperclip-runnerd')).mode & 0o777, 0o755);
assert.equal(lstatSync(join(output, 'assembly-receipt.json')).mode & 0o777, 0o644);
result = invoke('unpack', revision, archivePath, checksumPath, output);
assert.equal(result.status, 1); assert.match(result.stderr, /new absolute owned directory/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
test('image-sourced Linux daemon recording validates data without executing its ELF on the host', () => {
const root = mkdtempSync(join(tmpdir(), 'runner-image-daemon-record-'));
const helper = fileURLToPath(new URL('../../../scripts/release-runner-artifacts.mjs', import.meta.url));
try {
// This header fixture cannot execute. Successful recording proves this
// transfer leg consumes image-probed metadata rather than host execution.
const binary = join(root, 'runnerd'), metadata = join(root, 'metadata.json'), imagePath = join(root, 'image.json');
const image = `ghcr.io/paperclipai/paperclip@${digest}`;
writeFileSync(binary, releaseData().get('bin/linux-x64/paperclip-runnerd'), { mode: 0o755 });
writeFileSync(metadata, JSON.stringify(daemonMetadata()));
const imageData = { Os: 'linux', Architecture: 'amd64', Id: digest, RepoDigests: [image],
Config: { Labels: { 'org.opencontainers.image.revision': revision }, Env: ['PRIVATE_TOKEN=do-not-export'] } };
writeFileSync(imagePath, JSON.stringify([imageData]));
const output = join(root, 'output');
let result = spawnSync(process.execPath, [helper, 'record-image-binary', revision, binary, metadata, imagePath, output, image], { encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
const receipt = readFileSync(join(output, 'receipt.json'), 'utf8');
assert.equal(JSON.parse(receipt).providerImage.requestedImage, image);
assert.equal(JSON.parse(receipt).sha256, `sha256:${createHash('sha256').update(readFileSync(binary)).digest('hex')}`);
assert.doesNotMatch(receipt, /PRIVATE_TOKEN|do-not-export|Config/);
imageData.Config.Labels['org.opencontainers.image.revision'] = 'c'.repeat(40);
writeFileSync(imagePath, JSON.stringify([imageData]));
const wrong = join(root, 'wrong');
result = spawnSync(process.execPath, [helper, 'record-image-binary', revision, binary, metadata, imagePath, wrong, image], { encoding: 'utf8' });
assert.equal(result.status, 1); assert.equal(existsSync(wrong), false);
} finally { rmSync(root, { recursive: true, force: true }); }
});
test("release transfer rejects links, unsafe or duplicate paths, truncation and permissions before extraction", () => {
const archive = makeRunnerReleaseArchive(releaseData(), revision);
for (const corrupt of [
tar => { tar.fill(0, 0, 100); tar.write("../paperclip-runnerd", 0); },
tar => { tar[156] = 50; },
tar => { tar.subarray(0, 100).copy(tar, 1024); },
tar => { tar.write("00004755\0", 100, 8); },
]) {
const tar = gunzipSync(archive); corrupt(tar);
const headerStart = tar[1024] === tar[0] && tar.subarray(1024, 1124).equals(tar.subarray(0, 100)) ? 1024 : 0;
const header = tar.subarray(headerStart, headerStart + 512); header.fill(32, 148, 156);
const sum = [...header].reduce((sum, byte) => sum + byte, 0);
header.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
const bytes = gzipSync(tar), hash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
assert.throws(() => readRunnerReleaseArchive(bytes, revision, hash), /Unsafe|forbidden|permissions/);
}
const truncated = archive.subarray(0, archive.length - 4);
assert.throws(() => readRunnerReleaseArchive(truncated, revision, `sha256:${createHash("sha256").update(truncated).digest("hex")}`));
});
test("release data rejects rehashed wrong targets and provider-image provenance", () => {
const files = releaseData();
files.set("bin/darwin-arm64/paperclip-runnerd", files.get("bin/linux-x64/paperclip-runnerd"));
const receipt = JSON.parse(files.get("assembly-receipt.json"));
receipt.files["bin/darwin-arm64/paperclip-runnerd"] = `sha256:${createHash("sha256").update(files.get("bin/darwin-arm64/paperclip-runnerd")).digest("hex")}`;
files.set("assembly-receipt.json", Buffer.from(JSON.stringify(receipt)));
assert.throws(() => verifyRunnerReleaseData(files, revision), /architecture mismatch/);
for (const changes of [{ sourceRevision: "c".repeat(40) }, { requestedImage: `ghcr.io/paperclipai/paperclip@${digest}`, repoDigests: [] }]) {
const source = releaseData(), receipt = JSON.parse(source.get("assembly-receipt.json"));
Object.assign(receipt.providerImage, changes); source.set("assembly-receipt.json", Buffer.from(JSON.stringify(receipt)));
assert.throws(() => verifyRunnerReleaseData(source, revision), /image (?:source|digest) mismatch/);
}
const incompatible = releaseData(), incompatibleReceipt = JSON.parse(incompatible.get('assembly-receipt.json'));
incompatibleReceipt.daemonMetadata['linux-x64'].nativeExecutionVersion = 999;
incompatible.set('assembly-receipt.json', Buffer.from(JSON.stringify(incompatibleReceipt)));
assert.throws(() => verifyRunnerReleaseData(incompatible, revision), /Incompatible daemon nativeExecutionVersion/);
const mixed = releaseData(), mixedReceipt = JSON.parse(mixed.get('assembly-receipt.json'));
mixedReceipt.linuxDaemonImage.imageId = `sha256:${'c'.repeat(64)}`;
mixed.set('assembly-receipt.json', Buffer.from(JSON.stringify(mixedReceipt)));
assert.throws(() => verifyRunnerReleaseData(mixed, revision), /same verified image/);
});
test("release transfer binds qualification JSON snapshots to the selected source", () => {
const files = releaseData();
assert.throws(() => verifyRunnerReleaseData(files, revision, { profiles: Buffer.from('{}'), distributions: Buffer.from('{}') }), /source profile snapshot mismatch/);
for (const snapshots of [[{}, distributions], [profiles, {}], [null, null]]) {
assert.throws(() => verifyReleaseProviderPack(seal(fixture()), revision, ...snapshots), /Invalid release qualification snapshots/);
}
const noCursorVersion = structuredClone(profiles); delete noCursorVersion.profiles.cursor.agentServerVersion;
const malformedClosure = structuredClone(distributions); malformedClosure.platforms['linux-x64'].closureSha256 = 'unknown';
for (const snapshots of [[noCursorVersion, distributions], [profiles, malformedClosure]]) {
assert.throws(() => verifyReleaseProviderPack(seal(fixture()), revision, ...snapshots), /Invalid release qualification snapshots/);
}
const altered = structuredClone(profiles); altered.profiles.claude.commandDigest = digest;
assert.throws(() => verifyReleaseProviderPack(seal(fixture()), revision, altered, distributions), /ACPX profiles/);
assert.equal(verifyReleaseProviderPack(seal(fixture()), revision, profiles, distributions).payload.runnerSourceRevision, revision);
});
test("accepts a pack whose source and profiles match the assembled release", () => {
const manifest = seal(fixture()); assert.equal(verifyReleaseProviderPack(manifest, revision), manifest);
});
@@ -1,7 +1,10 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, grokConsumerDockerArgs } from '../grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, grokConsumerDockerArgs, installedCodexProbeSource } from '../grok-public-install-sandbox.mjs';
const paths = { assets: '/private/staging/assets', consumer: '/private/staging/consumer', cache: '/private/staging/cache', uid: 1001, gid: 1001 };
const values = (args, flag) => args.flatMap((value, index) => value === flag ? [args[index + 1]] : []);
@@ -50,3 +53,25 @@ test('verification never elevates PR-controlled provisioning or cleanup on the h
assert.doesNotMatch(source, /\bsudo\b/);
assert.ok(source.includes("const prerequisite = join(root, 'native/grok')"));
});
test('the installed Codex probe exercises the public export and rejects incomplete or mismatched packages', () => {
const root = mkdtempSync(join(tmpdir(), 'installed-codex-probe-'));
try {
const consumer = join(root, 'consumer'); mkdirSync(consumer);
const index = join(consumer, 'index.mjs'), command = join(consumer, 'codex'), probe = join(root, 'probe.mjs');
const executable = version => writeFileSync(command, `#!/bin/sh\n[ "$1" = --version ] || exit 65\nprintf '%s\\n' 'codex-cli ${version}'\n`, { mode: 0o755 });
writeFileSync(probe, installedCodexProbeSource(index, consumer, '0.160.0'));
const run = () => spawnSync(process.execPath, [probe], { encoding: 'utf8', timeout: 10_000, env: { PATH: '/usr/bin:/bin', NODE_PATH: '' } });
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(command)};`);
executable('0.160.0');
let result = run(); assert.equal(result.status, 0, result.stderr);
assert.equal(JSON.parse(result.stdout).pinnedCodexCommandVerified, true);
assert.equal(JSON.parse(result.stdout).providerCalls, 0);
executable('9.9.9'); result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /qualified pin/);
rmSync(command); result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /ENOENT/);
const outside = join(root, 'outside'); writeFileSync(outside, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(outside)};`);
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /inside the installed consumer/);
writeFileSync(index, 'export const unrelated = true;'); result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /must export the pinned Codex resolver/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
@@ -15,6 +15,110 @@ function readWorkflow(name) {
return readFileSync(path.join(repoRoot, ".github/workflows", name), "utf8");
}
function job(workflow, name) {
return workflow.split(`\n ${name}:\n`)[1]?.split(/\n [a-z_]+:\n/)[0] ?? '';
}
function condition(block, needs, github = {}, inputs = {}, isCancelled = false) {
const expression = block.match(/ if: >-\n((?: .+\n)+)/)?.[1]?.trim();
assert.ok(expression, 'Routing must use an explicit condition, including skipped ancestors');
return Function('needs', 'github', 'inputs', 'cancelled', `return (${expression})`)(needs, github, inputs, () => isCancelled);
}
test('release producer mode validates immutable data inputs and adds only the missing native target jobs', () => {
const workflow = readWorkflow('release-verify.yml');
assert.match(workflow, /runner_assets_only:[\s\S]*?default: false/);
for (const name of ['runner_chaos_evals', 'typecheck', 'general_tests', 'serialized_tests', 'runner_workflow_evals', 'verify_paperclip_runner', 'build']) {
// The default source gate keeps all its prior checks. Producer-only mode
// must not create a second broad suite or repeat paid eval authorization.
assert.match(job(workflow, name), /if: \$\{\{ !inputs\.runner_assets_only \}\}/, name);
}
const request = job(workflow, 'runner_release_request');
assert.doesNotMatch(request, /uses: actions\/checkout|secrets\./);
const validation = request.split(' run: |\n')[1];
assert.ok(validation);
const source = 'a'.repeat(40);
for (const [SOURCE_SHA, expected] of [[source, 0], ['master', 1], ['', 1]]) {
const result = spawnSync('bash', ['-c', validation], { env: { ...process.env, SOURCE_SHA }, encoding: 'utf8' });
assert.equal(result.status, expected, result.stderr);
}
for (const name of ['runner_release_binaries', 'runner_release_pack']) {
const producer = job(workflow, name);
assert.match(producer, /needs: runner_release_request/);
assert.match(producer, /contents: read/);
assert.doesNotMatch(producer, /secrets\.|contents: write|id-token: write|packages: write|--push|--password/);
assert.match(producer, /test "\$\(git rev-parse HEAD\)" = "\$SOURCE_SHA"/);
}
const binaries = job(workflow, 'runner_release_binaries');
assert.deepEqual([...binaries.matchAll(/- target: (.+)/g)].map(match => match[1]), ['darwin-arm64', 'darwin-x64']);
assert.match(binaries, /cargo build --release.*--locked/);
assert.match(binaries, /record-binary/);
const pack = job(workflow, 'runner_release_pack');
assert.doesNotMatch(pack, /docker pull|IMAGE_DIGEST/);
assert.match(pack, /--target runner-provider-pack/);
assert.match(pack, /docker cp "\$owner:\$pack_path"/);
assert.match(pack, /daemon_path=\/app\/server\/dist\/vendor\/paperclip-runner\/bin\/paperclip-runnerd/);
assert.match(pack, /--entrypoint "\$daemon_path" "\$image" --build-metadata/);
assert.match(pack, /docker cp "\$owner:\$daemon_path"/);
assert.match(pack, /timeout 30s docker start --attach "\$owner"/);
assert.match(pack, /record-image-binary/);
assert.match(pack, /trap 'docker rm -f "\$owner"/);
const helper = readFileSync(path.join(repoRoot, 'scripts/release-runner-artifacts.mjs'), 'utf8');
assert.match(helper, /stage-release-runner-binaries\.mjs/);
assert.equal((helper.match(/execFileSync\(process\.execPath/g) ?? []).length, 1, 'Only the existing assembler owns staging');
});
test('channel assembly routing preserves skipped promotions and blocks incomplete data before publishers', () => {
const workflow = readWorkflow('release.yml');
const skipped = () => ({ result: 'skipped', outputs: {} });
const base = () => Object.fromEntries(['verify_canary', 'select_nightly', 'select_beta', 'verify_beta_candidate', 'preflight_stable', 'verify_stable'].map(name => [name, skipped()]));
const pin = job(workflow, 'pin_runner_release_source');
assert.equal(condition(pin, base(), { event_name: 'workflow_dispatch' }), false);
for (const [name, extra, event] of [
['verify_canary', {}, 'push'], ['select_nightly', { proceed: 'true' }, 'schedule'],
['select_beta', { mode: 'promote' }, 'workflow_dispatch'],
]) {
const needs = base(); needs[name] = { result: 'success', outputs: extra };
assert.equal(condition(pin, needs, { event_name: event }), true, `${name} with unrelated skipped gates`);
assert.equal(condition(pin, needs, { event_name: event }, {}, true), false);
}
const stable = base(); stable.preflight_stable.result = 'success'; stable.verify_stable.result = 'success';
assert.equal(condition(pin, stable, { event_name: 'workflow_dispatch' }), true);
stable.verify_stable.result = 'failure';
assert.equal(condition(pin, stable, { event_name: 'workflow_dispatch' }), false);
assert.match(pin, /git show "\$source_sha:scripts\/release\.sh" \| grep -F 'PAPERCLIP_RELEASE_RUNNER_ASSETS'/);
assert.match(pin, /git cat-file -e "\$source_sha:scripts\/release-runner-artifacts\.mjs"/);
const ready = { verify_canary: { result: 'success' },
select_nightly: { result: 'success', outputs: { proceed: 'true' } }, smoke_nightly: { result: 'success' },
select_beta: { result: 'success', outputs: { mode: 'promote' } }, verify_beta_candidate: skipped(),
verify_stable: { result: 'success' },
pin_runner_release_source: { result: 'success', outputs: { required: 'true' } }, release_runner_assets: { result: 'success' } };
for (const [name, github, inputs] of [
['publish_canary', { event_name: 'push' }, {}], ['publish_nightly', { event_name: 'schedule' }, {}],
['publish_beta', { event_name: 'workflow_dispatch' }, {}],
['preview_stable', { event_name: 'workflow_dispatch' }, { channel: 'stable', dry_run: true }],
['publish_stable', { event_name: 'workflow_dispatch' }, { channel: 'stable', dry_run: false }],
]) {
const block = job(workflow, name);
assert.equal(condition(block, ready, github, inputs), true, name);
for (const result of ['failure', 'cancelled', 'skipped']) {
assert.equal(condition(block, { ...ready, release_runner_assets: { result } }, github, inputs), false, `${name}/${result}`);
}
const older = { ...ready, pin_runner_release_source: { result: 'success', outputs: { required: 'false' } }, release_runner_assets: skipped() };
assert.equal(condition(block, older, github, inputs), true, `${name} older promoted contract`);
assert.match(block, /working-directory: source/);
const validation = block.match(/name: Validate runner data against[\s\S]*?(?=\n - name:)/)?.[0];
assert.ok(validation);
assert.match(validation, /node "\$GITHUB_WORKSPACE\/trusted\/scripts\/release-runner-artifacts\.mjs" unpack/);
assert.match(validation, /"\$RUNNER_TEMP\/runner-release-assets" "\$GITHUB_WORKSPACE\/source"/);
assert.doesNotMatch(validation, /node scripts\//, 'Credentialed publishers may consume only trusted transfer tooling');
}
const release = readFileSync(path.join(repoRoot, 'scripts/release.sh'), 'utf8');
assert.ok(release.indexOf('pnpm build\n# Production publication') < release.indexOf('PAPERCLIP_RELEASE_RUNNER_ASSETS/bin'));
assert.ok(release.indexOf('PAPERCLIP_RELEASE_RUNNER_ASSETS/bin') < release.indexOf('node "$REPO_ROOT/scripts/build-standalone-public-packages.mjs"'));
assert.match(release, /elif \[ "\$dry_run" = false \]; then\n\s+release_fail/);
});
test("chaos verification isolates callers that verify the same source commit", () => {
const chaosWorkflow = readWorkflow("runner-chaos-evals.yml");
const group = chaosWorkflow.match(/^ group: (.+)$/m)?.[1];
@@ -45,7 +149,10 @@ test("canary reuses exact-source proof while stable keeps full verification", ()
assert.match(canary, /SOURCE_SHA: \$\{\{ github\.sha \}\}/);
assert.match(canary, /run: node scripts\/cloud-source-verification\.mjs "\$SOURCE_SHA"/);
assert.doesNotMatch(canary, /release-verify\.yml|continue-on-error|always\(\)/);
assert.match(releaseWorkflow, /publish_canary:\n\s+if: github\.event_name == 'push'\n\s+needs: verify_canary/);
const publishCanary = job(releaseWorkflow, 'publish_canary');
assert.match(publishCanary, /needs: \[verify_canary, pin_runner_release_source, release_runner_assets\]/);
assert.match(publishCanary, /needs\.verify_canary\.result == 'success'/);
assert.match(publishCanary, /needs\.release_runner_assets\.result == 'success'/);
// The stable lane is gated on the stable channel since the nightly lane
// was added; a `needs:` line (for example a preflight job) may sit between
// the gate and the delegation.
+26
View File
@@ -7,6 +7,32 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [
'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts',
];
// Execute this in the existing offline public consumer, never in the checkout.
// The public index and returned command must both come from the installed graph.
export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersion) {
return `
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { accessSync, constants, realpathSync, statSync } from 'node:fs';
import { isAbsolute, relative, sep } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = realpathSync(${JSON.stringify(consumerRoot)});
const contained = path => { const value = relative(root, realpathSync(path)); return value !== '' && value !== '..' && !value.startsWith('..' + sep) && !isAbsolute(value); };
const index = ${JSON.stringify(indexPath)};
assert.ok(contained(index), 'Codex public index must be installed in the consumer');
const { resolvePinnedCodexCommand } = await import(pathToFileURL(index).href);
assert.equal(typeof resolvePinnedCodexCommand, 'function', 'Installed public index must export the pinned Codex resolver');
const command = resolvePinnedCodexCommand();
assert.ok(isAbsolute(command) && contained(command), 'Pinned Codex must resolve inside the installed consumer');
assert.ok(statSync(command).isFile(), 'Pinned Codex must be a regular executable');
accessSync(command, constants.X_OK);
const version = execFileSync(command, ['--version'], { timeout: 30_000, maxBuffer: 128 * 1024, encoding: 'utf8',
env: { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/tmp', NODE_PATH: '' } }).trim();
assert.equal(version, 'codex-cli ' + ${JSON.stringify(expectedVersion)}, 'Installed Codex version must match the qualified pin');
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, pinnedCodexVersion: ${JSON.stringify(expectedVersion)}, providerCalls: 0 }));
`;
}
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMb = 256 }) {
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
throw new Error('Public-install verification requires an unprivileged host user');
+265
View File
@@ -0,0 +1,265 @@
// Data transfer for the existing release assembler. Producer jobs have no
// publishing credentials; consumers never execute binaries from the archive.
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { gzipSync, gunzipSync } from 'node:zlib';
import { runnerBinaryTarget } from '../packages/paperclip-runner/src/live/runner-binary.ts';
import { verifyReleaseProviderPack } from '../packages/paperclip-runner/scripts/release-provider-pack.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const RELEASE_RUNNER_TARGETS = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
const dataPaths = [...RELEASE_RUNNER_TARGETS.map(target => `bin/${target}/paperclip-runnerd`),
'bin/release-manifest.json', 'remote-provider-packs/linux-x64/provider-pack.json'];
const archivePaths = [...dataPaths, 'assembly-receipt.json'];
const maxBinary = 128 * 1024 * 1024;
const maxArchive = 3 * maxBinary + 2 * 1024 * 1024;
const digest = value => `sha256:${createHash('sha256').update(value).digest('hex')}`;
const revision = value => assert.match(value ?? '', /^[a-f0-9]{40}$/, 'Runner artifacts require a full source SHA');
function file(path, limit = maxBinary) {
const stat = lstatSync(path);
assert.ok(stat.isFile() && !stat.isSymbolicLink() && stat.size <= limit, `Unsafe or oversized artifact ${path}`);
return readFileSync(path);
}
function inside(root, path, limit = maxBinary) {
const components = path.split('/');
for (let length = 0; length < components.length; length++) {
const directory = length === 0 ? root : join(root, ...components.slice(0, length));
const stat = lstatSync(directory);
assert.ok(stat.isDirectory() && !stat.isSymbolicLink(), 'Artifact directories must not be symlinks');
}
return file(join(root, path), limit);
}
function destination(root, path) {
assert.ok(archivePaths.includes(path), 'Unknown artifact destination');
const components = path.split('/');
for (let length = 0; length < components.length; length++) {
const directory = length === 0 ? root : join(root, ...components.slice(0, length));
if (!existsSync(directory)) mkdirSync(directory);
const stat = lstatSync(directory);
assert.ok(stat.isDirectory() && !stat.isSymbolicLink(), 'Artifact output directories must not be symlinks');
}
const output = join(root, path);
let existing;
try { existing = lstatSync(output); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
assert.ok(!existing || existing.isFile() && !existing.isSymbolicLink() && existing.nlink === 1,
'Refuse to overwrite an artifact link or non-file');
return output;
}
function newDirectory(path) {
assert.ok(isAbsolute(path) && resolve(path) === path && path !== '/' && !existsSync(path), 'Artifact output must be a new absolute owned directory');
mkdirSync(path, { recursive: true, mode: 0o700 });
}
function writeJson(path, value) { writeFileSync(path, JSON.stringify(value, null, 2) + '\n'); }
function imageProvenance(path, sourceRevision, requestedImage = '') {
const [image] = JSON.parse(file(path, 512 * 1024));
assert.equal(image.Os, 'linux'); assert.equal(image.Architecture, 'amd64');
assert.equal(image.Config?.Labels?.['org.opencontainers.image.revision'], sourceRevision);
assert.match(image.Id, /^sha256:[a-f0-9]{64}$/);
if (requestedImage) {
assert.match(requestedImage, /^ghcr\.io\/paperclipai\/paperclip@sha256:[a-f0-9]{64}$/);
assert.ok(image.RepoDigests?.includes(requestedImage));
}
return { sourceRevision, platform: 'linux/amd64', imageId: image.Id, requestedImage, repoDigests: image.RepoDigests ?? [] };
}
function sourcePins(sourceRoot = repo) {
return { profiles: file(join(sourceRoot, 'packages/paperclip-runner/acpx-profiles.json'), 512 * 1024),
distributions: file(join(sourceRoot, 'packages/paperclip-runner/cursor-distributions.json'), 512 * 1024) };
}
// The unbuilt producer cannot import the compiled eval join. Check its same
// daemon contracts here, including the durable/remote capabilities needed by
// production. Metadata is a compatibility claim, not embedded source proof.
export function verifyReleaseDaemonMetadata(metadata) {
assert.equal(metadata?.schema, 'paperclip-runner/runnerd-build-metadata/v1');
assert.equal(metadata.binaryName, 'paperclip-runnerd', 'Unexpected release daemon name');
assert.equal(metadata.packageName, '@paperclipai/paperclip-runner', 'Unexpected release daemon package');
assert.ok(typeof metadata.packageVersion === 'string' && metadata.packageVersion.trim(), 'Missing daemon package version');
for (const [field, expected] of [['binaryContractVersion', 2], ['nativeExecutionVersion', 1], ['harnessDriverVersion', 1]]) {
assert.equal(metadata[field], expected, `Incompatible daemon ${field}`);
}
assert.equal(metadata.prp?.name, 'paperclip.runner', 'Incompatible daemon protocol family');
assert.ok(Number.isSafeInteger(metadata.prp.minimumVersion) && metadata.prp.minimumVersion === 1 &&
Number.isSafeInteger(metadata.prp.maximumVersion) && metadata.prp.maximumVersion >= 2, 'Incompatible daemon protocol versions');
for (const [field, required] of [
['durableSessionCapabilities', ['unlimited_runtime', 'connection_lease_renewal']],
['prpTransportModes', ['dial_ws_loopback', 'dial_wss', 'listen_ws']],
]) {
assert.ok(Array.isArray(metadata[field]) && required.every(capability => metadata[field].includes(capability)),
`Missing release daemon ${field}`);
}
return metadata;
}
export function verifyRunnerReleaseData(files, sourceRevision, pins = sourcePins()) {
revision(sourceRevision);
assert.deepEqual([...files.keys()].sort(), [...archivePaths].sort(), 'Release archive must contain exactly the known data files');
const receipt = JSON.parse(files.get('assembly-receipt.json'));
assert.equal(receipt.schema, 'paperclip.runner.release-assembly.v1');
assert.equal(receipt.sourceRevision, sourceRevision, 'Assembly source mismatch');
assert.deepEqual(Object.keys(receipt.files ?? {}).sort(), [...dataPaths].sort());
for (const path of dataPaths) assert.equal(digest(files.get(path)), receipt.files[path], `Assembly artifact digest mismatch: ${path}`);
const manifest = JSON.parse(files.get('bin/release-manifest.json'));
assert.equal(manifest.schema, 'paperclip.runner.release-binaries.v1');
assert.equal(manifest.sourceRevision, sourceRevision, 'Daemon source mismatch');
assert.deepEqual(Object.keys(manifest.platforms ?? {}).sort(), [...RELEASE_RUNNER_TARGETS].sort(), 'Missing or duplicate release target');
assert.deepEqual(Object.keys(receipt.daemonMetadata ?? {}).sort(), [...RELEASE_RUNNER_TARGETS].sort(), 'Missing daemon compatibility receipt');
for (const target of RELEASE_RUNNER_TARGETS) {
const path = `bin/${target}/paperclip-runnerd`, artifact = manifest.platforms[target];
assert.equal(artifact.path, `${target}/paperclip-runnerd`, 'Unsafe daemon path');
assert.equal(runnerBinaryTarget(files.get(path)), target, 'Daemon architecture mismatch');
assert.equal(digest(files.get(path)), artifact.sha256, 'Daemon digest mismatch');
verifyReleaseDaemonMetadata(receipt.daemonMetadata[target]);
}
assert.equal(receipt.sourcePins?.profiles, digest(pins.profiles), 'Checked-out source profile snapshot mismatch');
assert.equal(receipt.sourcePins?.distributions, digest(pins.distributions), 'Checked-out source distribution snapshot mismatch');
const pack = verifyReleaseProviderPack(JSON.parse(files.get('remote-provider-packs/linux-x64/provider-pack.json')), sourceRevision,
JSON.parse(pins.profiles), JSON.parse(pins.distributions));
assert.equal(manifest.remoteProviderPack?.target, 'linux-x64');
assert.equal(manifest.remoteProviderPack?.digest, pack.digest, 'Provider pack assembly identity mismatch');
assert.equal(manifest.remoteProviderPack?.sourceRevision, sourceRevision);
assert.equal(receipt.providerImage?.sourceRevision, sourceRevision, 'Provider image source mismatch');
assert.equal(receipt.providerImage?.platform, 'linux/amd64');
assert.match(receipt.providerImage?.imageId ?? '', /^sha256:[a-f0-9]{64}$/);
if (receipt.providerImage.requestedImage) {
assert.match(receipt.providerImage.requestedImage, /^ghcr\.io\/paperclipai\/paperclip@sha256:[a-f0-9]{64}$/);
assert.ok(receipt.providerImage.repoDigests?.includes(receipt.providerImage.requestedImage), 'Provider image digest mismatch');
}
assert.deepEqual(receipt.linuxDaemonImage, receipt.providerImage, 'Linux daemon and provider pack must come from the same verified image');
return receipt;
}
// A deliberately small ustar data envelope: no directories, links, extensions,
// executable hooks or arbitrary paths. Validate the entire envelope before any
// destination file exists, including compressed/uncompressed bounds and hashes.
export function readRunnerReleaseArchive(bytes, sourceRevision, checksum, pins) {
assert.ok(bytes.length <= maxArchive, 'Release archive is oversized');
assert.equal(digest(bytes), checksum, 'Release archive checksum mismatch');
const tar = gunzipSync(bytes, { maxOutputLength: maxArchive });
const files = new Map();
const octal = bytes => {
const value = bytes.toString('ascii').replace(/\0.*$/s, '').trim();
assert.match(value, /^[0-7]+$/, 'Invalid archive numeric field');
return parseInt(value, 8);
};
let offset = 0;
while (offset + 512 <= tar.length) {
const header = tar.subarray(offset, offset + 512);
if (header.every(byte => byte === 0)) {
assert.ok(tar.subarray(offset).every(byte => byte === 0), 'Unexpected archive trailer');
break;
}
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/s, '');
assert.ok(archivePaths.includes(name) && !files.has(name), 'Unsafe, unexpected or duplicate archive path');
assert.ok(header.subarray(345, 500).every(byte => byte === 0), 'Archive prefixes are unsupported');
assert.ok(header[156] === 0 || header[156] === 48, 'Archive links and extensions are forbidden');
assert.ok(header.subarray(157, 257).every(byte => byte === 0), 'Archive link target is forbidden');
const sum = [...header].reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
assert.equal(octal(header.subarray(148, 156)), sum, 'Archive header checksum mismatch');
const size = octal(header.subarray(124, 136));
assert.ok(size <= (name.endsWith('paperclip-runnerd') ? maxBinary : 512 * 1024) && offset + 512 + size <= tar.length, 'Oversized or truncated archive member');
const mode = octal(header.subarray(100, 108));
assert.equal(mode, name.endsWith('paperclip-runnerd') ? 0o755 : 0o644, 'Unexpected archive file permissions');
files.set(name, tar.subarray(offset + 512, offset + 512 + size));
offset += 512 + Math.ceil(size / 512) * 512;
}
assert.ok(offset + 1024 <= tar.length, 'Archive must have a complete end marker');
verifyRunnerReleaseData(files, sourceRevision, pins);
return files;
}
export function makeRunnerReleaseArchive(files, sourceRevision) {
verifyRunnerReleaseData(files, sourceRevision);
const chunks = [];
for (const path of archivePaths) {
const bytes = files.get(path), header = Buffer.alloc(512);
header.write(path);
const field = (offset, width, value) => header.write(value.toString(8).padStart(width - 1, '0') + '\0', offset, width, 'ascii');
field(100, 8, path.endsWith('paperclip-runnerd') ? 0o755 : 0o644);
field(108, 8, 0); field(116, 8, 0); field(124, 12, bytes.length); field(136, 12, 0);
header.fill(32, 148, 156); header[156] = 48;
header.write('ustar\0', 257, 6, 'ascii'); header.write('00', 263, 2, 'ascii');
const sum = [...header].reduce((sum, byte) => sum + byte, 0);
header.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii');
chunks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
}
chunks.push(Buffer.alloc(1024));
return gzipSync(Buffer.concat(chunks));
}
async function main() {
const [mode, sourceRevision, ...args] = process.argv.slice(2);
revision(sourceRevision);
if (mode === 'record-binary') {
const [target, binary, output, ...extra] = args;
assert.ok(RELEASE_RUNNER_TARGETS.includes(target) && !extra.length);
const bytes = file(binary); assert.equal(runnerBinaryTarget(bytes), target);
assert.equal(`${process.platform}-${process.arch}`, target, 'Execute metadata only on the daemon native target');
const metadata = JSON.parse(execFileSync(binary, ['--build-metadata'], { timeout: 15_000, maxBuffer: 128 * 1024 }));
verifyReleaseDaemonMetadata(metadata);
newDirectory(output); writeFileSync(join(output, 'paperclip-runnerd'), bytes, { mode: 0o755 });
writeJson(join(output, 'receipt.json'), { sourceRevision, target, sha256: digest(bytes), metadata });
} else if (mode === 'record-image-binary') {
const [binary, metadataPath, imageMetadata, output, requestedImage = '', ...extra] = args;
assert.equal(extra.length, 0);
const bytes = file(binary); assert.equal(runnerBinaryTarget(bytes), 'linux-x64');
// Metadata was executed in this same image, whose libc matches the daemon.
// The host validates data only and must never execute the extracted ELF.
const metadata = verifyReleaseDaemonMetadata(JSON.parse(file(metadataPath, 128 * 1024)));
const providerImage = imageProvenance(imageMetadata, sourceRevision, requestedImage);
newDirectory(output); writeFileSync(join(output, 'paperclip-runnerd'), bytes, { mode: 0o755 });
writeJson(join(output, 'receipt.json'), { sourceRevision, target: 'linux-x64', sha256: digest(bytes), metadata, providerImage });
} else if (mode === 'record-pack') {
const [packPath, imageMetadata, output, requestedImage = '', ...extra] = args; assert.equal(extra.length, 0);
const bytes = file(packPath, 512 * 1024); verifyReleaseProviderPack(JSON.parse(bytes), sourceRevision);
const providerImage = imageProvenance(imageMetadata, sourceRevision, requestedImage);
newDirectory(output); writeFileSync(join(output, 'provider-pack.json'), bytes);
writeJson(join(output, 'receipt.json'), { sourceRevision, sha256: digest(bytes), providerImage });
} else if (mode === 'assemble') {
const [artifacts, output, ...extra] = args; assert.equal(extra.length, 0);
const platforms = {}, daemonMetadata = {};
let linuxDaemonImage;
assert.deepEqual((await import('node:fs')).readdirSync(artifacts).sort(), [...RELEASE_RUNNER_TARGETS, 'provider-pack'].sort());
for (const target of RELEASE_RUNNER_TARGETS) {
const path = join(artifacts, target, 'paperclip-runnerd'), bytes = inside(artifacts, `${target}/paperclip-runnerd`);
const receipt = JSON.parse(inside(artifacts, `${target}/receipt.json`, 128 * 1024));
assert.equal(receipt.sourceRevision, sourceRevision); assert.equal(receipt.target, target);
daemonMetadata[target] = verifyReleaseDaemonMetadata(receipt.metadata);
if (target === 'linux-x64') linuxDaemonImage = receipt.providerImage;
assert.equal(receipt.sha256, digest(bytes)); assert.equal(runnerBinaryTarget(bytes), target);
platforms[target] = { path: resolve(path), sha256: receipt.sha256 };
}
const packPath = join(artifacts, 'provider-pack/provider-pack.json'), bytes = inside(artifacts, 'provider-pack/provider-pack.json', 512 * 1024);
const packReceipt = JSON.parse(inside(artifacts, 'provider-pack/receipt.json', 128 * 1024));
assert.equal(packReceipt.sourceRevision, sourceRevision); assert.equal(packReceipt.sha256, digest(bytes));
assert.deepEqual(linuxDaemonImage, packReceipt.providerImage, 'Linux daemon and provider pack must come from the same verified image');
verifyReleaseProviderPack(JSON.parse(bytes), sourceRevision);
newDirectory(output);
const manifestPath = join(output, 'assembler-input.json');
writeJson(manifestPath, { sourceRevision, platforms, remoteProviderPack: { path: resolve(packPath), sha256: packReceipt.sha256 } });
// The existing assembler owns source/profile-aware staging. Never replace
// it with a second daemon assembly implementation in this transfer helper.
execFileSync(process.execPath, [join(repo, 'packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs'), manifestPath], { stdio: 'inherit' });
const files = new Map(dataPaths.map(path => [path, file(join(repo, 'packages/paperclip-runner/dist', path), path.endsWith('paperclip-runnerd') ? maxBinary : 512 * 1024)]));
const pins = sourcePins();
const receipt = { schema: 'paperclip.runner.release-assembly.v1', sourceRevision,
sourcePins: { profiles: digest(pins.profiles), distributions: digest(pins.distributions) }, daemonMetadata,
providerImage: packReceipt.providerImage, linuxDaemonImage, files: Object.fromEntries([...files].map(([path, bytes]) => [path, digest(bytes)])) };
files.set('assembly-receipt.json', Buffer.from(JSON.stringify(receipt, null, 2) + '\n'));
const archive = makeRunnerReleaseArchive(files, sourceRevision);
writeFileSync(join(output, 'runner-release-assets.tar.gz'), archive);
writeFileSync(join(output, 'runner-release-assets.tar.gz.sha256'), digest(archive) + '\n');
} else if (mode === 'unpack') {
const [archive, checksum, output, sourceRoot = repo, ...extra] = args; assert.equal(extra.length, 0);
const files = readRunnerReleaseArchive(file(archive, maxArchive), sourceRevision, file(checksum, 256).toString().trim(), sourcePins(sourceRoot));
newDirectory(output);
for (const [path, bytes] of files) {
writeFileSync(destination(output, path), bytes, { mode: path.endsWith('paperclip-runnerd') ? 0o755 : 0o644 });
}
console.log(JSON.stringify({ sourceRevision, targets: RELEASE_RUNNER_TARGETS, archiveSha256: digest(file(archive, maxArchive)), providerCalls: 0 }));
} else throw new Error('Unknown release artifact operation');
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) await main();
+31
View File
@@ -284,6 +284,37 @@ release_info ""
release_info "==> Step 2/7: Building workspace artifacts..."
cd "$REPO_ROOT"
pnpm build
# Production publication requires the data envelope from the separately gated,
# credential-free assembler. The publisher never executes its producer tooling.
# A developer dry-run may still inspect a host-only payload; it does not qualify
# a release. Official dry-runs receive the same complete assets as publication.
if [ -n "${PAPERCLIP_RELEASE_RUNNER_ASSETS:-}" ]; then
node --input-type=module - "$PAPERCLIP_RELEASE_RUNNER_ASSETS" "$CURRENT_SHA" <<'NODE'
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { lstatSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const [assets, source] = process.argv.slice(2);
const receipt = JSON.parse(readFileSync(join(assets, 'assembly-receipt.json')));
assert.equal(receipt.schema, 'paperclip.runner.release-assembly.v1');
assert.equal(receipt.sourceRevision, source, 'Release runner source mismatch');
const paths = ['bin/darwin-arm64/paperclip-runnerd', 'bin/darwin-x64/paperclip-runnerd', 'bin/linux-x64/paperclip-runnerd',
'bin/release-manifest.json', 'remote-provider-packs/linux-x64/provider-pack.json'];
assert.deepEqual(Object.keys(receipt.files).sort(), paths.sort());
for (const path of paths) {
const target = join(assets, path), stat = lstatSync(target);
assert.ok(stat.isFile() && !stat.isSymbolicLink());
assert.equal(`sha256:${createHash('sha256').update(readFileSync(target)).digest('hex')}`, receipt.files[path]);
}
NODE
cp -Rf "$PAPERCLIP_RELEASE_RUNNER_ASSETS/bin" "$REPO_ROOT/packages/paperclip-runner/dist/"
cp -Rf "$PAPERCLIP_RELEASE_RUNNER_ASSETS/remote-provider-packs" "$REPO_ROOT/packages/paperclip-runner/dist/"
cp -Rf "$REPO_ROOT/packages/paperclip-runner/dist/." "$REPO_ROOT/server/dist/vendor/paperclip-runner/"
elif [ "$dry_run" = false ]; then
release_fail "complete release runner assets are required; use the gated release workflow or supply PAPERCLIP_RELEASE_RUNNER_ASSETS."
else
release_info " Host-only dry-run: release runner matrix is not qualified."
fi
node "$REPO_ROOT/scripts/build-standalone-public-packages.mjs"
bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh"
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
+7 -2
View File
@@ -9,7 +9,7 @@ import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { materializePublishManifest, prepareBundledPackage } from './prepare-bundled-package.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, grokConsumerDockerArgs } from './grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, grokConsumerDockerArgs, installedCodexProbeSource } from './grok-public-install-sandbox.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
assert.equal(process.platform, 'linux', 'Run this verification on disposable EC2 Linux, not a developer host');
const root = mkdtempSync(join(tmpdir(), 'paperclip-grok-public-install-'));
@@ -94,6 +94,11 @@ try {
assert.equal(existsSync(prerequisite), false, 'npm must not provision Grok');
const server = join(consumer, 'node_modules/@paperclipai/server');
const installed = join(server, 'dist/vendor/paperclip-runner');
const codexVersion = JSON.parse(readFileSync(join(repo, 'packages/paperclip-runner/acpx-profiles.json'), 'utf8')).profiles.codex.agentRuntimeVersion;
assert.match(codexVersion, /^\d+\.\d+\.\d+$/);
writeFileSync(join(assets, 'codex-probe.mjs'), installedCodexProbeSource(
'/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/index.js', '/consumer', codexVersion), { mode: 0o644 });
isolated(['node', '/packages/codex-probe.mjs']);
assert.ok(existsSync(join(installed, 'providers/grok/launcher.cjs')));
assert.equal(existsSync(join(consumer, 'node_modules/@paperclipai/grok-acp')), false);
assert.equal(existsSync(join(installed, 'providers/grok/bin')), false);
@@ -114,7 +119,7 @@ try {
// Only the positive probe sees this file at the canonical sandbox path.
run(process.execPath, [join(repo, 'packages/paperclip-runner/scripts/provision-grok.mjs'), prerequisite]);
isolated(['node', '/packages/probe.mjs', 'present'], { prerequisite });
console.log(JSON.stringify({ schema: 'paperclip.grok.public-npm-install.v1', sourceRevision, releaseVersion, lifecycleScriptsEnabled: true, lifecycleSentinelVerified: true, lifecycleNetwork: 'none', consumerImage: GROK_PUBLIC_INSTALL_IMAGE, consumerUid, consumerLockPreserved: true, cleanNpmInstall: true, packageCount: needed.size, builtinLauncherPresent: true, separateGrokPackage: false, npmProvisionedBinary: false, missingPrerequisiteRejected: true, provisionedBinaryVerified: true, commandLeaseVerified: true, providerCalls: 0 }));
console.log(JSON.stringify({ schema: 'paperclip.grok.public-npm-install.v1', sourceRevision, releaseVersion, lifecycleScriptsEnabled: true, lifecycleSentinelVerified: true, lifecycleNetwork: 'none', consumerImage: GROK_PUBLIC_INSTALL_IMAGE, consumerUid, consumerLockPreserved: true, cleanNpmInstall: true, packageCount: needed.size, pinnedCodexCommandVerified: true, pinnedCodexVersion: codexVersion, builtinLauncherPresent: true, separateGrokPackage: false, npmProvisionedBinary: false, missingPrerequisiteRejected: true, provisionedBinaryVerified: true, commandLeaseVerified: true, providerCalls: 0 }));
} finally {
rmSync(root, { recursive: true, force: true });
}