fix(runner): provision pinned Pi in published server installs

Add explicit host-only setup, verify the public server vendor layout, and route readiness through the packaged runner boundary. Preserve exact Pi closure and normal-mode admission checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-01 22:45:01 -05:00
1 parent c806b94084
commit c27bbcee25
18 files changed
+607 -39

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { Command } from "commander";
import { afterEach, expect, it } from "vitest";
import { registerRuntimeCommands, resolvePiProvisioner } from "../commands/runtime.js";
const roots: string[] = [];
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
async function fixture() {
const root = await mkdtemp(join(tmpdir(), "paperclip-cli-runtime-")); roots.push(root);
const cli = join(root, "dist/vendor/paperclip-runner/cli"); await mkdir(cli, { recursive: true });
await writeFile(join(root, "package.json"), '{"name":"@paperclipai/server"}');
await writeFile(join(root, "dist/index.js"), "throw new Error('server must not start during setup resolution')");
await writeFile(join(cli, "provision-pi.cjs"), "fixture");
return { root, cli, url: pathToFileURL(join(root, "dist/index.js")).href };
}
it("locates the public server's setup entrypoint without importing its API server", async () => {
const f = await fixture(); expect(await resolvePiProvisioner(f.url)).toBe(join(f.cli, "provision-pi.cjs"));
});
it("rejects foreign package identity and a setup entrypoint outside that package", async () => {
const f = await fixture(); const other = await fixture();
await writeFile(join(f.root, "package.json"), '{"name":"foreign"}');
await expect(resolvePiProvisioner(f.url)).rejects.toThrow("identity");
await writeFile(join(f.root, "package.json"), '{"name":"@paperclipai/server"}');
await rm(join(f.cli, "provision-pi.cjs")); await symlink(join(other.cli, "provision-pi.cjs"), join(f.cli, "provision-pi.cjs"));
await expect(resolvePiProvisioner(f.url)).rejects.toThrow("escapes");
});
it("provides an explicit Pi-only operator command and rejects other providers before resolution", async () => {
const program = new Command(); registerRuntimeCommands(program);
await expect(program.parseAsync(["node", "paperclipai", "runtime", "setup", "untrusted"])).rejects.toThrow("Supported explicit runtime setup");
});
+52
View File
@@ -0,0 +1,52 @@
import { spawn } from "node:child_process";
import { lstat, readFile, realpath } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { Command } from "commander";
/** Resolve the public server dependency, without importing/starting the server. */
export async function resolvePiProvisioner(serverUrl: string): Promise<string> {
const url = new URL(serverUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Pi setup requires an installed Paperclip server");
const entry = await realpath(fileURLToPath(url));
if (!entry.endsWith("/dist/index.js")) throw new Error("Pi setup requires the published server layout");
const root = resolve(dirname(entry), "..");
const manifest = join(root, "package.json");
const info = await lstat(manifest);
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536 || JSON.parse(await readFile(manifest, "utf8")).name !== "@paperclipai/server") throw new Error("Pi setup server package identity is invalid");
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-pi.cjs");
if (await realpath(provisioner) !== provisioner || !(await lstat(provisioner)).isFile()) throw new Error("Pi setup entrypoint escapes its server package");
return provisioner;
}
export async function setupPiRuntime(): Promise<void> {
const provisioner = await resolvePiProvisioner(import.meta.resolve("@paperclipai/server"));
// Only the explicit setup command can download pinned public dependencies.
// Do not forward provider credentials, proxy/npm config, HOME or NODE_OPTIONS.
const child = spawn(process.execPath, [provisioner], {
stdio: "inherit", env: { PATH: process.env.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" },
});
const cancel = () => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); };
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
try {
await new Promise<void>((accept, reject) => {
let spawnError: Error | undefined;
child.on("error", error => { spawnError = error; });
child.once("close", (code, signal) => {
if (spawnError) reject(spawnError);
else if (code !== 0 || signal) reject(new Error("Pi setup did not finish. Review its error; an existing invalid installation is never replaced automatically."));
else accept();
});
});
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
}
export function registerRuntimeCommands(program: Command): void {
program.command("runtime").description("Manage explicitly installed agent runtimes")
.command("setup <provider>")
.description("Install and verify the pinned Pi runtime for this host (public downloads; no model calls)")
.action(async (provider: string) => {
if (provider !== "pi") throw new Error("Supported explicit runtime setup: paperclipai runtime setup pi");
await setupPiRuntime();
});
}
+3
View File
@@ -1,3 +1,4 @@
import { registerRuntimeCommands } from "./commands/runtime.js";
import { registerEmailCommands } from "./commands/client/email.js";
import { Command } from "commander";
import { warnIfUnsupportedNodeVersion } from "@paperclipai/shared/node-version";
@@ -101,6 +102,7 @@ program
.action(updateCommand);
program.hook("preAction", async (_thisCommand, actionCommand) => {
if (actionCommand.parent?.name() === "runtime") return; // Public runtime setup never reads instance config or credentials.
const options = actionCommand.optsWithGlobals() as DataDirOptionLike & TestDriveOptions;
let dataDirOptions: DataDirOptionLike = options;
if (actionCommand.name() === "test-drive") {
@@ -124,6 +126,7 @@ program.hook("preAction", async (_thisCommand, actionCommand) => {
});
registerTestDriveCommand(program);
registerRuntimeCommands(program);
program
.command("onboard")