From 5c2a78fa45151771429b8f4050867da189e910eb Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 7 Oct 2026 07:03:25 -0500 Subject: [PATCH] ci(hermes): provision pinned runtime for selected paid campaigns Select candidate assets consistently for immutable image identity and remote packs; provision local Linux assets before the protected paid step exposes credentials. Preserve default-branch dispatch and qualification gates. Co-Authored-By: Paperclip --- .github/workflows/runner-full-stack-e2e.yml | 35 ++++++++++++++++----- tests/runner-e2e/README.md | 7 +++++ tests/runner-e2e/provision-hermes-linux.sh | 12 +++++++ tests/runner-e2e/workflow-security.test.ts | 23 ++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/runner-e2e/provision-hermes-linux.sh diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index 97afd34ead..be2489a8e8 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -223,6 +223,7 @@ jobs: needs_runner_typescript: ${{ steps.catalog.outputs.needs_runner_typescript }} needs_native_binaries: ${{ steps.catalog.outputs.needs_native_binaries }} needs_remote_provider_pack: ${{ steps.catalog.outputs.needs_remote_provider_pack }} + candidate_providers: ${{ steps.catalog.outputs.candidate_providers }} execution_ids: ${{ steps.catalog.outputs.execution_ids }} max_parallel: ${{ steps.catalog.outputs.max_parallel }} daytona_image_content_id: ${{ steps.daytona_image_content.outputs.content_id }} @@ -271,12 +272,6 @@ jobs: - run: pnpm install --frozen-lockfile - # The v2 contract fails closed unless every Docker FROM is digest-pinned, - # and hashes those exact base references into the immutable image tag. - - name: Compute Daytona image content ID with pinned bases - id: daytona_image_content - run: echo "content_id=$(pnpm --silent test:e2e:runner:image-id)" >> "$GITHUB_OUTPUT" - - name: Validate selectors and emit matrix id: catalog env: @@ -341,6 +336,7 @@ jobs: echo "needs_runner_typescript=$(jq -r '[.include[] | select((.profileId == "runner-opencode") or (.profileId | startswith("runner-acpx-")) or (.suiteId == "openrouter-model-breadth"))] | length > 0' <<< "$catalog_json")" echo "needs_native_binaries=$(jq -r '[.include[] | select((.profileId | startswith("runner-")) or (.suiteId == "openrouter-model-breadth"))] | length > 0' <<< "$catalog_json")" echo "needs_remote_provider_pack=$(jq -r '[.include[] | select((.environmentId == "daytona") and ((.profileId == "runner-opencode") or (.profileId | startswith("runner-acpx-"))))] | length > 0' <<< "$catalog_json")" + echo "candidate_providers=$(jq -r 'if any(.include[]; .profileId == "runner-acpx-hermes") then "hermes" else "" end' <<< "$catalog_json")" echo "execution_ids=$(jq -c '.executionIds' <<< "$catalog_json")" } >> "$GITHUB_OUTPUT" if ! [[ "$MAX_PARALLEL_LIMIT" =~ ^[1-9][0-9]*$ ]] || [ "$MAX_PARALLEL_LIMIT" -gt 100 ]; then @@ -360,6 +356,14 @@ jobs: fi echo "max_parallel=$MAX_PARALLEL" >> "$GITHUB_OUTPUT" + # Exact optional provider assets are part of the immutable image identity. + # Selecting assets never promotes a pending profile to qualified. + - name: Compute Daytona image content ID with pinned bases + id: daytona_image_content + env: + CANDIDATE_PROVIDERS: ${{ steps.catalog.outputs.candidate_providers }} + run: echo "content_id=$(pnpm --silent test:e2e:runner:image-id "--candidate-providers=$CANDIDATE_PROVIDERS")" >> "$GITHUB_OUTPUT" + daytona_image: name: Publish verified Daytona image needs: [authorize, target_lock, catalog] @@ -433,6 +437,7 @@ jobs: IMAGE_CACHE: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-buildcache-amd64 TARGET_SHA: ${{ needs.authorize.outputs.target_sha }} TARGET_LOCK_SHA256: ${{ needs.target_lock.outputs.lock_sha256 }} + CANDIDATE_PROVIDERS: ${{ needs.catalog.outputs.candidate_providers }} TARGET_REF: ${{ needs.authorize.outputs.target_ref }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | @@ -467,6 +472,7 @@ jobs: --build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}" \ --build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=${TARGET_SHA}" \ --build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}" \ + --build-arg "PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS=${CANDIDATE_PROVIDERS}" \ --file docker/daytona-runner/Dockerfile \ --tag "$IMAGE_TAG" \ "${cache_args[@]}" \ @@ -721,6 +727,13 @@ jobs: test -d packages/paperclip-eval-kernel/dist test -d packages/paperclip-runner/dist + - name: Provision pinned Hermes assets before assembling the remote pack + if: needs.catalog.outputs.needs_remote_provider_pack == 'true' && needs.catalog.outputs.candidate_providers == 'hermes' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends python3-venv + bash tests/runner-e2e/provision-hermes-linux.sh + - name: Assemble native remote provider pack if: needs.catalog.outputs.needs_remote_provider_pack == 'true' env: @@ -728,7 +741,8 @@ jobs: # content ID. Matching that revision lets remote execution reuse the # verified pack already installed in the immutable image. PAPERCLIP_RUNNER_SOURCE_REVISION: ${{ needs.daytona_image.outputs.source_revision }} - run: node packages/paperclip-runner/scripts/build-provider-pack.mjs packages/paperclip-runner/provider-pack + CANDIDATE_PROVIDERS: ${{ needs.catalog.outputs.candidate_providers }} + run: node packages/paperclip-runner/scripts/build-provider-pack.mjs packages/paperclip-runner/provider-pack "--candidate-providers=$CANDIDATE_PROVIDERS" - name: Package verified remote provider pack if: needs.catalog.outputs.needs_remote_provider_pack == 'true' @@ -890,6 +904,13 @@ jobs: if: matrix.environmentId == 'local' && (matrix.profileId == 'legacy-opencode' || matrix.profileId == 'runner-opencode' || matrix.suiteId == 'openrouter-model-breadth') run: node packages/paperclip-runner/scripts/materialize-opencode-binary.mjs + - name: Provision pinned Hermes before the paid local test + if: matrix.environmentId == 'local' && matrix.profileId == 'runner-acpx-hermes' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends python3-venv bubblewrap + bash tests/runner-e2e/provision-hermes-linux.sh + - name: Install checksum-verified Grok executable if: matrix.environmentId == 'local' && (matrix.profileId == 'runner-acpx-grok' || matrix.profileId == 'runner-acpx-grok-subscription') run: sudo node packages/paperclip-runner/scripts/provision-grok.mjs /opt/paperclip/providers/grok/1.0.13/grok diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index cfc423cd35..d313241877 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -1477,6 +1477,13 @@ Hermes uses an existing managed OpenRouter connection through the ordinary connection fixture. It remains pending qualification; the native transport fixture does not substitute for these browser and remote workflows. +When a Hermes cell is selected, the trusted workflow provisions its pinned +Python closure on Linux before exposing credentials, and selects the same +candidate assets for the image content identity, Docker build and remote +provider pack. The workflow must be available on the default branch before +dispatching a branch campaign; dispatching from a development branch remains +forbidden. The asset selection leaves Hermes pending qualification. + ```sh pnpm test:e2e:runner -- --list --suite extended-harnesses pnpm test:e2e:runner -- --id extended-harnesses.runner-acpx-pi.local.hello-complete diff --git a/tests/runner-e2e/provision-hermes-linux.sh b/tests/runner-e2e/provision-hermes-linux.sh new file mode 100644 index 0000000000..21d984678d --- /dev/null +++ b/tests/runner-e2e/provision-hermes-linux.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Credential-free setup for an explicitly selected native Hermes campaign. +set -euo pipefail +test "$(uname -s)" = Linux +test "$(uname -m)" = x86_64 +repository_root="$(cd "$(dirname "$0")/../.." && pwd)" +hermes_build_root="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/hermes-build.XXXXXX")" +trap 'rm -rf "$hermes_build_root"' EXIT +python3 -m venv "$hermes_build_root" +"$hermes_build_root/bin/pip" install --disable-pip-version-check --no-input uv==0.12.17 +PATH="$hermes_build_root/bin:$PATH" node "$repository_root/packages/paperclip-runner/scripts/provision-hermes.mjs" \ + "$repository_root/packages/paperclip-runner/provider-assets/hermes/linux-x64" diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index c1173c16be..9d714488fa 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -15,6 +15,29 @@ const everydayOracleImage = "python@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285"; describe("public repository paid workflow security", () => { + it("provisions selected Hermes assets before credentials and uses the same selection for image identity and packs", async () => { + const workflow = await readFile(path.join(repositoryRoot, ".github/workflows/runner-full-stack-e2e.yml"), "utf8"); + const catalog = workflow.slice(workflow.indexOf(" catalog:"), workflow.indexOf(" daytona_image:")); + expect(catalog).toContain('if any(.include[]; .profileId == "runner-acpx-hermes") then "hermes" else "" end'); + expect(catalog.indexOf("Validate selectors and emit matrix")).toBeLessThan(catalog.indexOf("Compute Daytona image content ID")); + expect(catalog).toContain('"--candidate-providers=$CANDIDATE_PROVIDERS"'); + const image = workflow.slice(workflow.indexOf(" daytona_image:"), workflow.indexOf(" build_runner_artifacts:")); + expect(image).toContain('--build-arg "PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS=${CANDIDATE_PROVIDERS}"'); + const pack = workflow.slice(workflow.indexOf(" build_remote_provider_pack:"), workflow.indexOf(" test:")); + expect(pack).toContain("needs.catalog.outputs.candidate_providers == 'hermes'"); + expect(pack.indexOf("Provision pinned Hermes assets")).toBeLessThan(pack.indexOf("Assemble native remote provider pack")); + expect(pack).toContain('"--candidate-providers=$CANDIDATE_PROVIDERS"'); + expect(pack).not.toContain("secrets."); + const paid = workflow.slice(workflow.indexOf(" test:"), workflow.indexOf(" aggregate:")); + expect(paid).toContain("matrix.environmentId == 'local' && matrix.profileId == 'runner-acpx-hermes'"); + const setup = paid.indexOf("Provision pinned Hermes before the paid local test"); + expect(setup).toBeGreaterThan(0); + expect(setup).toBeLessThan(paid.indexOf("secrets.OPENROUTER_API_KEY")); + expect(paid.slice(setup, paid.indexOf("Install checksum-verified Grok"))).not.toContain("secrets."); + const provision = await readFile(path.join(repositoryRoot, "tests/runner-e2e/provision-hermes-linux.sh"), "utf8"); + expect(provision).toContain("uv==0.12.17"); + expect(provision).toContain("scripts/provision-hermes.mjs"); + }); it("keeps the manual EC2 image build credential-free and pins the authorized target", async () => { const workflow = await readFile(path.join(repositoryRoot, ".github/workflows/docker-runner-check.yml"), "utf8"); const manual = workflow.slice(workflow.indexOf(" authorize_manual:"));