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)
This commit is contained in:
Dotta 2026-09-03 18:10:38 -05:00 committed by GitHub
parent 871f7d1124
commit 03faa644fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 23 deletions

View File

@ -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"

View File

@ -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 () => {