From 56cf3dbb698aa7785680fba8c1bf25be97de0159 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 9 Sep 2026 16:35:37 -0500 Subject: [PATCH] fix(runner): preserve provider root mode across Docker export --- docker/daytona-runner/Dockerfile | 4 +++- packages/paperclip-runner/scripts/build-provider-pack.mjs | 4 ++-- .../paperclip-runner/scripts/build-provider-pack.test.mjs | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index d0e0e92406..66d14c8808 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -44,7 +44,9 @@ RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \ # The trusted qualification entry exports the identical canonical provider stage. FROM scratch AS provider-pack-export -COPY --from=provider-pack-build /provider-pack / +# Keep the pack beneath the exporter-owned outer directory: BuildKit gives +# that directory host-local permissions rather than the source root's mode. +COPY --from=provider-pack-build /provider-pack /provider-pack # Fleet sandbox base image. Keep this section aligned with # paperclipai/paperclip-cloud/fleet-sandbox-image/Dockerfile. The only Paperclip diff --git a/packages/paperclip-runner/scripts/build-provider-pack.mjs b/packages/paperclip-runner/scripts/build-provider-pack.mjs index f523ecf86a..0f7696497c 100644 --- a/packages/paperclip-runner/scripts/build-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/build-provider-pack.mjs @@ -48,12 +48,12 @@ export function buildProviderPack({ workspaceRoot = defaultWorkspaceRoot, output // Same filesystem as OUTPUT, so successful publication is an atomic rename. // Docker reads its inputs before this empty directory receives exported files. const temporaryParent = mkdtempSync(join(dirname(outputRoot), ".paperclip-provider-pack-")); - const exported = join(temporaryParent, "exported"), backup = join(temporaryParent, "previous"); + const exportRoot = join(temporaryParent, "exported"), exported = join(exportRoot, "provider-pack"), backup = join(temporaryParent, "previous"); let previousMoved = false, published = false; try { const build = run("docker", ["build", "--platform", "linux/amd64", "--target", "provider-pack-export", "--file", dockerfile, "--build-arg", `PAPERCLIP_RUNNER_SOURCE_REVISION=${revision}`, - "--output", `type=local,dest=${exported}`, workspaceRoot], + "--output", `type=local,dest=${exportRoot}`, workspaceRoot], { cwd: workspaceRoot, stdio: "inherit", timeout: 12 * 60 * 1000, killSignal: "SIGTERM" }); if (build.error || build.status !== 0) throw new Error("Canonical provider-pack build failed"); const manifest = verifyProviderPack(exported, { revision, lockSha256: expectedLock }); diff --git a/packages/paperclip-runner/scripts/build-provider-pack.test.mjs b/packages/paperclip-runner/scripts/build-provider-pack.test.mjs index 2b64ee2858..27eae7e652 100644 --- a/packages/paperclip-runner/scripts/build-provider-pack.test.mjs +++ b/packages/paperclip-runner/scripts/build-provider-pack.test.mjs @@ -19,7 +19,12 @@ function fixture(runTest) { writeFileSync(join(workspaceRoot, "pnpm-lock.yaml"), "different CI resolution"); const calls = []; function exported(args, tamper) { - const destination = args[args.indexOf("--output") + 1].slice("type=local,dest=".length); + const exportRoot = args[args.indexOf("--output") + 1].slice("type=local,dest=".length); + // BuildKit's local exporter owns the outer directory and creates it 0700. + // The nested pack must retain the mode bound in its integrity manifest. + mkdirSync(exportRoot, { recursive: true, mode: 0o700 }); + chmodSync(exportRoot, 0o700); + const destination = join(exportRoot, "provider-pack"); const paths = { nodeCommand: "node_modules/node/bin/node", productionLock: "pnpm-lock.yaml", opencodeCommand: "node_modules/.bin/opencode", opencodeExecutable: "node_modules/opencode-ai/bin/opencode.exe", @@ -125,6 +130,7 @@ for (const [name, tamper] of [ ["extra dependency", root => writeFileSync(join(root, "node_modules/unexpected"), "extra")], ["lost executable mode", root => chmodSync(join(root, "node_modules/pi/vendor/pi"), 0o644)], ["directory mode", root => chmodSync(join(root, "node_modules/pi/vendor"), 0o700)], + ["pack root mode", root => chmodSync(root, 0o700)], ["changed internal symlink", root => { unlinkSync(join(root, "node_modules/pi-link")); symlinkSync(".bin/pi", join(root, "node_modules/pi-link")); }], ["escaping symlink", root => { unlinkSync(join(root, "node_modules/pi-link")); symlinkSync("../../", join(root, "node_modules/pi-link")); }], ["absolute symlink", root => { unlinkSync(join(root, "node_modules/pi-link")); symlinkSync(join(root, "node_modules/pi/vendor/pi"), join(root, "node_modules/pi-link")); }],