mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 23:39:21 +02:00
93 lines
6.9 KiB
JavaScript
93 lines
6.9 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { execFileSync } from "node:child_process";
|
|
import { access, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { HERMES_CLOSURES } from "../src/drivers/acpx/hermes-distributions.ts";
|
|
import { hermesProvisioningConfig } from "./hermes-provisioning-config.mjs";
|
|
|
|
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
|
|
/** Download the same pinned GitHub archive without the unauthenticated API quota. */
|
|
export async function downloadPinnedHermesArchive(version, { request = fetch, wait = delay } = {}) {
|
|
const deadline = Date.now() + 120_000;
|
|
const url = `https://codeload.github.com/NousResearch/hermes-agent/legacy.tar.gz/${version.commit}`;
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
const remaining = deadline - Date.now();
|
|
if (remaining <= 0) throw new Error("Hermes source download deadline exceeded; retry setup later");
|
|
const response = await request(url, { signal: AbortSignal.timeout(Math.min(30_000, remaining)) });
|
|
if (response.ok) {
|
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
if (digest(bytes) !== version.archiveSha256) throw new Error("Hermes source digest mismatch");
|
|
return bytes;
|
|
}
|
|
await response.body?.cancel();
|
|
const failure = `Hermes source download failed: HTTP ${response.status}`;
|
|
if (![429, 500, 502, 503, 504].includes(response.status) || attempt === 3) {
|
|
throw new Error(`${failure}${attempt > 1 ? ` after ${attempt} attempts` : ""}; retry setup later`);
|
|
}
|
|
const retryAfter = response.headers.get("retry-after");
|
|
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
const retryAt = retryAfter === null ? NaN : Date.parse(retryAfter);
|
|
const waitMs = Number.isFinite(seconds) ? Math.max(0, seconds * 1_000)
|
|
: Number.isFinite(retryAt) ? Math.max(0, retryAt - Date.now()) : attempt * 1_000;
|
|
if (waitMs > 30_000 || waitMs + 1 >= deadline - Date.now()) {
|
|
throw new Error(`${failure}; retry setup later after the download service's rate limit expires`);
|
|
}
|
|
await wait(waitMs);
|
|
}
|
|
}
|
|
|
|
/** Source provisioning and the public setup command share one pinned operation. */
|
|
export async function materializePinnedHermesDistribution({ destination, provider, materializer, verify }) {
|
|
if (!destination || !isAbsolute(destination) || resolve(destination) !== destination || destination.includes("\0")) throw new Error("Hermes provisioning requires a new normalized absolute destination");
|
|
const expectedClosureSha256 = HERMES_CLOSURES[`${process.platform}-${process.arch}`];
|
|
if (!expectedClosureSha256) throw new Error("Hermes platform is not a qualification target");
|
|
const version = JSON.parse(await readFile(join(provider, "version.json"), "utf8"));
|
|
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
if (await realpath(dirname(destination)) !== dirname(destination)) throw new Error("Hermes provisioning parent must be a real directory");
|
|
const existing = await lstat(destination).catch(error => { if (error.code !== "ENOENT") throw error; return null; });
|
|
if (existing) throw new Error("Hermes provisioning destination already exists; refusing to overwrite it");
|
|
const temporary = await mkdtemp(join(dirname(destination), ".hermes-install-"));
|
|
const source = join(temporary, "source");
|
|
const prepared = join(temporary, "distribution");
|
|
const env = { PATH: process.env.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8", HOME: join(temporary, "home"),
|
|
UV_CACHE_DIR: join(temporary, "uv-cache"), UV_PYTHON_INSTALL_DIR: join(temporary, "uv-python") };
|
|
try {
|
|
await mkdir(source, { mode: 0o700 });
|
|
await mkdir(env.HOME, { mode: 0o700 });
|
|
let uvVersion;
|
|
try { uvVersion = execFileSync("uv", ["--version"], { env, encoding: "utf8", timeout: 10000 }); }
|
|
catch (error) { if (error.code === "ENOENT") throw new Error("Hermes setup requires uv 0.12.17 on PATH. Install that version and retry setup."); throw error; }
|
|
if (!/^uv 0\.12\.17(?:\s|$)/.test(uvVersion)) throw new Error("Hermes provisioning requires uv 0.12.17; install that version before provisioning");
|
|
const bytes = await downloadPinnedHermesArchive(version);
|
|
const archive = join(temporary, "source.tar.gz");
|
|
await writeFile(archive, bytes, { mode: 0o600, flag: "wx" });
|
|
execFileSync("tar", ["-xzf", archive, "--strip-components=1", "-C", source], { env, timeout: 30000 });
|
|
if (digest(await readFile(join(source, "uv.lock"))) !== version.lockSha256) throw new Error("Hermes dependency lock digest mismatch");
|
|
const configFile = join(temporary, "uv.toml");
|
|
await writeFile(configFile, hermesProvisioningConfig(await readFile(join(source, "pyproject.toml"), "utf8")), { mode: 0o600, flag: "wx" });
|
|
execFileSync("uv", ["sync", "--locked", "--config-file", configFile, "--no-dev", "--no-install-project", "--python", version.python,
|
|
"--extra", "acp", "--extra", "mcp", "--extra", "anthropic", "--extra", "bedrock", "--extra", "google"], { cwd: source, env, stdio: "inherit", timeout: 300000 });
|
|
if (digest(await readFile(join(source, "uv.lock"))) !== version.lockSha256) throw new Error("Hermes dependency lock changed during installation");
|
|
execFileSync(join(source, ".venv/bin/python"), [materializer, source, prepared, provider], { env, stdio: "inherit", timeout: 120000 });
|
|
const manifest = JSON.parse(await readFile(join(prepared, "manifest.json"), "utf8"));
|
|
if (digest(JSON.stringify(manifest.entries)) !== expectedClosureSha256) throw new Error("Hermes provisioned closure does not match its reviewed pin");
|
|
// Public setup checks every execution byte before publication. Native admission
|
|
// repeats that verification before the runtime stages credentials.
|
|
await verify?.(prepared, expectedClosureSha256);
|
|
const conflict = await lstat(destination).catch(error => { if (error.code !== "ENOENT") throw error; return null; });
|
|
if (conflict) throw new Error("Hermes destination changed during setup; refusing to overwrite it");
|
|
await rename(prepared, destination);
|
|
return { destination, closureSha256: expectedClosureSha256, version: version.release };
|
|
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
}
|
|
|
|
if (process.argv[1]?.endsWith("/provision-hermes.mjs") && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
access(join(root, "src/providers/hermes/version.json")).then(() => join(root, "src/providers/hermes"), () => join(root, "dist/providers/hermes"))
|
|
.then(provider => materializePinnedHermesDistribution({ destination: process.argv[2], provider, materializer: join(root, "scripts/materialize-hermes.py") }))
|
|
.then(result => console.log(JSON.stringify(result))).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
}
|