fix(evals): scaffold preview plugins through the owned source CLI

This commit is contained in:
Dotta committed 2026-10-27 08:35:37 -05:00
1 parent 23ca3f56a5
commit e29c9cce65
7 files changed
+133 -9

No files matched your search

@@ -1,10 +1,11 @@
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
import { mkdtemp, mkdir, writeFile, readFile, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { mirrorPreviewSdk } from "./external-plugin-sdk-mirror.js";
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
import type { PaperclipPluginManifestV1 } from "../../../packages/shared/src/types/plugin.js";
@@ -17,15 +18,17 @@ export function previewFixtureManifest(key: string): PaperclipPluginManifestV1 {
tools: [{ name: "preview", displayName: "Preview website", description: "Read the current synthetic website preview", parametersSchema: { type: "object", properties: {}, additionalProperties: false } }],
};
}
/** Runtime-only controlled package. CLI scaffolding and package admission happen
* during the coordinator's selected attempt, never during discovery/unit tests. */
/** CLI scaffolding and package admission run during the selected attempt.
* The explicit offline calibration exercises scaffolding without admission. */
export async function createPreviewFixture(ctx: ChatCatalogContext) {
const temporary = await mkdtemp(path.join(tmpdir(), "paperclip-preview-eval-"));
const temporary = await realpath(await mkdtemp(path.join(tmpdir(), "paperclip-preview-eval-")));
const packageRoot = path.join(temporary, "website-preview");
const cli = fileURLToPath(new URL("../../../cli/dist/index.js", import.meta.url));
const cli = fileURLToPath(new URL("../../../cli/src/index.ts", import.meta.url));
const loader = fileURLToPath(new URL("../../../cli/node_modules/tsx/dist/loader.mjs", import.meta.url));
const sdkUrl = new URL("../../../packages/plugins/sdk/dist/index.js", import.meta.url).href;
try {
await promisify(execFile)(process.execPath, [cli, "plugin", "init", "@paperclip-eval/website-preview", "--output", temporary, "--display-name", "Website Preview Eval"], { timeout: 30_000, maxBuffer: 256_000, env: { PATH: process.env.PATH ?? "", NODE_ENV: "test", DO_NOT_TRACK: "1" } });
const mirror = await mirrorPreviewSdk(fileURLToPath(new URL("../../../packages/plugins/sdk/", import.meta.url)), temporary);
await promisify(execFile)(process.execPath, ["--import", loader, cli, "plugin", "init", "@paperclip-eval/website-preview", "--output", temporary, "--display-name", "Website Preview Eval", "--sdk-path", mirror.sdkPath], { cwd: temporary, timeout: 30_000, maxBuffer: 256_000, env: { HOME: temporary, PATH: process.env.PATH ?? "", NODE_ENV: "test", DO_NOT_TRACK: "1", PAPERCLIP_TELEMETRY_DISABLED: "1" } });
const key = `paperclip.eval-preview-${ctx.input.nonce.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
const manifest = previewFixtureManifest(key);
const marker = `Synthetic website preview ${ctx.input.nonce}`;
@@ -43,7 +46,7 @@ const plugin=definePlugin({
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@paperclip-eval/website-preview", version: "0.1.0", type: "module", private: true, paperclipPlugin: { manifest: "./dist/manifest.js", worker: "./dist/worker.js" } }));
await writeFile(path.join(packageRoot, "dist/manifest.js"), manifestSource); await writeFile(path.join(packageRoot, "dist/worker.js"), workerSource);
const digest = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex");
await ctx.input.evidence("chat-catalog-vetted-preview-package.json", { synthetic: true, pluginKey: key, manifestSha256: digest(manifestSource), workerSha256: digest(workerSource), cliSha256: digest(await readFile(cli)), sdkEntrySha256: digest(await readFile(fileURLToPath(sdkUrl))) });
await ctx.input.evidence("chat-catalog-vetted-preview-package.json", { synthetic: true, cliMode: "supported-source-cli-tsx", cliLoaderSha256: digest(await readFile(loader)), pluginKey: key, sdkMirror: { files: mirror.files, archiveWrites: mirror.archiveWrites, packageBuilds: mirror.packageBuilds, packagePacks: mirror.packagePacks }, manifestSha256: digest(manifestSource), workerSha256: digest(workerSource), cliSha256: digest(await readFile(cli)), sdkEntrySha256: digest(await readFile(fileURLToPath(sdkUrl))) });
return { key, packageRoot, marker, async close() { await rm(temporary, { recursive: true, force: true }); } };
} catch { await rm(temporary, { recursive: true, force: true }); throw new Error("Disposable plugin scaffold failed; command output withheld"); }
}
@@ -0,0 +1,48 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { mkdtemp, mkdir, readFile, rm, writeFile, access } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
import { createPreviewFixture } from "./external-plugin-fixture.js";
it.skipIf(process.env.PAPERCLIP_CHAT_EVAL_PLUGIN_SCAFFOLD_CALIBRATION !== "1")("qualifies the actual source CLI scaffold with explicit owned SDK custody and no build or pack", async () => {
const root = await mkdtemp(path.join(tmpdir(), "plugin-cli-offline-control-"));
const previousPath = process.env.PATH;
let fixture: Awaited<ReturnType<typeof createPreviewFixture>> | undefined;
try {
const bin = path.join(root, "bin"); await mkdir(bin);
const called = path.join(root, "unexpected-pnpm-call");
await writeFile(path.join(bin, "pnpm"), `#!/bin/sh\nprintf invoked > '${called}'\nexit 91\n`, { mode: 0o700 });
const cli = fileURLToPath(new URL("../../../cli/dist/index.js", import.meta.url));
const sourceSdk = fileURLToPath(new URL("../../../packages/plugins/sdk/", import.meta.url));
const hash = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
const cliBefore = hash(await readFile(cli));
const original = { failed: false, missingCheckoutDependency: false };
try {
await promisify(execFile)(process.execPath, [cli, "plugin", "init", "@paperclip-eval/default-sdk-control", "--output", root], {
cwd: root, timeout: 30_000, maxBuffer: 256_000, env: { HOME: root, PATH: bin, NODE_ENV: "test", DO_NOT_TRACK: "1", PAPERCLIP_TELEMETRY_DISABLED: "1" },
});
} catch (error) {
original.failed = true;
original.missingCheckoutDependency = /ERR_MODULE_NOT_FOUND/.test(String((error as { stderr?: string }).stderr)) && /Cannot find package 'zod'/.test(String((error as { stderr?: string }).stderr));
}
expect(original).toEqual({ failed: true, missingCheckoutDependency: true });
let evidence: { sdkMirror: { files: Array<{ path: string; sha256: string }> } } | undefined;
process.env.PATH = bin;
const ctx = { input: { nonce: "offline-cli-calibration", evidence: async (_name: string, value: unknown) => { evidence = value as typeof evidence; } } } as unknown as ChatCatalogContext;
fixture = await createPreviewFixture(ctx);
expect(JSON.parse(await readFile(path.join(fixture.packageRoot, "package.json"), "utf8"))).toMatchObject({ name: "@paperclip-eval/website-preview" });
expect(evidence?.sdkMirror.files.length).toBeGreaterThan(1);
for (const file of evidence!.sdkMirror.files) expect(hash(await readFile(path.join(sourceSdk, file.path)))).toBe(file.sha256);
expect(hash(await readFile(cli))).toBe(cliBefore);
await expect(access(called)).rejects.toThrow();
} finally {
if (previousPath === undefined) delete process.env.PATH; else process.env.PATH = previousPath;
await fixture?.close(); await rm(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,29 @@
import { mkdtemp, mkdir, readFile, rm, realpath, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { mirrorPreviewSdk } from "./external-plugin-sdk-mirror.js";
it("retains exact public SDK bytes in a separately owned scaffold workspace", async () => {
const root = await mkdtemp(path.join(tmpdir(), "sdk-custody-control-"));
try {
const source = path.join(root, "source"); await mkdir(path.join(source, "dist"), { recursive: true });
const manifest = JSON.stringify({ name: "@paperclipai/plugin-sdk", exports: { ".": { import: "./dist/index.js" } } });
await writeFile(path.join(source, "package.json"), manifest); await writeFile(path.join(source, "dist/index.js"), "export const publicSdk = true;\n");
const owned = path.join(root, "owned"); await mkdir(owned);
await expect(mirrorPreviewSdk(source, source)).rejects.toThrow("separately owned");
const result = await mirrorPreviewSdk(source, owned);
expect(result.sdkPath).toBe(path.join(await realpath(owned), "packages/plugins/sdk"));
expect(result.archiveWrites).toBe(false); expect(result.packageBuilds).toBe(0); expect(result.packagePacks).toBe(0);
for (const file of result.files) {
const original = await readFile(path.join(source, file.path));
expect(await readFile(path.join(result.sdkPath, file.path))).toEqual(original);
expect(file.sha256).toBe(createHash("sha256").update(original).digest("hex"));
}
await expect(mirrorPreviewSdk(source, owned)).rejects.toThrow();
const other = path.join(root, "other"); await mkdir(other);
await symlink(path.join(source, "package.json"), path.join(source, "dist/escape.js"));
await expect(mirrorPreviewSdk(source, other)).rejects.toThrow("symbolic links");
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -0,0 +1,41 @@
import { createHash } from "node:crypto";
import { lstat, mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises";
import path from "node:path";
/** Copy genuine SDK package/dist bytes into the owned scaffold workspace. The
* public CLI can then use its workspace SDK route without packing the archive. */
export async function mirrorPreviewSdk(source: string, ownedRoot: string) {
const root = await realpath(ownedRoot), sourceRoot = await realpath(source);
if (sourceRoot === root || root.startsWith(sourceRoot + path.sep) || sourceRoot.startsWith(root + path.sep))
throw new Error("SDK mirror requires a separately owned output root");
const sdkPath = path.join(root, "packages/plugins/sdk");
await mkdir(sdkPath, { recursive: true });
const files: Array<{ path: string; sha256: string; byteSize: number }> = [];
let totalBytes = 0;
async function copy(relative: string): Promise<void> {
const sourcePath = path.join(sourceRoot, relative), targetPath = path.join(sdkPath, relative);
const stat = await lstat(sourcePath);
if (stat.isSymbolicLink()) throw new Error("SDK mirror rejects symbolic links");
if (stat.isDirectory()) {
await mkdir(targetPath, { recursive: true });
for (const name of (await readdir(sourcePath)).sort()) await copy(path.join(relative, name));
return;
}
if (!stat.isFile() || files.length >= 512 || stat.size > 4_194_304 || totalBytes + stat.size > 33_554_432)
throw new Error("SDK mirror exceeds its bounded public package/dist inventory");
const bytes = await readFile(sourcePath);
await writeFile(targetPath, bytes, { flag: "wx", mode: 0o600 });
const hash = (body: Buffer) => createHash("sha256").update(body).digest("hex");
const sha256 = hash(bytes);
if (hash(await readFile(targetPath)) !== sha256) throw new Error("SDK mirror byte custody mismatch");
totalBytes += bytes.length;
files.push({ path: relative, sha256, byteSize: bytes.length });
}
await copy("package.json");
const manifest = JSON.parse(await readFile(path.join(sdkPath, "package.json"), "utf8"));
if (manifest.name !== "@paperclipai/plugin-sdk" || manifest.exports?.["."]?.import !== "./dist/index.js")
throw new Error("SDK mirror is not the actual public plugin SDK package");
await copy("dist");
if (!files.some(file => file.path === path.join("dist", "index.js"))) throw new Error("SDK mirror has no real compiled entrypoint");
return { sdkPath, files, archiveWrites: false as const, packageBuilds: 0 as const, packagePacks: 0 as const };
}
@@ -321,6 +321,9 @@ export const secretDeliveryExternalCases = [
"../../server/src/services/heartbeat/run-preparation.ts",
"../../cli/dist/index.js",
"../../cli/src/index.ts",
"../../cli/node_modules/tsx/dist/loader.mjs",
"chat-catalog-domains/external-plugin-sdk-mirror.ts",
"../../packages/plugins/sdk/package.json",
"../../packages/plugins/create-paperclip-plugin/src/index.ts",
"../../packages/plugins/sdk/src/index.ts",
"../../packages/plugins/sdk/dist/index.js",
@@ -77,7 +77,7 @@ export const runtimeFailedRunCases = [runtimeDefinition("runs-adapters.inspect-f
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });
return { worker, task: done.task, ...failure, successfulRun, successArtifactVerified: true, failureLogAvailable };
}, gradeFailedAttempt, {
sourceFiles: ["chat-catalog-domains/runtime-failed-runs.ts", "chat-catalog-domains/runtime-native-failure.ts", "chat-catalog-domains/runtime-support.ts", "chat-scenarios/runs-adapters.yaml"],
sourceFiles: ["chat-catalog-domains/runtime-failed-runs.ts", "chat-catalog-domains/runtime-native-failure.ts", "chat-catalog-domains/runtime-support.ts", "chat-scenarios/runs-adapters.yaml", "../../ui/src/components/TaskChatThread.tsx", "../../ui/src/components/task-chat/TaskChatMarker.tsx", "../../ui/src/components/task-chat/TaskChatThreadView.tsx", "../../ui/src/pages/AgentDetail.tsx"],
expectedRunCount: 3, attemptTimeoutMs: 720_000,
runPolicy: { kind: "terminal-counts", terminalStatusCounts: { failed: 1, succeeded: 2 } },
async observe(ctx, state) {
@@ -41,7 +41,7 @@ export const runtimeSessionCases = [runtimeDefinition("runs-adapters.reset-sessi
for (const doc of documents) revisions.push(...await ctx.input.api.get<Entity[]>(`/api/issues/${state.task.id}/documents/${encodeURIComponent(doc.key!)}/revisions`));
return { ...state, priorComments: comments, priorDocuments: documents, priorRevisions: revisions };
}, gradeSessionReset, {
sourceFiles: ["chat-catalog-domains/runtime-session.ts", "chat-catalog-domains/runtime-workspaces.ts", "chat-catalog-domains/runtime-support.ts", "chat-scenarios/runs-adapters.yaml"],
sourceFiles: ["chat-catalog-domains/runtime-session.ts", "chat-catalog-domains/runtime-workspaces.ts", "chat-catalog-domains/runtime-support.ts", "chat-scenarios/runs-adapters.yaml", "../../ui/src/components/AgentActionButtons.tsx", "../../ui/src/pages/AgentDetail.tsx"],
expectedRunCount: 3, attemptTimeoutMs: 720_000,
async act(ctx, state) {
await pollUntil({ label: "Canonical session guidance settled", deadlineAt: Date.now() + 150_000,