From 6f7e2d20d96ca58b76c28c7dcc69bf53b9d1a93d Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 12:10:49 -0500 Subject: [PATCH] Audit Copilot provider packs through verified offline launch Hash the exact Copilot builder and materializer into Daytona image identity, and add a credential-free initialize smoke that exercises the packaged installation registry and native snapshot launch. Co-Authored-By: Paperclip --- .../scripts/copilot-provider-pack-smoke.mjs | 104 ++++++++++++++++++ tests/runner-e2e/daytona-image-content.ts | 2 + tests/runner-e2e/daytona-image.test.ts | 2 + 3 files changed, 108 insertions(+) create mode 100644 packages/paperclip-runner/scripts/copilot-provider-pack-smoke.mjs diff --git a/packages/paperclip-runner/scripts/copilot-provider-pack-smoke.mjs b/packages/paperclip-runner/scripts/copilot-provider-pack-smoke.mjs new file mode 100644 index 0000000000..eec37a8692 --- /dev/null +++ b/packages/paperclip-runner/scripts/copilot-provider-pack-smoke.mjs @@ -0,0 +1,104 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { once } from "node:events"; +import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +// Build verification only. No prompt, credential, model request, or runtime +// installation occurs; the provider runs offline with isolated state. +const pack = await realpath(resolve(process.argv[2] ?? "provider-pack")); +const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "utf8")); +// Bind the audited pack explicitly, just as the verified sidecar launcher does. +process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = pack; +process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = join(pack, "package.json"); +const candidate = manifest.payload.candidateProviders?.copilot; +assert.equal(candidate?.version, "1.0.88"); +assert.equal(candidate.qualification, "pending"); +const { verifyAcpxProfileInstallation } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/profile-installation.js"))); +const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/qualified-profiles.js"))); +const { COPILOT_ACP_CLIENT_CAPABILITIES } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/copilot-events.js"))); +// This explicit value exercises profile admission only; no model is selected +// or used by initialize, and this is not a claim of model availability. +const profile = resolveQualifiedAcpxProfile("copilot", "gpt-4.1"); +const installation = await verifyAcpxProfileInstallation(profile); +assert.equal(installation.commandDigest, candidate.profileDigest); +const lease = await installation.openCommand(); +const root = await mkdtemp(join(tmpdir(), "paperclip-copilot-pack-smoke-")); +let child; +let exited; +try { + const environment = { PATH: "/usr/bin:/bin", COPILOT_OFFLINE: "true", COPILOT_AUTO_UPDATE: "false", NO_COLOR: "1" }; + for (const name of ["HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) { + environment[name] = join(root, name.toLowerCase()); + await mkdir(environment[name], { mode: 0o700 }); + } + await writeFile(join(environment.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 }); + child = lease.spawn([], { cwd: root, env: environment, stdio: "pipe" }); + exited = once(child, "exit"); + const initialized = new Promise((done, reject) => { + let buffer = ""; + let total = 0; + const timeout = setTimeout(() => reject(new Error("Copilot ACP initialize timed out")), 20_000); + timeout.unref(); + child.once("error", reject); + child.once("exit", () => reject(new Error("Copilot exited before initialize"))); + child.stdout.on("data", chunk => { + total += chunk.length; + if (total > 1024 * 1024) { reject(new Error("Copilot ACP output exceeded bound")); return; } + buffer += chunk.toString("utf8"); + while (buffer.includes("\n")) { + const newline = buffer.indexOf("\n"); + const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); + if (!line.trim()) continue; + let response; + try { response = JSON.parse(line); } catch { reject(new Error("Copilot emitted malformed ACP JSON")); return; } + if (response.id === 0 && response.method === undefined) { + clearTimeout(timeout); + if (response.error || !response.result) reject(new Error("Copilot rejected ACP initialize")); + else done(response.result); + } + } + }); + child.stderr.resume(); // Never persist diagnostics that may contain paths. + }); + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: { protocolVersion: 1, clientInfo: { name: "paperclip-pack-offline-smoke", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES } })}\n`); + const result = await initialized; + assert.equal(result.protocolVersion, 1); + assert.equal(result.agentInfo?.version, "1.0.88"); + child.stdin.end(); + const timer = setTimeout(() => child.kill("SIGTERM"), 1_000); + const [exitCode, signal] = await within(exited, 10_000); + clearTimeout(timer); + assert.equal(exitCode, 0, "Copilot did not exit cleanly after stdin EOF"); + assert.equal(signal, null); + const executable = await readFile(join(pack, candidate.path, "copilot")); + process.stdout.write(`${JSON.stringify({ + schema: "paperclip.copilot-provider-pack-smoke/v1", sourceRevision: manifest.payload.runnerSourceRevision, + providerPackDigest: manifest.digest, platform: process.platform, architecture: process.arch, + candidate, executableSha256: `sha256:${createHash("sha256").update(executable).digest("hex")}`, + registryLaunch: "verifyAcpxProfileInstallation/openCommand/spawn", initializeRequestId: 0, + initialize: result, cleanExit: true, inheritedCredentials: false, promptSent: false, + networkMode: "COPILOT_OFFLINE=true", costUsd: 0, + qualification: "pending: no credential, entitlement, model execution or Daytona", + }, null, 2)}\n`); +} finally { + if (child && child.exitCode === null && child.signalCode === null) { + child.stdin.end(); + child.kill("SIGTERM"); + await within(exited, 5_000); + } + await lease.close(); + await rm(root, { recursive: true, force: true }); +} + + +async function within(promise, timeoutMs) { + let timer; + try { + return await Promise.race([promise, new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("Copilot smoke cleanup was not confirmed")), timeoutMs); + })]); + } finally { clearTimeout(timer); } +} diff --git a/tests/runner-e2e/daytona-image-content.ts b/tests/runner-e2e/daytona-image-content.ts index 92ffbe1b70..c56d3c5ad1 100644 --- a/tests/runner-e2e/daytona-image-content.ts +++ b/tests/runner-e2e/daytona-image-content.ts @@ -36,6 +36,8 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [ "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-copilot-distribution.mjs", + "packages/paperclip-runner/scripts/materialize-copilot-binary.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", diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 997a8e5bd6..8bb5855f5f 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -200,6 +200,8 @@ describe("runner E2E Daytona image contract", () => { "packages/paperclip-eval-kernel/src", "packages/paperclip-runner/package.json", "packages/paperclip-runner/scripts/candidate-provider-pack.mjs", + "packages/paperclip-runner/scripts/build-copilot-distribution.mjs", + "packages/paperclip-runner/scripts/materialize-copilot-binary.mjs", "packages/paperclip-runner/runner/crates", "packages/paperclip-runner/src", ]) {