paperclip/packages/paperclip-runner/scripts/verify-pi-provider-launch.mjs

79 lines
4.0 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
// Runs inside the Linux image build against the actual deployed provider pack.
// An ACP session starts the pinned Pi RPC child without making a model request.
const pack = resolve(process.argv[2]);
const { createAcpxPackageJsonResolver, verifyQualifiedAcpxInstallation } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/installation-integrity.js")));
const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/qualified-profiles.js")));
const root = await mkdtemp(join(tmpdir(), "pi-qualified-launch-"));
const installation = await verifyQualifiedAcpxInstallation(
resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731"),
createAcpxPackageJsonResolver(pack),
);
const lease = await installation.openCommand();
const child = lease.spawn([], { cwd: root, detached: true, env: {
HOME: root, PATH: "/usr/bin:/bin", PI_CODING_AGENT_DIR: join(root, ".pi"),
// Makes the static OpenRouter model catalog selectable; never sends a prompt.
OPENROUTER_API_KEY: "qualification-no-model-requests",
} });
// Register before requesting a session so even an early close is observed.
let childDidClose = false;
const childClosed = new Promise((resolveClose) => child.once("close", () => { childDidClose = true; resolveClose(); }));
let buffer = "", stderr = "";
const pending = new Map();
child.stderr.on("data", (data) => { stderr = (stderr + data).slice(-4000); });
child.stdout.on("data", (data) => {
buffer += data;
assert(buffer.length < 1024 * 1024, "Provider protocol output exceeded bound");
let end;
while ((end = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
if (!line.trim()) continue;
const response = JSON.parse(line);
pending.get(response.id)?.(response);
}
});
const request = (id, method, params) => new Promise((resolveRequest, reject) => {
const timer = setTimeout(() => reject(new Error(`${method} timed out: ${stderr}`)), 30_000);
pending.set(id, (response) => { clearTimeout(timer); pending.delete(id); resolveRequest(response); });
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
try {
const initialize = await request(1, "initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "paperclip-image-qualification", version: "1" } });
assert.equal(initialize.error, undefined, JSON.stringify(initialize.error));
const session = await request(2, "session/new", { cwd: root, mcpServers: [] });
assert.equal(session.error, undefined, JSON.stringify(session.error));
assert.equal(typeof session.result?.sessionId, "string");
console.log("Verified Pi ACP and pinned RPC runtime started successfully");
} finally {
// Pi can still write its private home while handling SIGTERM. Keep the
// temporary directory until its pipes close, with a bounded group teardown.
let forceTimer, deadlineTimer;
const shutdownDeadline = new Promise((_, reject) => {
forceTimer = setTimeout(() => {
try { if (!childDidClose) process.kill(-child.pid, "SIGKILL"); }
catch (error) { if (error.code !== "ESRCH") reject(error); }
}, 5_000);
deadlineTimer = setTimeout(() => reject(new Error("Pi qualification process did not close after SIGKILL")), 10_000);
});
try {
try { if (!childDidClose) process.kill(-child.pid, "SIGTERM"); }
catch (error) { if (error.code !== "ESRCH") throw error; }
await Promise.race([childClosed, shutdownDeadline]);
} finally {
clearTimeout(forceTimer);
clearTimeout(deadlineTimer);
try {
await lease.close();
} finally {
// A failed verifier aborts this disposable image-build step. Remove its
// temporary HOME even when shutdown or lease cleanup reports a failure.
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
}
}