diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index 294b5b1292..d5157e9d1c 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -20,8 +20,27 @@ WORKDIR /workspace COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc tsconfig.base.json ./ COPY patches ./patches COPY scripts/link-plugin-dev-sdk.mjs ./scripts/link-plugin-dev-sdk.mjs -COPY packages/paperclip-eval-kernel ./packages/paperclip-eval-kernel -COPY packages/paperclip-runner ./packages/paperclip-runner +COPY \ + packages/paperclip-eval-kernel/package.json \ + packages/paperclip-eval-kernel/tsconfig.json \ + ./packages/paperclip-eval-kernel/ +COPY packages/paperclip-eval-kernel/src ./packages/paperclip-eval-kernel/src +COPY \ + packages/paperclip-runner/package.json \ + packages/paperclip-runner/styles.css \ + packages/paperclip-runner/tsconfig.json \ + packages/paperclip-runner/tsconfig.surfaces.json \ + ./packages/paperclip-runner/ +COPY packages/paperclip-runner/protocol ./packages/paperclip-runner/protocol +COPY packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_sidecar_contract.rs ./packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_sidecar_contract.rs +COPY \ + packages/paperclip-runner/scripts/acpx-sidecar-contract.mjs \ + packages/paperclip-runner/scripts/build-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 \ + ./packages/paperclip-runner/scripts/ +COPY packages/paperclip-runner/src ./packages/paperclip-runner/src RUN pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...' RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \ && PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_RUNNER_SOURCE_REVISION}" \ diff --git a/tests/runner-e2e/daytona-image-content.ts b/tests/runner-e2e/daytona-image-content.ts index d29cc3e601..23a9a5dca8 100644 --- a/tests/runner-e2e/daytona-image-content.ts +++ b/tests/runner-e2e/daytona-image-content.ts @@ -6,13 +6,14 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const repositoryRoot = path.resolve(import.meta.dirname, "../.."); export const DAYTONA_IMAGE_CONTENT_SCHEMA = - "paperclip-daytona-runner-image-content/v4"; + "paperclip-daytona-runner-image-content/v5"; export const DAYTONA_IMAGE_PLATFORM = "linux/amd64"; export const DAYTONA_IMAGE_DOCKERFILE_PATH = "docker/daytona-runner/Dockerfile"; -// This is the audited dependency closure of docker/daytona-runner/Dockerfile. -// Keep it conservative: a false positive only rebuilds the image, while a -// missing input could incorrectly reuse an incompatible paid-test image. +// This mirrors the explicit repository-local build inputs copied by +// docker/daytona-runner/Dockerfile. Broad package-tree COPYs are forbidden by +// the contract test so development-only files cannot silently enter the image +// without first changing this content-identity contract. export const DAYTONA_IMAGE_INPUT_PATHS = [ ".dockerignore", ".npmrc", @@ -23,20 +24,25 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [ "pnpm-workspace.yaml", "scripts/link-plugin-dev-sdk.mjs", "tsconfig.base.json", - "packages/paperclip-eval-kernel", - "packages/paperclip-runner", + "packages/paperclip-eval-kernel/package.json", + "packages/paperclip-eval-kernel/src", + "packages/paperclip-eval-kernel/tsconfig.json", + "packages/paperclip-runner/package.json", + "packages/paperclip-runner/protocol", + "packages/paperclip-runner/runner/Cargo.lock", + "packages/paperclip-runner/runner/Cargo.toml", + "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/build-verified-provider-entrypoints.mjs", + "packages/paperclip-runner/scripts/generate-acpx-sidecar-contract.mjs", + "packages/paperclip-runner/scripts/generate-protocol-schema-module.mjs", + "packages/paperclip-runner/src", + "packages/paperclip-runner/styles.css", + "packages/paperclip-runner/tsconfig.json", + "packages/paperclip-runner/tsconfig.surfaces.json", ] as const; -const ignoredGeneratedDirectoryPaths = new Set([ - "packages/paperclip-runner/dist", - "packages/paperclip-runner/runner/target", -]); - -// These paths do not contribute to the release runnerd binary or the -// executable/digested provider-pack runtime payload. They are also excluded -// from the real Docker build context by .dockerignore. Keep the two lists in -// lockstep: if a future build starts consuming one of these inputs, Docker must -// fail instead of publishing bytes that the content identity did not hash. const ignoredRunnerDevelopmentDirectoryPaths = new Set([ "packages/paperclip-runner/devtools", "packages/paperclip-runner/docs", @@ -70,7 +76,6 @@ function normalizedRelativePath(value: string): string { } function shouldIgnore(relativePath: string): boolean { - if (ignoredGeneratedDirectoryPaths.has(relativePath)) return true; return relativePath.split("/").includes("node_modules"); } @@ -213,7 +218,6 @@ async function hashEntry( const stats = await lstat(absolutePath); if (stats.isDirectory()) { - updateRecord(hash, "directory", normalizedPath); const entries = await readdir(absolutePath, { withFileTypes: true }); entries.sort((left, right) => compareNames(left.name, right.name)); for (const entry of entries) { diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 09e19b65ff..ef567ddf28 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -28,8 +28,21 @@ describe("runner E2E Daytona image contract", () => { "utf8", ), ]); + const normalizedDockerfile = dockerfile.replace(/\\\r?\n\s*/g, " "); expect(dockerfile).toContain("--bin paperclip-runnerd"); expect(dockerfile).toContain("build-provider-pack.mjs /provider-pack"); + expect(normalizedDockerfile).not.toContain( + "COPY packages/paperclip-eval-kernel ./packages/paperclip-eval-kernel", + ); + expect(normalizedDockerfile).not.toContain( + "COPY packages/paperclip-runner ./packages/paperclip-runner", + ); + expect(dockerfile).toContain( + "COPY packages/paperclip-eval-kernel/src ./packages/paperclip-eval-kernel/src", + ); + expect(dockerfile).toContain( + "COPY packages/paperclip-runner/src ./packages/paperclip-runner/src", + ); expect(dockerfile).toContain( "/opt/paperclip-runner/provider-pack/provider-pack.json", ); @@ -119,11 +132,19 @@ describe("runner E2E Daytona image contract", () => { "docker/daytona-runner/Dockerfile", "pnpm-lock.yaml", "patches", - "packages/paperclip-eval-kernel", - "packages/paperclip-runner", + "packages/paperclip-eval-kernel/src", + "packages/paperclip-runner/package.json", + "packages/paperclip-runner/runner/crates", + "packages/paperclip-runner/src", ]) { expect(DAYTONA_IMAGE_INPUT_PATHS).toContain(requiredPath); } + expect(DAYTONA_IMAGE_INPUT_PATHS).not.toContain( + "packages/paperclip-eval-kernel", + ); + expect(DAYTONA_IMAGE_INPUT_PATHS).not.toContain( + "packages/paperclip-runner", + ); expect(DAYTONA_IMAGE_DOCKERFILE_PATH).toBe( "docker/daytona-runner/Dockerfile", ); @@ -132,34 +153,69 @@ describe("runner E2E Daytona image contract", () => { expect(contentId).toMatch(/^[0-9a-f]{64}$/); }); - it("changes only when an image input, frontend, base, or platform changes", async () => { + it("changes for runtime source, package, lockfile, Dockerfile, frontend, base, or platform inputs", async () => { const root = await mkdtemp( path.join(tmpdir(), "paperclip-daytona-image-id-"), ); + const inputPaths = [ + "docker/daytona-runner/Dockerfile", + "package.json", + "pnpm-lock.yaml", + "packages/paperclip-runner/package.json", + "packages/paperclip-runner/src", + "packages/paperclip-runner/runner/crates", + ] as const; + const options = { + repositoryRoot: root, + inputPaths, + baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], + frontendDigest: `sha256:${"c".repeat(64)}`, + } as const; try { - await mkdir(path.join(root, "image-input")); + await mkdir(path.join(root, "docker/daytona-runner"), { + recursive: true, + }); + await mkdir(path.join(root, "packages/paperclip-runner/src"), { + recursive: true, + }); + await mkdir( + path.join( + root, + "packages/paperclip-runner/runner/crates/runner-core/src", + ), + { recursive: true }, + ); await writeFile( - path.join(root, "image-input", "runner.ts"), + path.join(root, "docker/daytona-runner/Dockerfile"), + "FROM pinned\n", + ); + await writeFile(path.join(root, "package.json"), '{"private":true}\n'); + await writeFile(path.join(root, "pnpm-lock.yaml"), "lockfileVersion: 9\n"); + await writeFile( + path.join(root, "packages/paperclip-runner/package.json"), + '{"name":"@paperclipai/paperclip-runner"}\n', + ); + await writeFile( + path.join(root, "packages/paperclip-runner/src/runner.ts"), "version one\n", ); - const baseline = await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], - baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], - frontendDigest: `sha256:${"c".repeat(64)}`, - }); + await writeFile( + path.join( + root, + "packages/paperclip-runner/runner/crates/runner-core/src/lib.rs", + ), + 'pub const VERSION: &str = "one";\n', + ); + const baseline = await computeDaytonaImageContentId(options); expect( await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], + ...options, baseImages: [`example.test/base:1@sha256:${"b".repeat(64)}`], - frontendDigest: `sha256:${"c".repeat(64)}`, }), ).not.toBe(baseline); expect( await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], + ...options, baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], frontendDigest: `sha256:${"d".repeat(64)}`, }), @@ -170,33 +226,27 @@ describe("runner E2E Daytona image contract", () => { "does not enter the image\n", ); expect( - await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], - baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], - frontendDigest: `sha256:${"c".repeat(64)}`, - }), + await computeDaytonaImageContentId(options), ).toBe(baseline); - await writeFile( - path.join(root, "image-input", "runner.ts"), - "version two\n", - ); + for (const relativePath of [ + "docker/daytona-runner/Dockerfile", + "package.json", + "pnpm-lock.yaml", + "packages/paperclip-runner/package.json", + "packages/paperclip-runner/src/runner.ts", + "packages/paperclip-runner/runner/crates/runner-core/src/lib.rs", + ]) { + const absolutePath = path.join(root, relativePath); + const original = await readFile(absolutePath, "utf8"); + await writeFile(absolutePath, `${original}changed\n`); + expect(await computeDaytonaImageContentId(options)).not.toBe(baseline); + await writeFile(absolutePath, original); + } expect( await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], - baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], - frontendDigest: `sha256:${"c".repeat(64)}`, - }), - ).not.toBe(baseline); - expect( - await computeDaytonaImageContentId({ - repositoryRoot: root, - inputPaths: ["image-input"], + ...options, platform: "linux/arm64", - baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`], - frontendDigest: `sha256:${"c".repeat(64)}`, }), ).not.toBe(baseline); } finally { @@ -262,6 +312,14 @@ describe("runner E2E Daytona image contract", () => { ); const baseline = await computeDaytonaImageContentId(options); + await mkdir(path.join(runnerRoot, "src/new-test-only-directory")); + await writeFile( + path.join( + runnerRoot, + "src/new-test-only-directory/transport-edge.test.ts", + ), + "new TypeScript test\n", + ); await writeFile(path.join(runnerRoot, "README.md"), "second readme\n"); await writeFile( path.join(runnerRoot, "docs/local-runner.md"),