mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 23:36:51 +02:00
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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
4a0b327727
commit
5c2a78fa45
4 files changed
+70
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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:"));
|
||||
|
||||
Reference in new issue
Block a user