From a3a71ebdb165e456a58757a1b6d233f302b2c840 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 11:59:18 -0500 Subject: [PATCH] Bind Daytona candidate image identity to selected provider assets Refresh the reviewed resolved dependency digest and fix ACP selector typing. Co-Authored-By: Paperclip --- docker/daytona-runner/Dockerfile | 2 +- docker/daytona-runner/README.md | 4 ++++ tests/runner-e2e/daytona-image-content.ts | 23 ++++++++++++++++++- tests/runner-e2e/daytona-image.test.ts | 9 ++++++++ ui/src/adapters/codex-local/config-fields.tsx | 2 +- 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index 27357e3d2e9..725cc4349e9 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -28,7 +28,7 @@ COPY cli/package.json ./cli/package.json # The complete resolved lock (including transitive integrity hashes) is reviewed. # Reject registry-time drift BEFORE installing packages or running lifecycle code. # Refresh this digest together with source/provider dependency changes. -ARG PAPERCLIP_RUNNER_LOCK_SHA256=d7d96cf0d98cf0946f6195e29ba173b03711a947a1c38f312b67cda56c254c22 +ARG PAPERCLIP_RUNNER_LOCK_SHA256=3b2842ff53812112cf2eb59efa0664e10630d5f7a7586813fdd07741131486f9 RUN pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile \ && printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \ && sha256sum -c /tmp/provider-lock.sha256 \ diff --git a/docker/daytona-runner/README.md b/docker/daytona-runner/README.md index e16f6fa1d6d..5eac1a84b8c 100644 --- a/docker/daytona-runner/README.md +++ b/docker/daytona-runner/README.md @@ -90,6 +90,10 @@ A pack with candidate assets does not qualify or enable that provider. For an isolated Linux x64 Daytona qualification image, pass `--build-arg PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS=cursor` with the normal build arguments. +Compute its content ID with the same selection: +`pnpm --silent test:e2e:runner:image-id --candidate-providers=cursor`. +Candidate assets and the default pack have distinct image identities. Never reuse +the default image's content ID for a candidate build. Use each provider branch's recorded version and explicit model. Keep credentials out of images. Paid qualification requires bound provider and Daytona credentials, inspectable spend, and the shared $100 ceiling recorded in the capability report. diff --git a/tests/runner-e2e/daytona-image-content.ts b/tests/runner-e2e/daytona-image-content.ts index 23a9a5dca8f..45538962c49 100644 --- a/tests/runner-e2e/daytona-image-content.ts +++ b/tests/runner-e2e/daytona-image-content.ts @@ -34,6 +34,7 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [ "packages/paperclip-runner/runner/crates", "packages/paperclip-runner/scripts/acpx-sidecar-contract.mjs", "packages/paperclip-runner/scripts/build-provider-pack.mjs", + "packages/paperclip-runner/scripts/candidate-provider-pack.mjs", "packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs", "packages/paperclip-runner/scripts/generate-acpx-sidecar-contract.mjs", "packages/paperclip-runner/scripts/generate-protocol-schema-module.mjs", @@ -65,6 +66,17 @@ export interface DaytonaImageContentOptions { platform?: string; baseImages?: readonly string[]; frontendDigest?: string; + /** Must match the Docker PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS build argument. */ + candidateProviders?: readonly string[]; +} + +export function normalizedDaytonaCandidateProviders(values: readonly string[]): string[] { + const selected = [...values].sort(); + if (selected.some(value => !["cursor", "copilot", "pi"].includes(value)) + || new Set(selected).size !== selected.length) { + throw new Error("Daytona candidate providers must be distinct known ACP profiles"); + } + return selected; } function compareNames(left: string, right: string): number { @@ -262,6 +274,9 @@ export async function computeDaytonaImageContentId( for (const baseImage of await resolveBaseImages(root, options.baseImages)) { updateRecord(hash, "base-image", baseImage); } + for (const candidate of normalizedDaytonaCandidateProviders(options.candidateProviders ?? [])) { + updateRecord(hash, "candidate-provider", candidate); + } for (const inputPath of inputPaths) { await hashEntry(hash, root, inputPath); } @@ -270,7 +285,13 @@ export async function computeDaytonaImageContentId( const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null; if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) { - computeDaytonaImageContentId() + const arguments_ = process.argv.slice(2); + const candidateFlag = arguments_[0]; + if (arguments_.length > 1 || (candidateFlag !== undefined && !candidateFlag.startsWith("--candidate-providers="))) { + throw new Error("Expected only --candidate-providers=cursor,copilot,pi"); + } + const candidateProviders = candidateFlag?.slice("--candidate-providers=".length).split(",").filter(Boolean) ?? []; + computeDaytonaImageContentId({ candidateProviders }) .then((contentId) => process.stdout.write(`${contentId}\n`)) .catch((error: unknown) => { process.stderr.write( diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 1588319845e..15865f126b5 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -166,6 +166,7 @@ describe("runner E2E Daytona image contract", () => { "patches", "packages/paperclip-eval-kernel/src", "packages/paperclip-runner/package.json", + "packages/paperclip-runner/scripts/candidate-provider-pack.mjs", "packages/paperclip-runner/runner/crates", "packages/paperclip-runner/src", ]) { @@ -242,6 +243,14 @@ describe("runner E2E Daytona image contract", () => { 'pub const VERSION: &str = "one";\n', ); const baseline = await computeDaytonaImageContentId(options); + const candidate = await computeDaytonaImageContentId({ ...options, candidateProviders: ["pi"] }); + expect(candidate).not.toBe(baseline); + expect(await computeDaytonaImageContentId({ ...options, candidateProviders: ["copilot", "pi"] })) + .toBe(await computeDaytonaImageContentId({ ...options, candidateProviders: ["pi", "copilot"] })); + await expect(computeDaytonaImageContentId({ ...options, candidateProviders: ["pi", "pi"] })) + .rejects.toThrow("distinct known"); + await expect(computeDaytonaImageContentId({ ...options, candidateProviders: ["unknown"] })) + .rejects.toThrow("distinct known"); expect( await computeDaytonaImageContentId({ ...options, diff --git a/ui/src/adapters/codex-local/config-fields.tsx b/ui/src/adapters/codex-local/config-fields.tsx index 6a315d52265..8a83098b504 100644 --- a/ui/src/adapters/codex-local/config-fields.tsx +++ b/ui/src/adapters/codex-local/config-fields.tsx @@ -240,7 +240,7 @@ export function CodexLocalConfigFields({ {runnerManaged && runnerProvider === "acpx" && (