feat: qualify macOS Codex runner defaults and release artifacts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 07:48:52 -05:00
1 parent 6ca302426b
commit 7b8d29a856
26 files changed
+1185 -111

No files matched your search

+132
View File
@@ -23,6 +23,11 @@ on:
required: false
default: release-smoke
type: string
qualification_source_sha:
description: Optional immutable source SHA for provider-free three-target package qualification
required: false
default: ""
type: string
workflow_call:
inputs:
paperclip_version:
@@ -36,6 +41,13 @@ on:
required: false
default: release-smoke
type: string
qualification_source_sha:
required: false
default: ""
type: string
permissions:
contents: read
jobs:
# The Docker smoke below can never exercise the background-service leg of
@@ -45,6 +57,7 @@ jobs:
# on the runner VM's systemd and require the installed service to end up
# serving.
smoke_service:
if: inputs.qualification_source_sha == ''
runs-on: ubuntu-latest
timeout-minutes: 30
@@ -98,6 +111,7 @@ jobs:
retention-days: 14
smoke:
if: inputs.qualification_source_sha == ''
runs-on: ubuntu-latest
timeout-minutes: 45
@@ -198,3 +212,121 @@ jobs:
- name: Stop Docker smoke container
if: always()
run: docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true
qualification_daemons:
if: inputs.qualification_source_sha != ''
uses: ./.github/workflows/release-verify.yml
with:
ref: ${{ inputs.qualification_source_sha }}
runner_binaries_only: true
qualification_pack:
if: inputs.qualification_source_sha != ''
needs: qualification_daemons
name: Assemble and verify public Linux packages
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.qualification_source_sha }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind the immutable Linux producer source
env:
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
- name: Build normal workspace outputs and public CLI
run: |
pnpm install --no-frozen-lockfile
pnpm build
bash scripts/build-npm.sh --skip-checks --skip-typecheck
mv cli/package.dev.json cli/package.json
- name: Assemble exact-source daemons before server packing
env:
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
run: |
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs \
assemble-daemons "$RUNNER_TEMP/release-runner-inputs" "$SOURCE_SHA"
cp -R packages/paperclip-runner/dist/bin/. server/dist/vendor/paperclip-runner/bin/
- name: Verify clean public installation and retain the same package bytes
env:
PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR: ${{ runner.temp }}/public-runner-packs
run: node scripts/verify-grok-npm-install.mjs
- name: Locate completed package assembly
id: pack_identity
if: ${{ !cancelled() }}
run: |
if [[ -s "$RUNNER_TEMP/public-runner-packs/manifest.json" ]]; then
echo "available=true" >> "$GITHUB_OUTPUT"
fi
- name: Retain completed package assembly even after a later Linux failure
if: ${{ !cancelled() && steps.pack_identity.outputs.available == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact_name }}-public-packages
path: ${{ runner.temp }}/public-runner-packs/
if-no-files-found: error
retention-days: 14
qualification_mac:
if: ${{ !cancelled() && inputs.qualification_source_sha != '' && needs.qualification_daemons.result == 'success' }}
needs: [qualification_daemons, qualification_pack]
name: Clean installed package (${{ matrix.target }})
runs-on: ${{ matrix.host }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
host: macos-15
- target: darwin-x64
host: macos-15-intel
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.qualification_source_sha }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ inputs.artifact_name }}-public-packages
path: ${{ runner.temp }}/public-runner-packs
- name: Install only the existing verifier tooling
run: pnpm install --ignore-scripts --no-frozen-lockfile
- name: Verify host identity and actual clean npm consumer
env:
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
node scripts/verify-grok-npm-install.mjs --mac-consumer \
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/mac-public-install.json"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: ${{ inputs.artifact_name }}-${{ matrix.target }}
path: ${{ runner.temp }}/mac-public-install.json
if-no-files-found: warn
retention-days: 14
+77
View File
@@ -7,18 +7,28 @@ on:
description: Commit SHA, branch, or tag to verify
required: true
type: string
runner_binaries_only:
description: Build exact-source release daemons without publishing or provider calls
required: false
default: false
type: boolean
outputs:
runner_artifact_prefix:
value: ${{ jobs.runner_binaries.outputs.artifact_prefix }}
# 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_binaries_only }}
name: Pre-release Runner chaos evals
uses: ./.github/workflows/runner-chaos-evals.yml
with:
ref: ${{ inputs.ref }}
typecheck:
if: ${{ !inputs.runner_binaries_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_binaries_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
@@ -205,6 +216,7 @@ jobs:
fi
serialized_tests:
if: ${{ !inputs.runner_binaries_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
@@ -254,6 +266,7 @@ jobs:
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
runner_workflow_evals:
if: ${{ !inputs.runner_binaries_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
@@ -284,6 +297,7 @@ jobs:
run: pnpm test:runner-workflow-evals
verify_paperclip_runner:
if: ${{ !inputs.runner_binaries_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
@@ -412,6 +426,7 @@ jobs:
run: pnpm --filter @paperclipai/paperclip-runner build:rust
build:
if: ${{ !inputs.runner_binaries_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
@@ -440,3 +455,65 @@ jobs:
- name: Build
run: pnpm build
runner_binaries:
if: inputs.runner_binaries_only
name: Release daemon (${{ matrix.target }})
runs-on: ${{ matrix.host }}
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- target: linux-x64
host: ubuntu-latest
- target: darwin-arm64
host: macos-15
- target: darwin-x64
host: macos-15-intel
outputs:
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
with:
version: 9.15.4
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
- name: Bind immutable producer source and target
id: identity
env:
SOURCE_SHA: ${{ inputs.ref }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
echo "artifact_prefix=runner-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
- name: Install dependencies and build production TypeScript contract
run: |
pnpm install --no-frozen-lockfile
pnpm --filter @paperclipai/paperclip-runner build:typescript
- name: Build the host daemon with the pinned toolchain
working-directory: packages/paperclip-runner
run: pnpm build:binary
- name: Record actual daemon metadata and immutable bytes
env:
SOURCE_SHA: ${{ inputs.ref }}
TARGET: ${{ matrix.target }}
run: |
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \
packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }}
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 14
+112 -8
View File
@@ -336,9 +336,54 @@ jobs:
SOURCE_SHA: ${{ github.sha }}
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
release_runner_source:
# Old promotions retain their already-shipped packaging contract. New
# sources opt into mandatory three-target assembly in their release.sh.
needs: [verify_canary, select_nightly, select_beta, preflight_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.preflight_stable.result == 'success')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
sha: ${{ steps.source.outputs.sha }}
required: ${{ steps.source.outputs.required }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
persist-credentials: false
- name: Read the exact source packaging contract
id: source
env:
SOURCE_SHA: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
echo "sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
source_script="$(git show "$SOURCE_SHA:scripts/release.sh")" || exit 1
if printf '%s\n' "$source_script" | grep -F 'assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR"' > /dev/null; then
echo "required=true" >> "$GITHUB_OUTPUT"
else
echo "required=false" >> "$GITHUB_OUTPUT"
fi
release_runner_binaries:
needs: release_runner_source
if: needs.release_runner_source.outputs.required == 'true'
uses: ./.github/workflows/release-verify.yml
with:
ref: ${{ needs.release_runner_source.outputs.sha }}
runner_binaries_only: true
publish_canary:
if: github.event_name == 'push'
needs: verify_canary
if: ${{ !cancelled() && github.event_name == 'push' && needs.verify_canary.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
needs: [verify_canary, release_runner_source, release_runner_binaries]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-canary
@@ -356,6 +401,17 @@ jobs:
with:
fetch-depth: 0
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -587,12 +643,14 @@ jobs:
artifact_name: nightly-release-smoke
publish_nightly:
needs: [select_nightly, smoke_nightly]
needs: [select_nightly, smoke_nightly, release_runner_source, release_runner_binaries]
# 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.select_nightly.outputs.proceed == 'true' &&
needs.release_runner_source.result == 'success' &&
(needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') &&
(needs.smoke_nightly.result == 'success' ||
(needs.smoke_nightly.result == 'skipped' && github.event_name == 'workflow_dispatch' && inputs.dry_run))
runs-on: ubuntu-latest
@@ -618,6 +676,17 @@ jobs:
ref: ${{ needs.select_nightly.outputs.sha }}
fetch-depth: 0
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -855,10 +924,12 @@ jobs:
ref: ${{ needs.select_beta.outputs.sha }}
publish_beta:
needs: [select_beta, verify_beta_candidate]
needs: [select_beta, verify_beta_candidate, release_runner_source, release_runner_binaries]
if: >-
!cancelled() &&
needs.select_beta.result == 'success' &&
needs.release_runner_source.result == 'success' &&
(needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') &&
(needs.verify_beta_candidate.result == 'success' ||
(needs.verify_beta_candidate.result == 'skipped' && needs.select_beta.outputs.mode == 'promote'))
runs-on: ubuntu-latest
@@ -884,6 +955,17 @@ jobs:
ref: ${{ needs.select_beta.outputs.sha }}
fetch-depth: 0
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -1240,8 +1322,8 @@ 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.preflight_stable.result == 'success' && needs.verify_stable.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
needs: [preflight_stable, verify_stable, release_runner_source, release_runner_binaries]
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
@@ -1254,6 +1336,17 @@ jobs:
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -1282,8 +1375,8 @@ 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.preflight_stable.result == 'success' && needs.verify_stable.result == 'success' && needs.release_runner_source.result == 'success' && (needs.release_runner_source.outputs.required == 'false' || needs.release_runner_binaries.result == 'success') }}
needs: [preflight_stable, verify_stable, release_runner_source, release_runner_binaries]
runs-on: ubuntu-latest
timeout-minutes: 150
environment: npm-stable
@@ -1301,6 +1394,17 @@ jobs:
fetch-depth: 0
ref: ${{ needs.preflight_stable.outputs.sha }}
- name: Download exact-source release daemon receipts
if: needs.release_runner_source.outputs.required == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
+62 -1
View File
@@ -1,6 +1,67 @@
# Runner defaults: staged delivery
Updated 2026-10-09.
Updated 2026-10-12.
## Current amendment: include macOS in this PR
The user requires macOS support in the same Codex-default PR. The earlier
Linux-only scope and macOS deferral below are superseded. Qualify Linux x64,
macOS arm64 and macOS x64 through the shipped package path. Windows and Linux
ARM keep their legacy defaults. Existing agents retain recorded execution.
The user explicitly invoked `dotta:paperclip-test-drive`. Real task/follow-up
acceptance is required on this Apple Silicon Mac. Intel builds must be attempted;
an attended Intel provider journey is not required. Reuse the healthy test drive
and the existing Product E2E helpers rather than create another test system.
Keep the current PR and branch: [#15683](https://github.com/paperclipai/paperclip/pull/15683),
`codex/codex-runner-default`. The last pushed source is
`6ca302426b768791b499d2d563bbca4276f2c908`. Its prior CI has 52 passing checks,
four conditional skips, fresh 5/5 review and eight resolved threads. These do
not qualify the new macOS changes. Current master has real merge conflicts;
integrate the fetched master after checkpointing the owned macOS changes.
The [test-drive server](http://127.0.0.1:3125/COD/agents/all) is running through
the normal dev supervisor at the pushed source. Health and bootstrap are ready;
the actual listener belongs to this worktree. The preserved Codex QA agent is
idle on `codex_local`, with its saved unrelated title edit intact. Dependencies
and ignored configuration had been cleared, so startup restored them and the
normal CLI signing keys while retaining the database and encrypted account key.
The existing cached development daemon is from a different Rust tree. It avoids
a local Rust build and is not macOS qualification evidence for this candidate.
Implementation ownership:
- Packaging worker: reuse the existing daemon assembler, release verification
and release-smoke workflows to produce source-bound Linux and both macOS
artifacts. Pack the normal npm graph, then test those same tarballs on actual
Mac hosts. Retain the Linux and provider-pack controls. Never bundle Codex
native payloads; use the installed official npm dependency and real protocol
compatibility. Compile Rust and Docker only on remote hosts.
- Selection worker: extend the shared availability contract and existing
creation, onboarding, discovery, approval, import and UI fixtures. Preserve
explicit legacy choices, external overrides and existing execution.
- Runtime worker: verify the existing Mac native launch, account isolation,
file delivery and continuity seams; fix only actual gaps. Keep the Linux-only
sandboxed workspace-command capability gated on unsupported platforms.
- Lead: integrate, run targeted and required checks, verify useful Mac native
task/follow-up artifacts and ordinary editing, refresh the test-drive on the
verified revision, and update the same PR and evidence record.
Reuse existing evaluations and assertions. Provider-free packaging and protocol
checks do not substitute for authenticated task completion. Preserve the $250
ceiling and historical unknown/cleanup holds; reconcile only documented
unstarted future reservations before any new provider turns. No additional
paid Linux campaign is authorized by this amendment. Embedded-browser-only
login, no automatic agent migration, and no merge or production deployment
remain constraints.
Next action: integrate the macOS packaging and policy changes, reconcile the
confirmed master conflicts, freeze the source, and run remote qualification for
the Mac artifacts. Native Apple Silicon live acceptance, new-head CI/review, managed
staging execution, code-owner approval and actual post-merge Cloud composition
remain unverified. Prior staging image/preflight evidence is retained at
[37962635959](https://github.com/paperclipai/paperclip/actions/runs/37962635959)
and [37965658123](https://github.com/paperclipai/paperclip-cloud/actions/runs/37965658123).
## Outcome and ownership
@@ -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, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -9,6 +9,7 @@ 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 { existingReleaseProviderPack } from "./stage-release-runner-binaries.mjs";
const revision = "a".repeat(40);
const digest = "sha256:" + "b".repeat(64);
@@ -47,7 +48,7 @@ test("rejects omitted Cursor, conflicting legacy inventory, stale sources and fo
});
test("release assembly rejects rehashed stale or missing packs before staging any files", () => {
const root = mkdtempSync(join(tmpdir(), "release-provider-pack-"));
const root = realpathSync(mkdtempSync(join(tmpdir(), "release-provider-pack-")));
const source = dirname(dirname(fileURLToPath(import.meta.url)));
try {
// Run the actual assembler in an isolated layout so it cannot overwrite built artifacts.
@@ -77,3 +78,19 @@ test("release assembly rejects rehashed stale or missing packs before staging an
}
} finally { rmSync(root, { recursive: true, force: true }); }
});
test("daemon-only assembly retains an existing same-source Linux provider-pack identity", () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "release-provider-retained-")));
try {
assert.equal(existingReleaseProviderPack(root, revision), undefined);
const file = join(root, "dist/remote-provider-packs/linux-x64/provider-pack.json");
mkdirSync(dirname(file), { recursive: true });
const pack = seal(fixture()); writeFileSync(file, JSON.stringify(pack));
assert.deepEqual(existingReleaseProviderPack(root, revision), pack);
assert.throws(() => existingReleaseProviderPack(root, "c".repeat(40)), /release source/);
const wrongTarget = fixture(); wrongTarget.target = { platform: "darwin", architecture: "arm64" };
writeFileSync(file, JSON.stringify(seal(wrongTarget)));
assert.throws(() => existingReleaseProviderPack(root, revision), /Linux x64 target/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
@@ -3,53 +3,133 @@
// Manifest: {sourceRevision, platforms: {"darwin-arm64": {path, sha256}, ...}, remoteProviderPack: {path, sha256}}.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { chmodSync, copyFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { runnerBinaryTarget } from "../src/live/runner-binary.ts";
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const required = ["darwin-arm64", "darwin-x64", "linux-x64"];
if (process.argv.length !== 3) throw Error("Usage: stage-release-runner-binaries.mjs MANIFEST.json");
const manifest = JSON.parse(readFileSync(process.argv[2], "utf8"));
if (!/^[a-f0-9]{40}$/.test(manifest.sourceRevision) || !manifest.platforms || Object.keys(manifest.platforms).sort().join() !== [...required].sort().join()) throw Error("Release daemon manifest must bind one source and all three supported targets");
const verified = required.map(target => {
const artifact = manifest.platforms[target];
if (!artifact || !isAbsolute(artifact.path) || !/^sha256:[a-f0-9]{64}$/.test(artifact.sha256)) throw Error(`Invalid ${target} release artifact`);
const bytes = readFileSync(artifact.path), sha256 = "sha256:" + createHash("sha256").update(bytes).digest("hex");
if (sha256 !== artifact.sha256 || runnerBinaryTarget(bytes) !== target) throw Error(`Release daemon ${target} digest or architecture mismatch`);
return { target, source: artifact.path, sha256 };
});
if (!manifest.remoteProviderPack) throw Error("Release assembly requires the matching Linux remote provider pack");
const entry = manifest.remoteProviderPack;
if (!isAbsolute(entry.path) || !/^sha256:[a-f0-9]{64}$/.test(entry.sha256)) throw Error("Invalid remote provider-pack release artifact");
const packBytes = readFileSync(entry.path);
if (packBytes.length > 512 * 1024 || "sha256:" + createHash("sha256").update(packBytes).digest("hex") !== entry.sha256) throw Error("Remote provider-pack release manifest digest mismatch");
const packIdentity = verifyReleaseProviderPack(JSON.parse(packBytes), manifest.sourceRevision);
const platforms = {};
for (const artifact of verified) {
const relative = `${artifact.target}/paperclip-runnerd`, destination = join(root, "dist", "bin", relative);
mkdirSync(dirname(destination), { recursive: true }); copyFileSync(artifact.source, destination); chmodSync(destination, 0o755);
// Copied Mach-O inodes need a fresh ad-hoc signature on macOS. Record the
// resulting packaged bytes, as well as the verified build artifact identity.
if (process.platform === "darwin" && artifact.target.startsWith("darwin-")) {
execFileSync("codesign", ["--force", "--sign", "-", destination], { stdio: "pipe" });
}
const sha256 = "sha256:" + createHash("sha256").update(readFileSync(destination)).digest("hex");
platforms[artifact.target] = { path: relative, sha256, sourceSha256: artifact.sha256 };
export const RELEASE_RUNNER_TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-x64"]);
const required = RELEASE_RUNNER_TARGETS;
const digest = bytes => "sha256:" + createHash("sha256").update(bytes).digest("hex");
function regularFile(path, maximumSize) {
const stat = lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size < 1 || stat.size > maximumSize
|| realpathSync(path) !== resolve(path)) throw Error("Release artifact must be a bounded canonical regular file");
return readFileSync(path);
}
{
const destination = join(root, "dist", "remote-provider-packs", "linux-x64", "provider-pack.json");
mkdirSync(dirname(destination), { recursive: true });
// Replace the inode so repeat assembly can preserve a read-only manifest.
const staged = `${destination}.staging-${process.pid}`;
try {
writeFileSync(staged, packBytes, { mode: 0o444, flag: "wx" });
renameSync(staged, destination);
} finally {
rmSync(staged, { force: true });
}
// Production TypeScript contracts are built by the existing producer step.
// Foreign artifacts are data: the assembler never executes another target.
async function metadataContract() {
const { PAPERCLIP_RUNNER_BUILD_METADATA } = await import("../dist/evals/build-metadata.js");
const { parsePaperclipRunnerdBuildMetadata } = await import("../dist/evals/runnerd-artifact.js");
return metadata => verifyReleaseRunnerMetadata(parsePaperclipRunnerdBuildMetadata(metadata), PAPERCLIP_RUNNER_BUILD_METADATA);
}
writeFileSync(join(root, "dist", "bin", "release-manifest.json"), JSON.stringify({schema:"paperclip.runner.release-binaries.v1",sourceRevision:manifest.sourceRevision,platforms,
remoteProviderPack: { target: "linux-x64", digest: packIdentity.digest, sourceRevision: packIdentity.payload.runnerSourceRevision }},null,2)+"\n");
console.log("Staged verified release daemons for " + required.join(", "));
export function verifyReleaseRunnerMetadata(metadata, expected) {
if (metadata.binaryContractVersion !== expected.contracts.runnerdArtifact
|| metadata.nativeExecutionVersion !== expected.contracts.nativeExecution
|| metadata.harnessDriverVersion !== expected.contracts.harnessDriver
|| metadata.packageName !== expected.package.name || metadata.packageVersion !== expected.package.version
|| metadata.prp?.name !== expected.prp.name || metadata.prp.minimumVersion > expected.prp.maximumVersion
|| metadata.prp.maximumVersion < expected.prp.minimumVersion) throw Error("Release daemon build metadata is incompatible with the production contract");
return metadata;
}
export function releaseDaemonManifest(artifactDirectory, sourceRevision, verifyMetadata) {
if (!/^[a-f0-9]{40}$/.test(sourceRevision)) throw Error("Release daemons require an immutable source revision");
if (readdirSync(artifactDirectory).sort().join() !== [...required].sort().join()) throw Error("Release daemons require exactly the three supported target directories");
const platforms = {};
for (const target of required) {
const directory = join(resolve(artifactDirectory), target);
if (readdirSync(directory).sort().join() !== "paperclip-runnerd,receipt.json") throw Error("Release target must contain only its daemon and receipt");
const receipt = JSON.parse(regularFile(join(directory, "receipt.json"), 64 * 1024));
const path = join(directory, "paperclip-runnerd"), bytes = regularFile(path, 256 * 1024 * 1024);
if (receipt.schema !== "paperclip.runner.release-artifact.v1" || receipt.sourceRevision !== sourceRevision
|| receipt.target !== target || receipt.sha256 !== digest(bytes) || runnerBinaryTarget(bytes) !== target) {
throw Error(`Release daemon ${target} source, digest or architecture mismatch`);
}
verifyMetadata(receipt.buildMetadata);
platforms[target] = { path, sha256: receipt.sha256, buildMetadata: receipt.buildMetadata };
}
return { sourceRevision, platforms };
}
export function existingReleaseProviderPack(packageRoot, sourceRevision) {
const file = join(packageRoot, "dist/remote-provider-packs/linux-x64/provider-pack.json");
if (!existsSync(file)) return undefined;
return verifyReleaseProviderPack(JSON.parse(regularFile(file, 512 * 1024)), sourceRevision);
}
async function main() {
if (process.argv[2] === "record") {
const [, , , sourceRevision, target, source, output] = process.argv;
if (process.argv.length !== 7 || !/^[a-f0-9]{40}$/.test(sourceRevision) || !required.includes(target)
|| target !== `${process.platform}-${process.arch}`) throw Error("Record requires the exact source and actual producer host target");
const bytes = regularFile(resolve(source), 256 * 1024 * 1024);
if (runnerBinaryTarget(bytes) !== target) throw Error("Built daemon architecture does not match its producer");
const buildMetadata = JSON.parse(execFileSync(resolve(source), ["--build-metadata"], {
timeout: 15_000, maxBuffer: 64 * 1024, encoding: "utf8",
}));
(await metadataContract())(buildMetadata);
mkdirSync(output, { recursive: true });
copyFileSync(source, join(output, "paperclip-runnerd"));
writeFileSync(join(output, "receipt.json"), JSON.stringify({ schema: "paperclip.runner.release-artifact.v1",
sourceRevision, target, sha256: digest(bytes), buildMetadata }, null, 2) + "\n");
return;
}
const daemonOnly = process.argv[2] === "assemble-daemons";
if (daemonOnly ? process.argv.length !== 5 : process.argv.length !== 3) throw Error("Usage: stage-release-runner-binaries.mjs MANIFEST.json | assemble-daemons ARTIFACT_DIRECTORY SOURCE_SHA");
const manifest = daemonOnly
? releaseDaemonManifest(process.argv[3], process.argv[4], await metadataContract())
: JSON.parse(regularFile(resolve(process.argv[2]), 512 * 1024));
if (!/^[a-f0-9]{40}$/.test(manifest.sourceRevision) || !manifest.platforms || Object.keys(manifest.platforms).sort().join() !== [...required].sort().join()) throw Error("Release daemon manifest must bind one source and all three supported targets");
const verified = required.map(target => {
const artifact = manifest.platforms[target];
if (!artifact || !isAbsolute(artifact.path) || !/^sha256:[a-f0-9]{64}$/.test(artifact.sha256)) throw Error(`Invalid ${target} release artifact`);
const bytes = regularFile(artifact.path, 256 * 1024 * 1024), sha256 = "sha256:" + createHash("sha256").update(bytes).digest("hex");
if (sha256 !== artifact.sha256 || runnerBinaryTarget(bytes) !== target) throw Error(`Release daemon ${target} digest or architecture mismatch`);
return { target, source: artifact.path, sha256 };
});
if (!manifest.remoteProviderPack && !daemonOnly) throw Error("Release assembly requires the matching Linux remote provider pack");
let packBytes, packIdentity;
if (manifest.remoteProviderPack) {
const entry = manifest.remoteProviderPack;
if (!isAbsolute(entry.path) || !/^sha256:[a-f0-9]{64}$/.test(entry.sha256)) throw Error("Invalid remote provider-pack release artifact");
packBytes = regularFile(entry.path, 512 * 1024);
if (packBytes.length > 512 * 1024 || "sha256:" + createHash("sha256").update(packBytes).digest("hex") !== entry.sha256) throw Error("Remote provider-pack release manifest digest mismatch");
packIdentity = verifyReleaseProviderPack(JSON.parse(packBytes), manifest.sourceRevision);
} else if (daemonOnly) {
// Preserve a qualified same-source Linux pack already materialized by the
// normal producer. It never becomes a Mac provider-pack qualification.
packIdentity = existingReleaseProviderPack(root, manifest.sourceRevision);
}
const platforms = {};
for (const artifact of verified) {
const relative = `${artifact.target}/paperclip-runnerd`, destination = join(root, "dist", "bin", relative);
mkdirSync(dirname(destination), { recursive: true }); copyFileSync(artifact.source, destination); chmodSync(destination, 0o755);
// Copied Mach-O inodes need a fresh ad-hoc signature on macOS. Record the
// resulting packaged bytes, as well as the verified build artifact identity.
if (process.platform === "darwin" && artifact.target.startsWith("darwin-")) {
execFileSync("codesign", ["--force", "--sign", "-", destination], { stdio: "pipe", timeout: 30_000 });
}
const sha256 = "sha256:" + createHash("sha256").update(readFileSync(destination)).digest("hex");
platforms[artifact.target] = { path: relative, sha256, sourceSha256: artifact.sha256 };
}
if (packBytes) {
const destination = join(root, "dist", "remote-provider-packs", "linux-x64", "provider-pack.json");
mkdirSync(dirname(destination), { recursive: true });
// Replace the inode so repeat assembly can preserve a read-only manifest.
const staged = `${destination}.staging-${process.pid}`;
try {
writeFileSync(staged, packBytes, { mode: 0o444, flag: "wx" });
renameSync(staged, destination);
} finally {
rmSync(staged, { force: true });
}
}
writeFileSync(join(root, "dist", "bin", "release-manifest.json"), JSON.stringify({schema:"paperclip.runner.release-binaries.v1",sourceRevision:manifest.sourceRevision,platforms,
...(packIdentity ? { remoteProviderPack: { target: "linux-x64", digest: packIdentity.digest, sourceRevision: packIdentity.payload.runnerSourceRevision } } : {})},null,2)+"\n");
console.log("Staged verified release daemons for " + required.join(", "));
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
@@ -1,11 +1,66 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { stageRunnerBinary } from "./stage-runner-binary.mjs";
import { RELEASE_RUNNER_TARGETS, releaseDaemonManifest, verifyReleaseRunnerMetadata } from "./stage-release-runner-binaries.mjs";
const contract = { package: { name: "@paperclipai/paperclip-runner", version: "0.0.0" },
contracts: { runnerdArtifact: 2, nativeExecution: 1, harnessDriver: 1 }, prp: { name: "paperclip.runner", minimumVersion: 1, maximumVersion: 3 } };
const metadata = { binaryContractVersion: 2, nativeExecutionVersion: 1, harnessDriverVersion: 1,
packageName: "@paperclipai/paperclip-runner", packageVersion: "0.0.0", prp: { name: "paperclip.runner", minimumVersion: 1, maximumVersion: 2 } };
test("release assembly binds all three actual targets, source, hashes and compatible metadata", async () => {
const root = await realpath(await mkdtemp(path.join(tmpdir(), "runner-release-inputs-"))), source = "a".repeat(40);
const verify = value => verifyReleaseRunnerMetadata(value, contract);
const receiptPath = target => path.join(root, target, "receipt.json");
try {
for (const target of RELEASE_RUNNER_TARGETS) {
await mkdir(path.join(root, target));
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); bytes.writeUInt32LE(target === "darwin-x64" ? 0x01000007 : 0x0100000c, 4); }
await writeFile(path.join(root, target, "paperclip-runnerd"), bytes);
await writeFile(receiptPath(target), JSON.stringify({ schema: "paperclip.runner.release-artifact.v1", sourceRevision: source,
target, sha256: "sha256:" + createHash("sha256").update(bytes).digest("hex"), buildMetadata: metadata }));
}
const actual = releaseDaemonManifest(root, source, verify);
assert.deepEqual(Object.keys(actual.platforms), RELEASE_RUNNER_TARGETS);
assert.equal(actual.remoteProviderPack, undefined, "Daemon-only data does not qualify a provider pack");
assert.throws(() => releaseDaemonManifest(root, "b".repeat(40), verify), /source, digest or architecture/);
const original = await readFile(receiptPath("darwin-arm64"), "utf8"), receipt = JSON.parse(original);
for (const delta of [{ target: "darwin-x64" }, { sha256: "sha256:" + "0".repeat(64) }, { sourceRevision: "b".repeat(40) },
{ buildMetadata: { ...metadata, binaryContractVersion: 99 } }]) {
await writeFile(receiptPath("darwin-arm64"), JSON.stringify({ ...receipt, ...delta }));
assert.throws(() => releaseDaemonManifest(root, source, verify));
}
await writeFile(receiptPath("darwin-arm64"), original);
const binary = path.join(root, "darwin-arm64/paperclip-runnerd"), bytes = await readFile(binary);
await writeFile(binary, await readFile(path.join(root, "darwin-x64/paperclip-runnerd")));
assert.throws(() => releaseDaemonManifest(root, source, verify), /digest or architecture/);
await rm(binary); await symlink(path.join(root, "darwin-x64/paperclip-runnerd"), binary);
assert.throws(() => releaseDaemonManifest(root, source, verify), /regular file/);
await rm(binary); await writeFile(binary, bytes);
await mkdir(path.join(root, "win32-x64"));
assert.throws(() => releaseDaemonManifest(root, source, verify), /exactly the three/);
await rm(path.join(root, "win32-x64"), { recursive: true });
await rm(receiptPath("linux-x64"));
assert.throws(() => releaseDaemonManifest(root, source, verify), /only its daemon and receipt/);
} finally { await rm(root, { recursive: true, force: true }); }
});
test("release metadata rejects incompatible native, harness and protocol contracts without semver heuristics", () => {
assert.equal(verifyReleaseRunnerMetadata(metadata, contract), metadata);
for (const delta of [{ nativeExecutionVersion: 9 }, { harnessDriverVersion: 9 }, { packageName: "untrusted" },
{ prp: { ...metadata.prp, name: "other" } }, { prp: { ...metadata.prp, minimumVersion: 4, maximumVersion: 5 } }]) {
assert.throws(() => verifyReleaseRunnerMetadata({ ...metadata, ...delta }, contract), /incompatible/);
}
});
async function withOwnedChildren(root, action) {
const owned = [];
+3 -2
View File
@@ -12,10 +12,11 @@ export function paperclipRunnerProfileForHarness(harness: string): { provider: "
return harness === "codex_local" ? { provider: "codex" } : undefined;
}
/** Public server packages currently ship a Linux x64 daemon; source-build targets are not release qualification. */
/** Creation defaults follow the shipped Linux x64 and macOS release artifacts. */
export function paperclipRunnerSupportsPlatform(harness: string, platform: string, architecture: string): boolean {
return !!paperclipRunnerProfileForHarness(harness)
&& platform === "linux" && architecture === "x64";
&& ((platform === "linux" && architecture === "x64")
|| (platform === "darwin" && (architecture === "arm64" || architecture === "x64")));
}
/** Recover harness identity without changing execution or guessing unknown providers. */
@@ -4,7 +4,7 @@ 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, assertInstalledCodexPlatformProvenance, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource } from '../grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } 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]] : []);
@@ -141,6 +141,18 @@ test('the installed Codex probe exercises the public export and rejects incomple
writeFileSync(legacyCommand, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(legacyCommand)};`);
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /Selected Codex platform version must match the qualified pin/);
// Native Codex uses protocol compatibility, not the ACPX version allowlist.
// Keep exact official-npm host provenance and the selected resource boundary.
mkdirSync(join(legacy, 'vendor'));
writeFileSync(legacyCommand, '#!/bin/sh\nprintf "codex-cli 0.156.1\\n"\n', { mode: 0o755 });
writeFileSync(security, securitySource.replaceAll(JSON.stringify(nativeVendor), JSON.stringify(join(legacy, 'vendor'))));
writeFileSync(probe, installedCodexProbeSource(index, consumer, undefined, { nativeCodex: true }));
result = run(); assert.equal(result.status, 0, result.stderr);
assert.equal(JSON.parse(result.stdout).installedCodexVersion, 'codex-cli 0.156.1');
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, undefined);
writeFileSync(probe, installedCodexProbeSource(index, consumer, '0.160.0'));
writeFileSync(security, securitySource);
writeFileSync(index, `import { writeFileSync } from 'node:fs'; export const resolvePinnedCodexCommand = () => {
writeFileSync(${JSON.stringify(join(platform, 'package.json'))}, ${JSON.stringify(JSON.stringify({ ...manifest, altered: true }))});
return ${JSON.stringify(command)}; };`);
@@ -229,3 +241,47 @@ test('consumer provenance rejects bundled, foreign, inconsistent and non-officia
assert.throws(() => check(entry, [{ path: 'node_modules/@openai/codex-darwin-arm64', manifest }]), /only the host/);
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, name: 'untrusted' } }]), /identity/);
});
test('hosted Mac lifecycle denies OS networking and writes outside owned scratch', () => {
const profile = macPublicInstallLifecycleProfile('/Users/runner/work/_temp/owned-check');
assert.match(profile, /\(deny network\*\)/);
assert.match(profile, /\(deny file-write\*/);
assert.match(profile, /subpath "\/Users\/runner\/work\/_temp\/owned-check"/);
assert.throws(() => macPublicInstallLifecycleProfile('relative'), /absolute/);
assert.throws(() => macPublicInstallLifecycleProfile('/tmp/owned\0bad'), /absolute/);
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
assert.match(source, /process.env.GITHUB_ACTIONS, 'true'/);
assert.match(source, /networkProbe, 'allowed'/);
assert.match(source, /networkProbe, 'denied'/);
assert.match(source, /error.code,'EPERM'/);
assert.match(source, /'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts'/);
assert.match(source, /readFileSync\(sentinel, 'utf8'\), 'ok'/);
assert.match(source, /readFileSync\(join\(consumer, 'package-lock.json'\), 'utf8'\), lock/);
assert.match(source, /finally \{ rmSync\(root, \{ recursive: true, force: true \}\); \}/);
});
test('installed daemon proof uses the production host resolver and complete source-bound manifest', () => {
const source = installedDaemonProbeSource('/consumer/server/dist/vendor/paperclip-runner', '/consumer', 'a'.repeat(40));
assert.match(source, /defaultCapabilityRunnerdBinary\(\)/);
assert.match(source, /Object.keys\(manifest.platforms\).sort\(\), targets.sort\(\)/);
assert.match(source, /manifest.sourceRevision, "a{40}"/);
assert.match(source, /runnerBinaryTarget\(bytes\), target/);
assert.match(source, /createHash\('sha256'\)/);
assert.match(source, /metadata.binaryContractVersion, expected.contracts.runnerdArtifact/);
assert.doesNotMatch(source, /PAPERCLIP_RUNNERD_BINARY|runner\/target\/debug|execFileSync\('paperclip-runnerd'/);
});
test('Mac startup wiring normalizes only validated manifest digests and uses server-scoped tooling', () => {
const source = macInstalledStartupProbeSource('/owned/consumer', '/owned/consumer/server', 'sha256:' + 'a'.repeat(64), '/source/tests/runner-e2e/installed-cli.ts');
assert.match(source, /expectedRunnerSha256:"a{64}"/);
assert.doesNotMatch(source, /expectedRunnerSha256:"sha256:/);
for (const digest of ['a'.repeat(64), 'sha256:bad', 'sha512:' + 'a'.repeat(64), 'sha256:' + 'A'.repeat(64)]) {
assert.throws(() => macInstalledStartupProbeSource('/owned/consumer', '/owned/consumer/server', digest, '/source/helper.ts'), /verified SHA-256/);
}
const verifier = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
assert.match(verifier, /\['--import', 'tsx', startupFile\], join\(repo, 'server'\)/);
const closedEnvironment = verifier.match(/const env = \{ PATH: `\$\{tools\}[\s\S]*?\};/)[0];
assert.doesNotMatch(closedEnvironment, /NODE_PATH|NODE_OPTIONS|\.\.\.process.env/);
});
@@ -1,7 +1,8 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import test from "node:test";
@@ -38,14 +39,14 @@ test("chaos verification isolates callers that verify the same source commit", (
test("canary reuses exact-source proof while stable keeps full verification", () => {
const releaseWorkflow = readWorkflow("release.yml");
const canary = releaseWorkflow.split(" verify_canary:\n")[1].split("\n publish_canary:")[0];
const canary = releaseWorkflow.match(/ verify_canary:\n[\s\S]*?(?=\n [a-z_]+:)/)[0];
assert.match(canary, /github\.repository == 'paperclipai\/paperclip' && github\.event_name == 'push' && github\.ref == 'refs\/heads\/master'/);
assert.match(canary, /actions: read/);
assert.match(canary, /ref: \$\{\{ github\.sha \}\}/);
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/);
assert.match(releaseWorkflow, /publish_canary:\n\s+if: .*github\.event_name == 'push'.*\n\s+needs: \[verify_canary, release_runner_source, release_runner_binaries\]/);
// 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.
@@ -480,3 +481,85 @@ test("direct protocol concurrency override only lowers the configured ceiling",
if (expected !== null) assert.equal(result.stdout, expected);
}
});
test("release daemon production uses exact-source native hosts and no publisher or provider permissions", () => {
const workflow = readWorkflow("release-verify.yml");
const producer = workflow.split(" runner_binaries:\n")[1];
for (const [target, host] of [["linux-x64", "ubuntu-latest"], ["darwin-arm64", "macos-15"], ["darwin-x64", "macos-15-intel"]]) {
assert.ok(producer.includes(`- target: ${target}\n host: ${host}`));
}
assert.match(producer, /persist-credentials: false/);
assert.match(producer, /contents: read/);
assert.doesNotMatch(producer, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
assert.match(producer, /\[\[ "\$SOURCE_SHA" =~ \^\[a-f0-9\]\{40\}\$ \]\]/);
assert.match(producer, /git rev-parse HEAD/);
assert.match(producer, /process.platform \+ "-" \+ process.arch/);
assert.match(producer, /pnpm build:binary/);
assert.match(producer, /stage-release-runner-binaries.mjs record/);
for (const job of ["runner_chaos_evals", "typecheck", "general_tests", "serialized_tests", "runner_workflow_evals", "verify_paperclip_runner", "build"]) {
assert.ok(workflow.includes(`${job}:\n if: \${{ !inputs.runner_binaries_only }}`));
}
const release = readWorkflow("release.yml");
for (const job of ["publish_canary", "publish_nightly", "publish_beta", "preview_stable", "publish_stable"]) {
const block = release.split(` ${job}:\n`)[1].split(/\n [a-z_]+:\n/)[0];
assert.match(block, /needs: .*release_runner_binaries/);
assert.match(block, /pattern: \$\{\{ needs.release_runner_binaries.outputs.runner_artifact_prefix \}\}-\*/);
assert.match(block, /PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR/);
}
const script = readFileSync(path.join(repoRoot, 'scripts/release.sh'), 'utf8');
assert.ok(script.indexOf('assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR"') < script.indexOf('scripts/build-standalone-public-packages.mjs'));
assert.match(script, /cp -R .*paperclip-runner\/dist\/bin\/\." .*server\/dist\/vendor\/paperclip-runner\/bin\//);
assert.match(script, /elif \[ "\$dry_run" = false \]; then[\s\S]*?release_fail "Public releases require/);
});
test("premerge Mac qualification consumes the same Linux-packed npm bytes without publishing", () => {
const smoke = readWorkflow("release-smoke.yml");
assert.match(smoke, /smoke_service:\n\s+if: inputs.qualification_source_sha == ''/);
assert.match(smoke, /smoke:\n\s+if: inputs.qualification_source_sha == ''/);
const qualification = smoke.split(" qualification_daemons:\n")[1];
assert.doesNotMatch(qualification, /secrets\.|npm publish|id-token: write|contents: write|continue-on-error/);
assert.match(qualification, /runner_binaries_only: true/);
assert.match(qualification, /PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR/);
assert.match(qualification, /if: \$\{\{ !cancelled\(\) && steps.pack_identity.outputs.available == 'true' \}\}/);
assert.match(qualification, /name: \$\{\{ inputs.artifact_name \}\}-public-packages/);
assert.match(qualification, /--mac-consumer/);
assert.match(qualification, /host: macos-15\n/);
assert.match(qualification, /host: macos-15-intel\n/);
assert.match(qualification, /pnpm install --ignore-scripts --no-frozen-lockfile/);
});
test("historical promotion sources keep their packaging path while new sources require daemon assembly", () => {
const workflow = readWorkflow("release.yml");
const selector = workflow.split(' - name: Read the exact source packaging contract')[1].split('\n release_runner_binaries:')[0];
const script = selector.split(' run: |\n')[1].split('\n').map(line => line.startsWith(' ') ? line.slice(10) : line).join('\n');
const root = mkdtempSync(path.join(tmpdir(), 'release-source-contract-'));
try {
const tools = path.join(root, 'tools'); mkdirSync(tools);
writeFileSync(path.join(tools, 'git'), '#!/bin/sh\ncase "$1" in\nrev-parse) printf "%s\\n" "$FIXTURE_HEAD";;\nshow) cat "$FIXTURE_SOURCE";;\n*) exit 3;;\nesac\n', { mode: 0o755 });
const source = path.join(root, 'release.sh'), output = path.join(root, 'outputs');
for (const [contents, required] of [['pnpm build\n', false], ['assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$SOURCE_SHA"\n', true]]) {
writeFileSync(source, contents); writeFileSync(output, '');
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), FIXTURE_SOURCE: source, GITHUB_OUTPUT: output,
} });
assert.equal(result.status, 0, result.stderr);
assert.match(readFileSync(output, 'utf8'), new RegExp(`required=${required}`));
}
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'b'.repeat(40), FIXTURE_SOURCE: source, GITHUB_OUTPUT: output,
} });
assert.equal(result.status, 1, 'Wrong checkout cannot opt out of mandatory assembly');
const unreadable = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), FIXTURE_SOURCE: path.join(root, 'absent'), GITHUB_OUTPUT: output,
} });
assert.equal(unreadable.status, 1, 'A failed immutable source read must not masquerade as a legacy source');
} finally { rmSync(root, { recursive: true, force: true }); }
for (const job of ['publish_canary','publish_nightly','publish_beta','preview_stable','publish_stable']) {
const block = workflow.split(` ${job}:\n`)[1].split(/\n [a-z_]+:\n/)[0];
assert.match(block, /needs.release_runner_source.result == 'success'/);
assert.match(block, /needs.release_runner_source.outputs.required == 'false' \|\| needs.release_runner_binaries.result == 'success'/);
assert.match(block, /Download exact-source release daemon receipts\n\s+if: needs.release_runner_source.outputs.required == 'true'/);
}
});
+13 -3
View File
@@ -43,15 +43,25 @@ export function paperclipRunnerBinaryNeedsBuild({
nativeRunnerRequired,
configuredBinary,
platform = process.platform,
architecture = process.arch,
}) {
if (!nativeRunnerRequired) return false;
if (configuredBinary?.trim()) return false;
const executable = platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd";
const packageRoot = path.join(repoRoot, "packages", "paperclip-runner");
const stagedBinary = path.join(packageRoot, "dist", "bin", executable);
const binaryStat = statSync(stagedBinary, { throwIfNoEntry: false });
if (!binaryStat?.isFile()) return true;
// Match production resolution: a staged release target takes precedence over
// the flat source-build output. Remote Mac builds must not trigger Cargo just
// because only the normal darwin-<arch> release executable is present.
let binaryStat;
for (const binary of [
path.join(packageRoot, "dist", "bin", `${platform}-${architecture}`, executable),
path.join(packageRoot, "dist", "bin", executable),
]) {
const candidate = statSync(binary, { throwIfNoEntry: false });
if (candidate?.isFile()) { binaryStat = candidate; break; }
}
if (!binaryStat) return true;
const runnerRoot = path.join(packageRoot, "runner");
const buildInputs = [
+71 -9
View File
@@ -60,7 +60,7 @@ export function assertInstalledCodexPlatformProvenance(lock, installed, target)
// 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) {
export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersion, { nativeCodex = false } = {}) {
return `
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
@@ -118,11 +118,11 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
assert.ok(selectedPlatform, 'Codex wrapper must resolve an official host platform package from its installed graph');
const selectedMetadata = JSON.parse(readFileSync(selectedPlatformManifest, 'utf8'));
assert.deepEqual(selectedMetadata, selectedPlatform.manifest, 'Selected Codex platform metadata must match its scanned provenance');
assert.equal(selectedMetadata.version, ${JSON.stringify(expectedVersion)} + '-' + process.platform + '-' + process.arch,
'Selected Codex platform version must match the qualified pin');
${nativeCodex ? '' : `assert.equal(selectedMetadata.version, ${JSON.stringify(expectedVersion)} + '-' + process.platform + '-' + process.arch,
'Selected Codex platform version must match the qualified pin');`}
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');
env: { PATH: process.env.PATH, HOME: process.env.HOME, NODE_PATH: '' } }).trim();
${nativeCodex ? "assert.match(version, /^codex-cli /, 'Installed official Codex command must execute');" : `assert.equal(version, 'codex-cli ' + ${JSON.stringify(expectedVersion)}, 'Installed Codex version must match the qualified pin');`}
const vendor = dirname(index);
const integrityPath = join(vendor, 'drivers/acpx/installation-integrity.js');
const profilesPath = join(vendor, 'drivers/acpx/qualified-profiles.js');
@@ -138,19 +138,81 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
assert.ok(!readRoots.includes(platformRoot) && !readRoots.includes(root) && !readRoots.includes(sandboxSource.HOME), 'Codex sandbox must not grant enclosing npm or credential-home access');
const sandboxArgs = createIsolatedCodexAppServerArgs(sandboxSource, readRoots).join('\\n');
assert.ok(sandboxArgs.includes(JSON.stringify(nativeVendor) + '="read"'), 'Codex sandbox configuration must retain the selected native executable resources');
const { createAcpxPackageJsonResolver, verifyQualifiedAcpxInstallation } = await import(pathToFileURL(integrityPath).href);
${nativeCodex ? '' : `const { createAcpxPackageJsonResolver, verifyQualifiedAcpxInstallation } = await import(pathToFileURL(integrityPath).href);
const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(profilesPath).href);
const serverRoot = resolve(vendor, '../../..');
assert.ok(contained(serverRoot), 'Codex package authority must be the installed server');
const installation = await verifyQualifiedAcpxInstallation(resolveQualifiedAcpxProfile('codex', 'gpt-5.4'),
createAcpxPackageJsonResolver(serverRoot, join(serverRoot, 'package.json')));
const lease = await installation.openCommand();
await lease.close();
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, pinnedCodexVersion: ${JSON.stringify(expectedVersion)},
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version, codexNativeSandboxResourcesVerified: true, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true, providerCalls: 0 }));
await lease.close();`}
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, installedCodexVersion: version,
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version,
codexNativeSandboxResourcesVerified: true,
${nativeCodex ? '' : `pinnedCodexVersion: ${JSON.stringify(expectedVersion)}, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true,`}
providerCalls: 0 }));
`;
}
/** The installed consumer must select its packaged host daemon, never PATH. */
export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevision) {
return `
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, join, relative, sep } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = realpathSync(${JSON.stringify(consumerRoot)}), output = realpathSync(${JSON.stringify(outputRoot)});
const inside = path => { const value = relative(root, realpathSync(path)); return value !== '' && value !== '..' && !value.startsWith('..' + sep) && !isAbsolute(value); };
assert.ok(inside(output));
const { defaultCapabilityRunnerdBinary } = await import(pathToFileURL(join(output, 'live/runnerd-codex-transport.js')).href);
const { runnerBinaryTarget } = await import(pathToFileURL(join(output, 'live/runner-binary.js')).href);
const { parsePaperclipRunnerdBuildMetadata } = await import(pathToFileURL(join(output, 'evals/runnerd-artifact.js')).href);
const { PAPERCLIP_RUNNER_BUILD_METADATA: expected } = await import(pathToFileURL(join(output, 'evals/build-metadata.js')).href);
const manifest = JSON.parse(readFileSync(join(output, 'bin/release-manifest.json'), 'utf8'));
const targets = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
assert.equal(manifest.schema, 'paperclip.runner.release-binaries.v1');
assert.equal(manifest.sourceRevision, ${JSON.stringify(sourceRevision)});
assert.deepEqual(Object.keys(manifest.platforms).sort(), targets.sort());
for (const target of targets) {
const record = manifest.platforms[target];
assert.equal(record.path, target + '/paperclip-runnerd');
const file = join(output, 'bin', record.path); assert.ok(inside(file));
const bytes = readFileSync(file);
assert.equal(runnerBinaryTarget(bytes), target);
assert.equal('sha256:' + createHash('sha256').update(bytes).digest('hex'), record.sha256);
}
const target = process.platform + '-' + process.arch, selected = defaultCapabilityRunnerdBinary();
assert.ok(inside(selected));
assert.equal(realpathSync(selected), realpathSync(join(output, 'bin', manifest.platforms[target].path)), 'Production must select the packaged host daemon');
const metadata = parsePaperclipRunnerdBuildMetadata(JSON.parse(execFileSync(selected, ['--build-metadata'], {
timeout: 15000, maxBuffer: 64 * 1024, encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, NODE_PATH: '' },
})));
assert.equal(metadata.binaryContractVersion, expected.contracts.runnerdArtifact);
assert.equal(metadata.nativeExecutionVersion, expected.contracts.nativeExecution);
assert.equal(metadata.harnessDriverVersion, expected.contracts.harnessDriver);
assert.equal(metadata.prp.name, expected.prp.name);
assert.ok(metadata.prp.minimumVersion <= expected.prp.maximumVersion && metadata.prp.maximumVersion >= expected.prp.minimumVersion);
console.log(JSON.stringify({ installedRunnerDaemonVerified: true, target, sourceRevision: manifest.sourceRevision,
sha256: manifest.platforms[target].sha256, buildMetadata: metadata, providerCalls: 0 }));
`;
}
// Hosted, credential-free Mac consumers defer only npm lifecycle execution.
// Offline npm flags are not an OS network boundary; sandbox-exec supplies it.
export function macPublicInstallLifecycleProfile(ownedRoot) {
if (!ownedRoot.startsWith('/') || ownedRoot.includes('\0')) throw new Error('Mac lifecycle requires an absolute owned root');
return `(version 1)\n(allow default)\n(deny network*)\n(deny file-write* (require-all (require-not (subpath ${JSON.stringify(ownedRoot)})) (require-not (literal "/dev/null"))))\n`;
}
export function macInstalledStartupProbeSource(consumerRoot, serverRoot, runnerDigest, helperPath) {
if (!/^sha256:[a-f0-9]{64}$/.test(runnerDigest)) throw new Error('Installed Mac startup requires the verified SHA-256 manifest identity');
return `import { probeInstalledCodexStartup } from ${JSON.stringify(helperPath)};
console.log(JSON.stringify(await probeInstalledCodexStartup({consumerRoot:${JSON.stringify(consumerRoot)},
serverRoot:${JSON.stringify(serverRoot)},expectedRunnerSha256:${JSON.stringify(runnerDigest.slice(7))}})));`;
}
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMiB = 256, temporaryExecutable = false }) {
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
throw new Error('Public-install verification requires an unprivileged host user');
+10
View File
@@ -284,6 +284,16 @@ release_info ""
release_info "==> Step 2/7: Building workspace artifacts..."
cd "$REPO_ROOT"
pnpm build
# Hosted producers supply data-only, hash/source/architecture-bound daemon
# receipts. Assemble after the build (which stages its host binary), before
# public packages are packed. The server vendors this same compiled runner.
if [ -n "${PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR:-}" ]; then
node "$REPO_ROOT/packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs" \
assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$(git rev-parse HEAD)"
cp -R "$REPO_ROOT/packages/paperclip-runner/dist/bin/." "$REPO_ROOT/server/dist/vendor/paperclip-runner/bin/"
elif [ "$dry_run" = false ]; then
release_fail "Public releases require the three hosted runner daemon artifacts; use the supported Release workflow."
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
+102 -2
View File
@@ -4,14 +4,95 @@
// explicit and separate from npm installation, and is removed in finally.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { createServer } from 'node:net';
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, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource } from './grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from './grok-public-install-sandbox.mjs';
import { retainRunnerQualificationPackages } from './retain-runner-qualification-packages.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath) {
assert.equal(process.env.GITHUB_ACTIONS, 'true', 'Mac clean consumers run only on disposable hosted CI');
assert.equal(process.platform, 'darwin');
assert.ok(['arm64', 'x64'].includes(process.arch));
assert.match(sourceRevision, /^[a-f0-9]{40}$/);
const packages = realpathSync(packageDirectory);
const manifest = JSON.parse(readFileSync(join(packages, 'manifest.json'), 'utf8'));
assert.equal(manifest.schema, 'paperclip.runner.public-packages/v1');
assert.equal(manifest.sourceRevision, sourceRevision);
assert.ok(manifest.archives.some(entry => entry.name === '@paperclipai/server'));
assert.ok(manifest.archives.some(entry => entry.name === 'paperclipai'));
assert.equal(new Set(manifest.archives.map(entry => entry.name)).size, manifest.archives.length);
const root = realpathSync(mkdtempSync(join(tmpdir(), 'paperclip-mac-public-install-')));
try {
const consumer = join(root, 'consumer'), home = join(root, 'home'), tools = join(root, 'tools');
for (const directory of [consumer, home, tools, join(root, 'tmp'), join(root, 'cache')]) mkdirSync(directory);
const npmCli = realpathSync(execFileSync('/usr/bin/which', ['npm'], { encoding: 'utf8' }).trim());
symlinkSync(process.execPath, join(tools, 'node'));
// npm subprocesses get a Node-only owned tool directory; no ambient harness
// command can hide missing installed dependencies.
const env = { PATH: `${tools}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: join(root, 'tmp'),
npm_config_cache: join(root, 'cache'), npm_config_audit: 'false', npm_config_fund: 'false' };
const run = (command, args, cwd = consumer) => execFileSync(command, args, { cwd, env,
timeout: 10 * 60_000, maxBuffer: 32 * 1024 * 1024, encoding: 'utf8' });
const tarballs = manifest.archives.map(entry => {
assert.match(entry.file, /^[A-Za-z0-9._-]+\.tgz$/); assert.match(entry.sha256, /^[a-f0-9]{64}$/);
const path = join(packages, entry.file);
assert.equal(realpathSync(path), path);
assert.equal(createHash('sha256').update(readFileSync(path)).digest('hex'), entry.sha256);
assertNoBundledCodexPayloads(run('/usr/bin/tar', ['-tzf', path]).trim().split('\n'));
return path;
});
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' }));
const sentinelSource = join(root, 'lifecycle-sentinel'); mkdirSync(sentinelSource);
writeFileSync(join(sentinelSource, 'package.json'), JSON.stringify({ name: 'paperclip-verification-lifecycle-sentinel', version: '1.0.0',
scripts: { postinstall: 'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'ok\')"' } }));
run(process.execPath, [npmCli, 'pack', '--ignore-scripts', '--pack-destination', root], sentinelSource);
const sentinelTar = join(root, readdirSync(root).find(name => name.endsWith('.tgz')));
run(process.execPath, [npmCli, 'install', '--ignore-scripts', '--omit=dev', ...tarballs, sentinelTar]);
const sentinel = join(consumer, 'node_modules/paperclip-verification-lifecycle-sentinel/lifecycle-ran');
assert.equal(existsSync(sentinel), false);
const lock = readFileSync(join(consumer, 'package-lock.json'), 'utf8');
const profile = join(root, 'lifecycle.sb'); writeFileSync(profile, macPublicInstallLifecycleProfile(root));
const listener = createServer(socket => socket.end());
await new Promise((resolve, reject) => { listener.once('error', reject); listener.listen(0, '127.0.0.1', resolve); });
try {
const port = listener.address().port;
const networkProbe = `import assert from 'node:assert/strict'; import net from 'node:net';
const socket = net.connect({host:'127.0.0.1',port:${port}}); socket.setTimeout(3000,()=>process.exit(2));
socket.once('connect',()=>{socket.destroy(); ${"process.argv[1] === 'denied' ? process.exit(3) : process.exit(0)"};});
socket.once('error',error=>{assert.equal(process.argv[1],'denied'); assert.equal(error.code,'EPERM');});`;
run(process.execPath, ['--input-type=module', '-e', networkProbe, 'allowed']);
run('/usr/bin/sandbox-exec', ['-f', profile, process.execPath, '--input-type=module', '-e', networkProbe, 'denied']);
} finally { await new Promise(resolve => listener.close(resolve)); }
run('/usr/bin/sandbox-exec', ['-f', profile, process.execPath, npmCli, 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts', '--foreground-scripts']);
assert.equal(readFileSync(sentinel, 'utf8'), 'ok');
assert.equal(readFileSync(join(consumer, 'package-lock.json'), 'utf8'), lock);
for (const entry of manifest.archives) {
assert.equal(JSON.parse(readFileSync(join(consumer, 'node_modules', entry.name, 'package.json'))).version, manifest.releaseVersion);
}
const server = join(consumer, 'node_modules/@paperclipai/server'), installed = join(server, 'dist/vendor/paperclip-runner');
const probe = (name, source) => { const file = join(root, name); writeFileSync(file, source); return JSON.parse(run(process.execPath, [file]).trim()); };
const daemon = probe('daemon-probe.mjs', installedDaemonProbeSource(installed, consumer, sourceRevision));
const codex = probe('codex-probe.mjs', installedCodexProbeSource(join(installed, 'index.js'), consumer, undefined, { nativeCodex: true }));
const startupScript = macInstalledStartupProbeSource(consumer, server, daemon.sha256, join(repo, 'tests/runner-e2e/installed-cli.ts'));
const startupFile = join(root, 'startup-probe.mts'); writeFileSync(startupFile, startupScript);
const startup = JSON.parse(run(process.execPath, ['--import', 'tsx', startupFile], join(repo, 'server')).trim());
const receipt = { schema: 'paperclip.runner.mac-public-install.v1', sourceRevision, platform: process.platform, architecture: process.arch,
cleanNpmInstall: true, packageCount: manifest.archives.length, codexPackedPayloadsOmitted: true,
consumerLockPreserved: true, lifecycleSentinelVerified: true, lifecycleNetwork: 'sandbox-exec denied (loopback negative control)',
...daemon, ...codex, startup, providerCalls: 0 };
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + '\n');
console.log(JSON.stringify(receipt));
} finally { rmSync(root, { recursive: true, force: true }); }
}
if (process.argv[2] === '--mac-consumer') {
assert.equal(process.argv.length, 6, 'Usage: --mac-consumer PACKAGE_DIRECTORY SOURCE_SHA RECEIPT_PATH');
await verifyMacConsumer(process.argv[3], process.argv[4], process.argv[5]);
} else {
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-'));
const prerequisite = join(root, 'native/grok');
@@ -64,6 +145,24 @@ try {
assertNoBundledCodexPayloads(run('tar', ['-tzf', tarball]).toString().trim().split('\n'));
tarballs.push(tarball);
}
// Preserve the exact Linux-produced public tarballs for independent Mac
// consumers even if a later Linux runtime probe fails. This is a packaging
// receipt, never an installation or provider-admission pass.
if (process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR) {
const output = resolve(process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR);
assert.equal(JSON.parse(readFileSync(join(repo, 'server/dist/build-info.json'))).commit, sourceRevision);
assert.ok(!existsSync(output), 'Never replace retained public packages');
mkdirSync(output, { recursive: true });
const { createHash } = await import('node:crypto');
const archives = [...needed].map((name, index) => {
const file = basename(tarballs[index]); cpSync(tarballs[index], join(output, file));
return { name, file, sha256: createHash('sha256').update(readFileSync(tarballs[index])).digest('hex') };
});
writeFileSync(join(output, 'manifest.json'), JSON.stringify({
schema: 'paperclip.runner.public-packages/v1', sourceRevision, releaseVersion,
publicInstallationVerified: false, providerCalls: 0, archives,
}, null, 2) + '\n');
}
const assets = join(root, 'assets'); mkdirSync(assets, { mode: 0o755 });
const consumer = join(root, 'consumer'); mkdirSync(consumer);
const cache = join(root, 'cache'); mkdirSync(cache);
@@ -153,3 +252,4 @@ try {
} finally {
rmSync(root, { recursive: true, force: true });
}
}
+4 -1
View File
@@ -168,7 +168,10 @@ describe("adapter routes", () => {
it.each([
[{ driver: "ssh", platform: "linux", architecture: "x64" }, "paperclip"],
[{ driver: "ssh", platform: "darwin", architecture: "arm64" }, "legacy"],
[{ driver: "ssh", platform: "darwin", architecture: "arm64" }, "paperclip"],
[{ driver: "ssh", platform: "darwin", architecture: "x64" }, "paperclip"],
[{ driver: "ssh", platform: "linux", architecture: "arm64" }, "legacy"],
[{ driver: "local", platform: "win32", architecture: "x64" }, "legacy"],
])("projects the selected environment once for the whole adapter inventory (%s)", async (target, defaultRunner) => {
const resolveRunnerTarget = vi.fn(async () => target);
const response = await request(createApp({}, { resolveRunnerTarget })).get("/api/adapters?companyId=company-1&environmentId=selected");
@@ -1260,7 +1260,9 @@ describe("agent skill routes", () => {
const codexRunnerCases = [
{ adapterType: "codex_local", runner: "auto", platform: "linux", expectedType: "paperclip_runner" },
{ adapterType: "codex_local", runner: "legacy", platform: "linux", expectedType: "codex_local" },
{ adapterType: "codex_local", runner: "auto", platform: "darwin", expectedType: "codex_local" },
{ adapterType: "codex_local", runner: "auto", platform: "darwin", expectedType: "paperclip_runner" },
{ adapterType: "codex_local", runner: "legacy", platform: "darwin", expectedType: "codex_local" },
{ adapterType: "codex_local", runner: "auto", platform: "win32", expectedType: "codex_local" },
];
it.each([...codexRunnerCases, { adapterType: "claude_local", runner: "auto", platform: "linux", expectedType: "claude_local" }].flatMap((runnerCase) => [
["agents", "paperclipai/paperclip/paperclip-create-agent"],
@@ -17,6 +17,7 @@ import {
} from "./helpers/embedded-postgres.js";
import { agentService } from "../services/agents.ts";
import { approvalService } from "../services/approvals.ts";
import { paperclipRunnerSupportsPlatform } from "@paperclipai/shared";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
@@ -71,9 +72,7 @@ describeEmbeddedPostgres("pending approval agent config integrity", () => {
const native = await lifecycle.requestHire(companyId, { name: "New Codex", adapterType: "codex_local", adapterConfig: { modelReasoningEffort: "high" }, status: "pending_approval" });
const legacy = await lifecycle.requestHire(companyId, { name: "Legacy Codex", adapterType: "codex_local", runner: "legacy", adapterConfig: { extraArgs: ["--search"] }, status: "pending_approval" });
const reviewedLegacy = await lifecycle.requestHire(companyId, { name: "Historical approved hire", adapterType: "codex_local", adapterConfig: {}, status: "pending_approval" }, { runnerResolved: true });
// Only public Linux x64 artifacts qualify an automatic native default;
// source-built macOS/Windows agents keep legacy until their packaging is qualified.
if (process.platform === "linux" && process.arch === "x64") {
if (paperclipRunnerSupportsPlatform("codex_local", process.platform, process.arch)) {
expect(native).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", modelReasoningEffort: "high" } });
} else {
expect(native).toMatchObject({ adapterType: "codex_local", adapterConfig: { modelReasoningEffort: "high" } });
+8 -6
View File
@@ -267,7 +267,9 @@ describeEmbeddedPostgres("built-in agents", () => {
])).toThrow("defaultAdapterType must be allowed");
});
it("lazily provisions one agent per company/key and updates the same row on setup", async () => {
it.each([{ platform: "linux", architecture: "x64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("lazily provisions one agent per company/key and updates the same row on setup ($platform/$architecture)", async ({ platform, architecture }) => {
runnerTarget.platform = platform;
runnerTarget.architecture = architecture;
const companyId = await seedCompany();
const svc = builtInAgentService(db);
@@ -327,9 +329,9 @@ describeEmbeddedPostgres("built-in agents", () => {
expect(switchedBack.agent).toMatchObject({ adapterType: "codex_local", adapterConfig: savedSettings });
const switchedAgain = await svc.ensure(companyId, "briefs", { runner: "paperclip" });
expect(switchedAgain.agent).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", ...savedSettings } });
// A saved native profile remains editable on source-built macOS installs.
runnerTarget.platform = "darwin";
runnerTarget.architecture = "arm64";
// Existing native execution survives editing on an unqualified target.
runnerTarget.platform = "win32";
runnerTarget.architecture = "x64";
const edited = await svc.ensure(companyId, "briefs", { adapterType: "codex_local", adapterConfig: { model: "gpt-5.4" } });
expect(edited.agent).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.4" } });
});
@@ -339,8 +341,8 @@ describeEmbeddedPostgres("built-in agents", () => {
const result = await builtInAgentService(db).provision(companyId, "briefs", { adapterType: "codex_local", adapterConfig: { model: "gpt-5.4" } }, { requestedByUserId: "board-user" });
expect(result.approval?.payload).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.4" } });
// Restart and approval activate the reviewed config even if eligibility changed.
runnerTarget.platform = "darwin";
runnerTarget.architecture = "arm64";
runnerTarget.platform = "win32";
runnerTarget.architecture = "x64";
await reconcileBuiltInAgentsOnStartup(db);
await approvalService(db).approve(result.approval!.id, "board-user");
const approved = await builtInAgentService(db).get(companyId, "briefs");
@@ -6067,6 +6067,7 @@ describe("company portability", () => {
it.each([
{ runner: "legacy", platform: "linux" },
{ runner: "legacy", platform: "darwin" },
{ runner: "paperclip", platform: "linux" },
{ runner: "paperclip", platform: "darwin" },
] as const)("exports the resolved Codex runner and preserves it on import ($runner/$platform)", async ({ runner, platform }) => {
@@ -6099,7 +6100,9 @@ describe("company portability", () => {
it.each([
{ action: "create", platform: "linux", expectedType: "paperclip_runner" },
{ action: "update", platform: "linux", expectedType: "codex_local" },
{ action: "create", platform: "darwin", expectedType: "codex_local" },
{ action: "create", platform: "darwin", expectedType: "paperclip_runner" },
{ action: "update", platform: "darwin", expectedType: "codex_local" },
{ action: "create", platform: "win32", expectedType: "codex_local" },
])("applies qualified Codex defaults only to new imports without a runner ($platform/$action)", async ({ action, platform, expectedType }) => {
runnerTarget.platform = platform;
runnerTarget.architecture = platform === "darwin" ? "arm64" : "x64";
@@ -6118,6 +6121,7 @@ describe("company portability", () => {
it.each([
{ existingRunner: "legacy", existingHarness: "codex_local", expectedType: "codex_local", platform: "linux" },
{ existingRunner: "legacy", existingHarness: "codex_local", expectedType: "codex_local", platform: "darwin" },
{ existingRunner: "paperclip", existingHarness: "codex_local", expectedType: "paperclip_runner", platform: "linux" },
{ existingRunner: "paperclip", existingHarness: "codex_local", expectedType: "paperclip_runner", platform: "darwin" },
{ existingRunner: "legacy", existingHarness: "claude_local", expectedType: "paperclip_runner", platform: "linux" },
@@ -110,6 +110,24 @@ describe("paperclip runner native dev prerequisite", () => {
).toBe(false);
});
it.each(["arm64", "x64"])("uses the staged Mac %s target before the flat build output", architecture => {
const checkout = createRunnerCheckout();
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, "target-runnerd");
const old = new Date(Date.now() - 2000), current = new Date(Date.now() + 2000);
fs.utimesSync(checkout.binary, old, old);
fs.utimesSync(target, current, current);
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
fs.rmSync(checkout.binary);
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
fs.utimesSync(target, old, old);
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
fs.rmSync(target);
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
});
it("does not build a workspace binary when an explicit binary is configured", () => {
expect(
paperclipRunnerBinaryNeedsBuild({
@@ -284,7 +284,9 @@ describeEmbeddedPostgres("plugin-managed agents", () => {
});
});
it("creates managed agents with the most-used compatible company adapter", async () => {
it.each([{ platform: "linux", architecture: "x64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("creates managed agents with the most-used compatible company adapter ($platform/$architecture)", async ({ platform, architecture }) => {
runnerTarget.platform = platform;
runnerTarget.architecture = architecture;
const pluginManifest = manifest();
pluginManifest.agents![0] = {
...pluginManifest.agents![0]!,
@@ -336,8 +338,8 @@ describeEmbeddedPostgres("plugin-managed agents", () => {
const reconciled = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" });
expect(reconciled.agent?.adapterConfig).toEqual(created.agent?.adapterConfig);
// Reset declaration defaults without requalifying an existing native agent.
runnerTarget.platform = "darwin";
runnerTarget.architecture = "arm64";
runnerTarget.platform = "win32";
runnerTarget.architecture = "x64";
const reset = await services.agents.managedReset({ companyId, agentKey: "wiki-maintainer" });
expect(reset.agent).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } });
});
@@ -9,10 +9,11 @@ vi.mock("./environment-config.js", () => ({ resolveEnvironmentDriverConfigForRun
vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: state.ssh }));
import { agentRunnerAvailability, resolveAgentRunnerTargetForCompany, resolveNewAgentRunner, resolveNewAgentRunnerForCompany } from "./agent-runner-selection.js";
const target = { driver: "local", platform: "linux", architecture: "x64" };
const qualifiedTargets = [target, { driver: "local", platform: "darwin", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "x64" }];
describe("server-owned Codex runner selection", () => {
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
it("uses native Codex automatically with compatible configuration and is idempotent", () => {
it.each(qualifiedTargets)("uses native Codex automatically with compatible configuration and is idempotent on $platform/$architecture", target => {
const input = { adapterType: "codex_local", target, adapterConfig: { model: "gpt-5.6-sol", modelReasoningEffort: "high", env: { OPENAI_API_KEY: { type: "secret_ref", secretId: "secret" } }, cwd: "/workspace", instructionsFilePath: "/workspace/AGENTS.md", workspaceStrategy: { type: "git_worktree" }, timeoutSec: 60, search: false, fastMode: false, dangerouslyBypassApprovalsAndSandbox: true } };
const resolved = resolveNewAgentRunner(input);
expect(resolved).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", codexPermissionMode: "never", modelReasoningEffort: "high", env: input.adapterConfig.env, workspaceStrategy: input.adapterConfig.workspaceStrategy, timeoutSec: 60 } });
@@ -23,7 +24,7 @@ describe("server-owned Codex runner selection", () => {
it.each(["claude_local", "opencode_local", "grok_local", "cursor", "pi_local", "process", "external"])("keeps %s default unchanged", adapterType => {
expect(resolveNewAgentRunner({ adapterType, target, adapterConfig: { custom: "preserved" } })).toEqual({ adapterType, adapterConfig: { custom: "preserved" } });
});
it("allows an explicit legacy runner and native to legacy round trip", () => {
it.each(qualifiedTargets)("allows an explicit legacy runner and native to legacy round trip on $platform/$architecture", target => {
expect(resolveNewAgentRunner({ adapterType: "codex_local", target, runner: "legacy", adapterConfig: { extraArgs: ["--search"] } }).adapterType).toBe("codex_local");
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "codex", model: "gpt-5.6-sol", lifecycleMode: "per_turn", codexPermissionMode: "never", modelReasoningEffort: "high" } })).toEqual({ adapterType: "codex_local", adapterConfig: { model: "gpt-5.6-sol", modelReasoningEffort: "high", dangerouslyBypassApprovalsAndSandbox: true } });
});
@@ -33,23 +34,26 @@ describe("server-owned Codex runner selection", () => {
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn", dangerouslyBypassApprovalsAndSandbox: false } }))
.toEqual({ adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false } });
});
it.each(["darwin", "win32"])("keeps unqualified %s targets legacy while preserving explicit native profiles", platform => {
const input = { adapterType: "codex_local", target: { driver: "local", platform, architecture: "x64" } };
it.each([{ platform: "win32", architecture: "x64" }, { platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "ia32" }])("keeps unqualified $platform/$architecture targets legacy while preserving explicit native profiles", ({ platform, architecture }) => {
const input = { adapterType: "codex_local", target: { driver: "local", platform, architecture } };
expect(resolveNewAgentRunner(input).adapterType).toBe("codex_local");
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" }, target: input.target };
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
expect(resolveNewAgentRunner({ ...saved, runner: "paperclip" })).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
});
it("automatically keeps unsupported targets and active external overrides legacy", () => {
it.each(qualifiedTargets)("automatically keeps unsupported targets and active external overrides legacy on $platform/$architecture", target => {
const input = { adapterType: "codex_local", target: { driver: "local", platform: "win32", architecture: "x64" } };
expect(resolveNewAgentRunner(input).adapterType).toBe("codex_local");
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
state.overridden.add("codex_local");
expect(agentRunnerAvailability("codex_local", target).defaultRunner).toBe("legacy");
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
state.overridden.clear();
state.overridden.add("paperclip_runner");
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
});
it("does not select a disabled runner", () => {
it.each(qualifiedTargets)("does not select a disabled runner on $platform/$architecture", target => {
state.disabled = ["paperclip_runner"];
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target, runner: "paperclip" })).toThrow("unavailable");
@@ -80,7 +84,9 @@ describe("server-owned Codex runner selection", () => {
});
it.each([
["Linux\nx86_64\n", "paperclip", "paperclip_runner"],
["Darwin\narm64\n", "legacy", "codex_local"],
["Darwin\narm64\n", "paperclip", "paperclip_runner"],
["Darwin\nx86_64\n", "paperclip", "paperclip_runner"],
["Linux\naarch64\n", "legacy", "codex_local"],
])("uses the same selected SSH target for discovery and saving (%s)", async (stdout, expectedRunner, expectedAdapter) => {
state.getEnvironment.mockResolvedValue({ id: "selected", driver: "ssh", status: "active", config: {} });
state.ssh.mockResolvedValue({ stdout });
@@ -113,9 +119,10 @@ describe("Codex creation runner contract", () => {
expect(agentRunner("codex_local")).toBe("legacy");
expect(agentRunner("paperclip_runner")).toBe("paperclip");
});
it("qualifies only the daemon actually shipped in public server packages", () => {
expect(paperclipRunnerSupportsPlatform("codex_local", "linux", "x64")).toBe(true);
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "arm64"], ["win32", "x64"], ["freebsd", "x64"]]) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(false);
it("qualifies only the Codex daemon targets shipped in public server packages", () => {
for (const { platform, architecture } of qualifiedTargets) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(true);
for (const [platform, architecture] of [["darwin", "ia32"], ["linux", "arm64"], ["win32", "x64"], ["win32", "arm64"], ["freebsd", "x64"]]) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(false);
expect(paperclipRunnerSupportsPlatform("claude_local", "darwin", "arm64")).toBe(false);
});
it("keeps auto out of saved update defaults and validates request intent", () => {
expect(createAgentSchema.parse({ name: "Codex", adapterType: "codex_local" }).runner).toBeUndefined();
+21 -2
View File
@@ -2001,12 +2001,31 @@ Run the explicit `paperclipai runtime setup pi` for that installation first.
This lane rejects candidate flags, local/remote daemon overrides, provider asset
or pack overrides, and Node injection. It launches the installed JavaScript CLI
without a TypeScript loader, rechecks its pins on controller restart, and records
`installed-cli-admission.json` in private attempt evidence. It supports only the
explicit Pi extended, native, controls and warm suites, which require none of the
`installed-cli-admission.json` in private attempt evidence. Pi uses the explicit
extended, native, controls and warm suites, which require none of the
source-only response barriers. Cursor/Copilot qualification gates stay intact.
Pi's historical `qualificationCandidate` fixture selector remains a roster key;
it no longer grants an opt-in when the source Pi declaration admits normal use.
The same four installed-package pins also admit three existing local native
Codex cells: `first-task.runner-codex.local.task-card-accept`,
`everyday-workflows.runner-codex.local.build-revise`, and
`agent-chat.runner-codex.local.continuity-restart`. Select exact IDs; other
Codex cells, legacy profiles and remote environments remain outside this lane.
These reuse the onboarding, independently checked file/revision, and restart
graders. They require separately authorized provider access and are not executed
by the provider-free release smoke.
The release consumer calls `probeInstalledCodexStartup` from `installed-cli.ts`
against its installed server, normal platform daemon and contained npm Codex.
It checks a real native read-only session and joined cleanup using empty homes,
file-only authentication storage and a closed environment. It sends no provider
turn, consults no account, records the actual CLI version without an equality
gate, and exposes no session IDs. Build/source, Mach-O/ELF and official npm
provenance checks precede this probe. Startup proof does not establish useful-file
delivery, authentication, reasoning effort or follow-up behavior; the existing
live journeys must prove those separately.
Installed Pi Daytona cells also require the published plugin fixture inputs
`PAPERCLIP_RUNNER_E2E_INSTALLED_DAYTONA_PLUGIN` (canonical package root),
`PAPERCLIP_RUNNER_E2E_INSTALLED_DAYTONA_PLUGIN_AUTHORITY` (canonical JSON file),
+73 -2
View File
@@ -1,11 +1,11 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { chmod, mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, expect, it } from "vitest";
import { runnerMatrix } from "./catalog.js";
import { assertInstalledCliSelection, installedCliKeys, verifyInstalledCli } from "./installed-cli.js";
import { assertInstalledCliSelection, installedCliKeys, probeInstalledCodexStartup, verifyInstalledCli } from "./installed-cli.js";
import { buildPaperclipServerEnvironment, buildRunnerE2EProcessEnvironment } from "./harness-env.js";
const cells = runnerMatrix.filter(e => e.profile.id === "runner-acpx-pi");
const roots: string[] = [];
@@ -25,6 +25,20 @@ it("admits all26 current Pi cases through pinned installed public CLI/server wit
const { env, server } = await fixture(); expect(cells).toHaveLength(26);
for (const cell of cells) expect(await verifyInstalledCli(env, [cell])).toMatchObject({ serverRoot: server, defaultRuntimeResolution: true, qualificationOverride: false });
});
it("admits existing Codex onboarding, useful-file revision and restart journeys without broadening other cells", async () => {
const { env, server } = await fixture();
for (const id of ["first-task.runner-codex.local.task-card-accept", "everyday-workflows.runner-codex.local.build-revise", "agent-chat.runner-codex.local.continuity-restart"]) {
const cell = runnerMatrix.find(e => e.id === id)!;
expect(cell).toBeDefined();
expect(await verifyInstalledCli(env, [cell])).toMatchObject({ serverRoot: server, defaultRuntimeResolution: true, qualificationOverride: false });
for (const key of ["PAPERCLIP_RUNNER_BINARY", "NODE_PATH"]) expect(() => assertInstalledCliSelection({ ...env, [key]: "foreign" }, [cell])).toThrow("forbids");
}
for (const id of ["first-task.legacy-codex.local.task-card-accept", "first-task.runner-codex.local.interview-first-response", "everyday-workflows.runner-codex.daytona.build-revise"]) {
const cell = runnerMatrix.find(e => e.id === id)!;
expect(cell).toBeDefined();
expect(() => assertInstalledCliSelection(env, [cell])).toThrow("explicit supported Pi or Codex");
}
});
it("qualified providers drop ambient admission while pending Copilot retains its exact gate", () => {
for (const cell of cells) expect(buildRunnerE2EProcessEnvironment({ PAPERCLIP_RUNNER_ACPX_QUALIFICATION: "ambient" }, [cell]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
for (const agent of ["copilot"]) {
@@ -92,3 +106,60 @@ it("startup-only accepts only explicit uncredentialed local hello and zero retri
expect(()=>assertInstalledStartupOnly({...f.env,KIMI_MODEL_API_KEY:"dummy-not-a-real-key"},[cell],options)).toThrow("credential inputs");
expect(()=>assertInstalledStartupOnly({},[cell],options)).toThrow("startup-only");
});
async function startupFixture(mode: "normal" | "wrong-model" | "ambient-codex" = "normal") {
const f = await fixture();
const vendor = join(f.server, "dist/vendor/paperclip-runner");
const daemon = join(vendor, "bin", `${process.platform}-${process.arch}`, "paperclip-runnerd");
const codex = join(f.root, "node_modules/@openai/codex/bin/codex");
const observations = join(f.root, "startup-observations.json");
await mkdir(join(vendor, "bin", `${process.platform}-${process.arch}`), { recursive: true });
await mkdir(join(f.server, "dist/services/native-runtime"), { recursive: true });
await mkdir(join(f.root, "node_modules/@openai/codex/bin"), { recursive: true });
await writeFile(daemon, "fixture-daemon");
await writeFile(codex, "#!/bin/sh\nprintf 'codex-cli 0.149.5\\n'\n");
await chmod(codex, 0o755);
await writeFile(join(f.server, "dist/services/native-runtime/native-codex-runner.js"), `export function resolvePaperclipRunnerBinary() { return ${JSON.stringify(daemon)}; }`);
await writeFile(join(vendor, "index.js"), `
import {writeFileSync} from "node:fs";
const state={calls:[],environmentKeys:[],fileAuthPinned:false};let retired=false;
const save=()=>writeFileSync(${JSON.stringify(observations)},JSON.stringify(state));
export function resolveCodexCommand(){return ${JSON.stringify(mode === "ambient-codex" ? process.execPath : codex)};}
export function createIsolatedCodexAppServerArgs(){return ["app-server"];}
export function createRunnerdCodexTransport(options){
state.environmentKeys=Object.keys(options.environment).sort();
state.fileAuthPinned=options.codexArgs.includes('cli_auth_credentials_store="file"');
save();return {transport:{async close(){retired=true;state.calls.push("transport-close");save();}},
evidence:()=>({runnerPid:111,codexPid:222,providerDriver:"codex_app_server",runnerExited:retired})};}
export class CodexAppServerDriver {
constructor(options){this.options=options;}
async openSession(){state.calls.push("open");save();const model=${JSON.stringify(mode === "wrong-model" ? "wrong-model" : "gpt-5.6-sol")};
return {ids:()=>({driverSessionId:"PRIVATE-DRIVER-ID",providerSessionId:"PRIVATE-PROVIDER-ID"}),
events:()=>({async *[Symbol.asyncIterator](){yield {eventType:"session.started",payload:{context:{model,sandbox:{permissionProfile:{id:"paperclip-runner-workspace-read-only"}}}}};}}),
startTurn(){state.calls.push("FORBIDDEN-TURN");save();throw new Error("No provider turn is permitted");},
async close(){retired=true;state.calls.push("session-close");save();}};}
}
`);
return { ...f, observations, expectedRunnerSha256: hash("fixture-daemon") };
}
it.skipIf(process.platform === "win32")("installed native startup uses closed homes, file-only auth and no turn or version equality gate", async () => {
const f = await startupFixture();
const receipt = await probeInstalledCodexStartup({ consumerRoot: f.root, serverRoot: f.server, expectedRunnerSha256: f.expectedRunnerSha256 });
expect(receipt).toMatchObject({ nativeSessionStarted: true, cleanupConfirmed: true, providerTurns: 0, credentialsUsed: false,
codexVersion: "codex-cli 0.149.5", defaultRuntimeResolution: true, reasoningEffortExercised: false });
expect(await readFile(f.observations, "utf8").then(JSON.parse)).toMatchObject({ calls: ["open", "session-close"], fileAuthPinned: true,
environmentKeys: ["CODEX_HOME", "HOME", "LC_ALL", "PAPERCLIP_RUNNER_NETWORK_ACCESS", "PATH", "TMPDIR", "TZ"] });
expect(JSON.stringify(receipt)).not.toMatch(/PRIVATE-(?:DRIVER|PROVIDER)-ID/);
});
it.skipIf(process.platform === "win32")("installed startup rejects stale daemon/ambient Codex and cleans a mismatched native session", async () => {
const f = await startupFixture();
await expect(probeInstalledCodexStartup({ consumerRoot: f.root, serverRoot: f.server, expectedRunnerSha256: "0".repeat(64) })).rejects.toThrow("reviewed pin");
await expect(readFile(f.observations)).rejects.toThrow();
const ambient = await startupFixture("ambient-codex");
await expect(probeInstalledCodexStartup({ consumerRoot: ambient.root, serverRoot: ambient.server, expectedRunnerSha256: ambient.expectedRunnerSha256 })).rejects.toThrow("ambient Codex");
const wrong = await startupFixture("wrong-model");
await expect(probeInstalledCodexStartup({ consumerRoot: wrong.root, serverRoot: wrong.server, expectedRunnerSha256: wrong.expectedRunnerSha256 })).rejects.toThrow("matching native identity");
expect(await readFile(wrong.observations, "utf8").then(JSON.parse)).toMatchObject({ calls: ["open", "session-close"] });
});
+98 -3
View File
@@ -1,21 +1,33 @@
/** Optional published-install lane. No production hook or admission override. */
import { constants } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import { lstat, mkdir, mkdtemp, open, realpath, rm } from "node:fs/promises";
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { findPackageJSON } from "node:module";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
import { CREDENTIAL_NAMES, type MatrixExecution } from "./types.js";
export const installedCliKeys = ["PAPERCLIP_RUNNER_E2E_INSTALLED_CLI", "PAPERCLIP_RUNNER_E2E_INSTALLED_CLI_SHA256", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_ROOT", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_SHA256"] as const;
const overrideKeys = ["PAPERCLIP_RUNNER_BINARY", "PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", "PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "PAPERCLIP_RUNNER_ACPX_QUALIFICATION", "PAPERCLIP_ACPX_BUILTIN_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"] as const;
const suites = new Set(["extended-harnesses", "pi-native", "pi-controls", "rich-acp-warm-continuity"]);
// Reuse the production-onboarding, independent file/revision, and restart
// journeys. Installed proof does not add cases or relax their existing graders.
const installedCodexCells = new Set([
"first-task.runner-codex.local.task-card-accept",
"everyday-workflows.runner-codex.local.build-revise",
"agent-chat.runner-codex.local.continuity-restart",
]);
export function usesInstalledCli(env: NodeJS.ProcessEnv): boolean {
return installedCliKeys.some(key => env[key] !== undefined);
}
export function assertInstalledCliSelection(env: NodeJS.ProcessEnv, executions: readonly MatrixExecution[]): void {
if (!usesInstalledCli(env)) return;
if (!installedCliKeys.every(key => Boolean(env[key])) || executions.length === 0 || executions.some(e => e.profile.id !== "runner-acpx-pi" || e.profile.qualificationCandidate !== "pi" || !e.suite.manualOnly || !suites.has(e.suite.id))) throw new Error("Installed CLI proof requires complete pins and explicit supported Pi cases");
if (!installedCliKeys.every(key => Boolean(env[key])) || executions.length === 0 || executions.some(e => !(
(e.profile.id === "runner-acpx-pi" && e.profile.qualificationCandidate === "pi" && e.suite.manualOnly && suites.has(e.suite.id)) ||
(e.profile.id === "runner-codex" && e.profile.provider === "codex" && e.environment.id === "local" && installedCodexCells.has(e.id))
))) throw new Error("Installed CLI proof requires complete pins and explicit supported Pi or Codex cases");
for (const key of overrideKeys) if (env[key] !== undefined) throw new Error(`Installed CLI proof forbids ${key}`);
if (env.NODE_OPTIONS !== undefined || env.NODE_PATH !== undefined) throw new Error("Installed CLI proof forbids ambient Node injection");
}
@@ -51,6 +63,89 @@ export async function verifyInstalledCli(env: NodeJS.ProcessEnv, executions: rea
return { schema: "paperclip.e2e.installed-cli-admission/v1", entry, cliRoot, cliSha256, serverRoot, serverEntry, serverSha256, version: cli.version, defaultRuntimeResolution: true, qualificationOverride: false };
}
/** Clean-install protocol proof only: no credentials, task, or provider turn. */
export async function probeInstalledCodexStartup(input: {
consumerRoot: string;
serverRoot: string;
expectedRunnerSha256: string;
timeoutMs?: number;
}) {
for (const key of [...overrideKeys, "NODE_OPTIONS", "NODE_PATH"]) {
if (process.env[key] !== undefined) throw new Error(`Installed Codex startup forbids ${key}`);
}
const inside = (root: string, filename: string) => {
const name = relative(root, filename);
return name !== "" && name !== ".." && !name.startsWith(`..${sep}`) && !isAbsolute(name);
};
for (const filename of [input.consumerRoot, input.serverRoot]) {
if (!isAbsolute(filename) || resolve(filename) !== filename || await realpath(filename) !== filename) throw new Error("Installed Codex startup paths must be canonical and unlinked");
}
if (!inside(join(input.consumerRoot, "node_modules"), input.serverRoot) || !/^[a-f0-9]{64}$/.test(input.expectedRunnerSha256)) throw new Error("Installed Codex startup requires a contained server and reviewed daemon digest");
const manifest = JSON.parse((await readOwned(join(input.serverRoot, "package.json"), 65536)).toString());
if (manifest.name !== "@paperclipai/server" || manifest.exports?.["."]?.import !== "./dist/index.js") throw new Error("Installed Codex startup requires the published server");
const vendorRoot = join(input.serverRoot, "dist/vendor/paperclip-runner");
const entries = [join(vendorRoot, "index.js"), join(input.serverRoot, "dist/services/native-runtime/native-codex-runner.js")];
for (const filename of entries) if (await realpath(filename) !== filename) throw new Error("Installed Codex startup runtime escapes its package");
const [runtime, production] = await Promise.all(entries.map(filename => import(pathToFileURL(filename).href)));
const runnerBinary = await realpath(production.resolvePaperclipRunnerBinary());
if (runnerBinary !== join(vendorRoot, "bin", `${process.platform}-${process.arch}`, "paperclip-runnerd")) throw new Error("Installed Codex startup requires the normal platform daemon; no source or flat fallback");
const digest = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
const runnerSha256 = digest(await readOwned(runnerBinary, 512 * 1024 * 1024));
if (runnerSha256 !== input.expectedRunnerSha256) throw new Error("Installed Codex startup daemon differs from its reviewed pin");
const directory = await realpath(await mkdtemp(join(tmpdir(), "pc-codex-startup-")));
const home = join(directory, "home"), codexHome = join(directory, "codex-home"), workspace = join(directory, "workspace");
for (const filename of [home, codexHome, workspace]) await mkdir(filename, { mode: 0o700 });
const environment = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin`, HOME: home, CODEX_HOME: codexHome,
TMPDIR: directory, LC_ALL: "C", TZ: "UTC", PAPERCLIP_RUNNER_NETWORK_ACCESS: "disabled" };
let session: any;
let bundle: any;
let cleanupConfirmed = false;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error("Installed Codex startup timed out")), Math.max(1, Math.min(input.timeoutMs ?? 45000, 120000)));
try {
const codexCommand = await realpath(runtime.resolveCodexCommand(undefined, environment, workspace));
if (!inside(join(input.consumerRoot, "node_modules"), codexCommand)) throw new Error("Installed Codex startup resolved an ambient Codex executable");
const codexSha256 = digest(await readOwned(codexCommand, 512 * 1024 * 1024));
const codexVersion = execFileSync(codexCommand, ["--version"], { cwd: workspace, env: environment, encoding: "utf8", timeout: 10000, maxBuffer: 16384 }).trim();
if (!codexVersion || codexVersion.length > 200) throw new Error("Installed Codex startup returned no bounded version identity");
// Keep the production security arguments; pin file-only auth for this
// credential-free fixture so macOS keychain accounts cannot be consulted.
const args = runtime.createIsolatedCodexAppServerArgs(environment);
args.splice(args.indexOf("app-server"), 0, "-c", 'cli_auth_credentials_store="file"');
bundle = runtime.createRunnerdCodexTransport({ provider: "codex", stateDirectory: directory, environment,
sourceCodexHome: "", runnerBinary, codexCommand, codexArgs: args, closeGraceMs: 10000,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } });
const driver = new runtime.CodexAppServerDriver({
taskEnvelope: { schema: "paperclip.skillless_task.v1", objective: "Credential-free installed startup proof.",
completionContract: { revision: "installed-startup-v1", criteria: [] }, constraints: [], expectedResultSchema: "paperclip.run_result.v1" },
model: "gpt-5.6-sol", conversationMode: "direct", requestedCollaborationMode: "plan", approvalPolicy: "never",
includeCollaborationModeInstructions: false, dynamicTools: [], environment,
transportFactory: () => bundle.transport, requireProviderSessionIdentity: true,
});
session = await driver.openSession({ runId: "installed-startup", normalizedSessionId: "installed-startup", workingDirectory: workspace, signal: controller.signal });
const event = await session.events()[Symbol.asyncIterator]().next();
const identity = session.ids();
const evidence = bundle.evidence();
if (event.value?.eventType !== "session.started" || event.value.payload?.context?.model !== "gpt-5.6-sol" ||
event.value.payload?.context?.sandbox?.permissionProfile?.id !== "paperclip-runner-workspace-read-only" ||
!identity.driverSessionId || !identity.providerSessionId || evidence.providerDriver !== "codex_app_server" ||
!(evidence.runnerPid > 0) || !(evidence.codexPid > 0)) throw new Error("Installed Codex startup returned no matching native identity or read-only model context");
await session.close({ reason: "credential-free installed startup complete" });
session = undefined;
if (!bundle.evidence().runnerExited) throw new Error("Installed Codex startup cleanup is incomplete");
cleanupConfirmed = true;
return { schema: "paperclip.e2e.installed-codex-startup/v1", platform: `${process.platform}-${process.arch}`,
runnerSha256, codexSha256, codexVersion, providerDriver: "codex_app_server", model: "gpt-5.6-sol",
nativeSessionStarted: true, cleanupConfirmed: true, providerTurns: 0, credentialsUsed: false,
defaultRuntimeResolution: true, reasoningEffortExercised: false };
} finally {
clearTimeout(timer);
if (session) await session.close({ reason: "installed startup cleanup" });
else if (bundle && !cleanupConfirmed) await bundle.transport.close("installed startup cleanup");
if (!bundle || bundle.evidence().runnerExited) await rm(directory, { recursive: true, force: true });
}
}
/** This prep lane cannot create agents or read the normal local credential file. */
export function assertInstalledStartupOnly(env: NodeJS.ProcessEnv, executions: readonly MatrixExecution[], options: { ids: string[]; maxParallel: number; maxAutomaticRetries: number; headed: boolean; ui: boolean; debug: boolean; list: boolean; matrixJson: boolean }) {
assertInstalledCliSelection(env, executions);
+8 -4
View File
@@ -200,10 +200,10 @@ afterEach(async () => {
container.remove();
});
describe("New agent setup", () => {
it("records an explicit legacy choice when it matches the Mac default, then preserves it on Linux", async () => {
it("records an explicit legacy choice when it matches the Windows default, then preserves it on Linux", async () => {
settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" });
envApi.list.mockResolvedValue([
{ id: "local-1", name: "Mac", driver: "local", status: "active", config: {} },
{ id: "local-1", name: "Windows", driver: "local", status: "active", config: {} },
{ id: "linux-ssh", name: "Linux", driver: "ssh", status: "active", config: {} },
]);
adapterApi.list.mockImplementation(async (context?: { environmentId?: string | null }) => state.adapters.map((adapter: any) =>
@@ -242,7 +242,7 @@ describe("New agent setup", () => {
it("discovers the runner for the selected target and rechecks after changing environments", async () => {
settings.get.mockResolvedValue({ defaultEnvironmentId: "linux-ssh" });
envApi.list.mockResolvedValue([
{ id: "local-1", name: "Mac", driver: "local", status: "active", config: {} },
{ id: "local-1", name: "Windows", driver: "local", status: "active", config: {} },
{ id: "linux-ssh", name: "Linux", driver: "ssh", status: "active", config: {} },
]);
adapterApi.list.mockImplementation(async (context?: { companyId: string; environmentId?: string | null }) => state.adapters.map((adapter: any) =>
@@ -287,9 +287,13 @@ describe("New agent setup", () => {
expect(api.testEnvironment.mock.calls[0][2]).toMatchObject({ runner: "legacy" });
expect(api.hire.mock.calls[0][1]).toMatchObject({ runner: "legacy" });
});
it("uses automatic Codex resolution through setup and hire without the experimental gate", async () => {
it.each(["Linux x64", "Mac arm64", "Mac x64"])("uses automatic Codex resolution through setup and hire without the experimental gate (%s)", async name => {
settings.getExperimental.mockResolvedValue({ enableNativeRunner: false });
envApi.list.mockResolvedValue([{ id: "local-1", name, driver: "local", status: "active", config: {} }]);
state.adapters = (state.adapters as any[]).map(adapter => adapter.type === "codex_local" ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" } : adapter);
await render("codex_local");
await act(async () => (container.querySelector("summary") as HTMLElement).click());
expect(container.querySelector('[aria-label="Runner"]')?.textContent).toBe("Paperclip Runner (default)");
await connect("OpenAI");
await click("Finish setup");
expect(api.hire.mock.calls[0][1].adapterType).toBe("codex_local");