mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
feat(runner): import verified Linux companions for normal Pi execution
Add explicit public CLI setup and full async source/profile/byte admission for an operator-pinned Linux companion. Preserve existing remote integrity checks and explicit overrides. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8ced6f9285
commit
f96195a7a6
7 files changed
+464
-9
No files matched your search
@@ -1,14 +1,14 @@
|
||||
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, realpath, 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";
|
||||
import { registerRuntimeCommands, resolvePiProvisioner, resolveRemoteCompanionImporter } 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 root = await realpath(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')");
|
||||
@@ -30,3 +30,16 @@ it("provides an explicit Pi-only operator command and rejects other providers be
|
||||
const program = new Command(); registerRuntimeCommands(program);
|
||||
await expect(program.parseAsync(["node", "paperclipai", "runtime", "setup", "untrusted"])).rejects.toThrow("Supported explicit runtime setup");
|
||||
});
|
||||
|
||||
it("resolves the companion importer only inside the actual public server tar layout", async () => {
|
||||
const f = await fixture(); const path = join(f.root, "dist/services/native-runtime/remote-pi-companion.js");
|
||||
await mkdir(join(f.root, "dist/services/native-runtime"), { recursive: true });
|
||||
await writeFile(path, "throw new Error('resolution must not execute importer')");
|
||||
expect(await resolveRemoteCompanionImporter(f.url)).toBe(path);
|
||||
const other = await fixture(); await rm(path); await symlink(join(other.cli, "provision-pi.cjs"), path);
|
||||
await expect(resolveRemoteCompanionImporter(f.url)).rejects.toThrow("escapes");
|
||||
});
|
||||
it("requires an explicit digest for the companion import command", async () => {
|
||||
const program = new Command(); program.exitOverride().configureOutput({ writeErr() {} }); registerRuntimeCommands(program);
|
||||
await expect(program.parseAsync(["node", "paperclipai", "runtime", "import-remote", "/unused"])).rejects.toThrow("sha256");
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import type { Command } from "commander";
|
||||
|
||||
/** Resolve the public server dependency, without importing/starting the server. */
|
||||
@@ -19,6 +19,14 @@ export async function resolvePiProvisioner(serverUrl: string): Promise<string> {
|
||||
return provisioner;
|
||||
}
|
||||
|
||||
export async function resolveRemoteCompanionImporter(serverUrl: string): Promise<string> {
|
||||
const provisioner = await resolvePiProvisioner(serverUrl);
|
||||
const serverRoot = resolve(dirname(provisioner), "../../../..");
|
||||
const modulePath = join(serverRoot, "dist/services/native-runtime/remote-pi-companion.js");
|
||||
if (await realpath(modulePath) !== modulePath || !(await lstat(modulePath)).isFile()) throw new Error("Remote companion importer escapes its installed server");
|
||||
return modulePath;
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -42,8 +50,23 @@ export async function setupPiRuntime(): Promise<void> {
|
||||
}
|
||||
|
||||
export function registerRuntimeCommands(program: Command): void {
|
||||
program.command("runtime").description("Manage explicitly installed agent runtimes")
|
||||
.command("setup <provider>")
|
||||
const runtime = program.command("runtime").description("Manage explicitly installed agent runtimes");
|
||||
runtime.command("import-remote <directory>")
|
||||
.description("Import a verified Linux Pi companion into the installed server (no downloads)")
|
||||
.requiredOption("--sha256 <digest>", "SHA256 of companion.json from the trusted release")
|
||||
.action(async (directory: string, options: { sha256: string }) => {
|
||||
const modulePath = await resolveRemoteCompanionImporter(import.meta.resolve("@paperclipai/server"));
|
||||
const importer = await import(pathToFileURL(modulePath).href) as { importRemotePiCompanion(input: { directory: string; sha256: string; checkCancelled: () => void }): Promise<unknown> };
|
||||
let cancelled = false;
|
||||
const cancel = () => { cancelled = true; };
|
||||
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
|
||||
try {
|
||||
const result = await importer.importRemotePiCompanion({ directory, sha256: options.sha256,
|
||||
checkCancelled: () => { if (cancelled) throw new Error("Remote companion import cancelled"); } });
|
||||
console.log(JSON.stringify(result));
|
||||
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
||||
});
|
||||
runtime.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");
|
||||
|
||||
Reference in new issue
Block a user