From 03faa644fb00b40186a0d5bed2cdb59f0fd8e560 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Thu, 3 Sep 2026 18:10:38 -0500 Subject: [PATCH] ci(runner): inspect Daytona image metadata remotely (#12795) ## Thinking Path The reused Daytona image path already verifies the signed immutable digest. It then downloads every filesystem layer only to read OCI config fields. Buildx can retrieve the same config from that immutable digest without pulling the layers. The assertions can therefore stay intact while removing the expensive transfer. ## What Changed - inspect the signed immutable Daytona image config through Buildx after GHCR logout - preserve digest, source revision, content ID, platform, user, and provider-pack assertions - extend the workflow contract test for the metadata-only path ## Verification - Daytona image and workflow security tests: 10 passed - Prettier and git diff checks passed - observed full pull/prune cost: about 4m55s; metadata inspection: about one second ## Risks The current image has one runnable linux/amd64 platform plus its attestation. A future genuinely multi-platform image would need explicit linux/amd64 selection. ## Model Used Codex (GPT-5) --- .github/workflows/runner-full-stack-e2e.yml | 34 +++++++++------------ tests/runner-e2e/daytona-image.test.ts | 19 ++++++++++-- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index c0eb1dfb44..a4125829d0 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -418,31 +418,25 @@ jobs: --certificate-oidc-issuer https://token.actions.githubusercontent.com \ "$IMAGE_TAG@$digest" >/dev/null immutable="${IMAGE_TAG%:*}@$digest" - # The Daytona base image is large. The build cache plus a second full - # anonymous pull can exhaust a standard GitHub-hosted runner before - # Docker creates the tiny metadata-probe container. The pushed digest - # is already immutable, so release the local builder/cache first. - docker buildx prune --all --force >/dev/null - docker system prune --all --force >/dev/null - anonymous_config="$(mktemp -d)" - docker --config "$anonymous_config" pull "$immutable" # The Dockerfile's final two RUN steps execute the runner metadata, # transport-mode, provider-pack JSON, and pinned ACP binary checks as - # root and as the unprivileged Daytona user. Starting another - # container after this full pull can exhaust the hosted runner's thin - # writable layer even after pruning, so assert the published image - # configuration here without creating a redundant container. - image_config="$(docker image inspect "$immutable" \ - --format '{{json .}}')" - published_content_id="$(jq -r '.Config.Labels["io.paperclip.runner.content-id"] // empty' <<< "$image_config")" - source_revision="$(jq -r '.Config.Labels["org.opencontainers.image.revision"] // empty' <<< "$image_config")" + # root and as the unprivileged Daytona user. Buildx reads the signed + # digest's OCI config directly from GHCR, so verification does not + # download the image's large filesystem layers. Logging out first + # preserves the proof that Daytona can retrieve this public image + # without the workflow's package credentials. + docker logout ghcr.io >/dev/null + image_config="$(docker buildx imagetools inspect "$immutable" \ + --format '{{json .Image}}')" + published_content_id="$(jq -r '.config.Labels["io.paperclip.runner.content-id"] // empty' <<< "$image_config")" + source_revision="$(jq -r '.config.Labels["org.opencontainers.image.revision"] // empty' <<< "$image_config")" test "$published_content_id" = "$IMAGE_CONTENT_ID" [[ "$source_revision" =~ ^[0-9a-f]{40}$ ]] jq -e \ - '.Architecture == "amd64" and - .Os == "linux" and - .Config.User == "daytona" and - (.Config.Env | any(startswith("PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=")))' \ + '.architecture == "amd64" and + .os == "linux" and + .config.User == "daytona" and + (.config.Env | any(startswith("PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=")))' \ <<< "$image_config" >/dev/null echo "image=$immutable" >> "$GITHUB_OUTPUT" echo "source_revision=$source_revision" >> "$GITHUB_OUTPUT" diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 19211d331d..ec56047665 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -71,8 +71,16 @@ describe("runner E2E Daytona image contract", () => { ); expect(workflow).not.toContain("e2e-git-${{ github.sha }}"); expect(workflow).toContain("cosign sign --yes"); - expect(workflow).toContain("docker image inspect"); - expect(workflow).toContain('.Config.User == "daytona"'); + expect(workflow).toContain("docker logout ghcr.io"); + expect(workflow).toContain(`docker buildx imagetools inspect "$immutable"`); + expect(workflow).toContain(`--format '{{json .Image}}'`); + expect(workflow).not.toContain(`docker --config "$anonymous_config" pull`); + expect(workflow).not.toContain("docker image inspect"); + expect(workflow).not.toContain("docker buildx prune --all --force"); + expect(workflow).not.toContain("docker system prune --all --force"); + expect(workflow).toContain('.architecture == "amd64"'); + expect(workflow).toContain('.os == "linux"'); + expect(workflow).toContain('.config.User == "daytona"'); expect(workflow).toContain("PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT="); expect(workflow).toContain( "node packages/paperclip-runner/scripts/build-provider-pack.mjs packages/paperclip-runner/provider-pack", @@ -83,7 +91,12 @@ describe("runner E2E Daytona image contract", () => { expect(workflow).toContain( "PAPERCLIP_RUNNER_SOURCE_REVISION: ${{ needs.daytona_image.outputs.source_revision }}", ); - expect(workflow).toContain("anonymous_config"); + expect(workflow.indexOf("cosign verify")).toBeLessThan( + workflow.indexOf("docker logout ghcr.io"), + ); + expect(workflow.indexOf("docker logout ghcr.io")).toBeLessThan( + workflow.indexOf(`--format '{{json .Image}}'`), + ); }); it("hashes the audited image dependency closure rather than the repository revision", async () => {