import { chmod, chown, mkdir, mkdtemp, readFile, rm, writeFile, } from "node:fs/promises"; import nodePath from "node:path"; import { field, isMapping } from "#common"; import type { ProfileFailure, ProfileIdentity } from "#profiles"; import { parseCredentials } from "./credentials.ts"; const FIRST_PROFILE_UID = 20_000; export interface RuntimeProfile extends ProfileIdentity { index: number; uid: number; gid: number; home: string; tokenFile: string | null; } export interface RuntimeManifest { runtimeDir: string; manifestPath: string; reviewerFile: string; outcomeFile: string; profiles: RuntimeProfile[]; } export interface RuntimeSuccess { ok: true; body: string; reviewPath: string; anchorsPath: string; profile: ProfileIdentity; } export interface RuntimeFailure { ok: false; failures: ProfileFailure[]; } export type RuntimeOutcome = RuntimeSuccess | RuntimeFailure; async function writePrivate( path: string, value: string, uid: number, gid: number, ): Promise { await writeFile(path, value, { mode: 0o600 }); await chmod(path, 0o600); await chown(path, uid, gid); } export async function prepareRuntime( actionPath: string, runtimeRoot: string, masterKey: string, ): Promise { const credentials = await parseCredentials( await readFile(nodePath.join(actionPath, "profiles.json"), "utf8"), masterKey, ); const runtimeDir = await mkdtemp(nodePath.join(runtimeRoot, "bot-agents-")); try { await chmod(runtimeDir, 0o711); const reviewerFile = nodePath.join(runtimeDir, "reviewer-token"); await writePrivate(reviewerFile, credentials.reviewerToken, 0, 0); // One profile at a time, in order, as each claims the next UID. const profiles: RuntimeProfile[] = []; for (const [index, profile] of credentials.profiles.entries()) { const uid = FIRST_PROFILE_UID + index; const home = nodePath.join(runtimeDir, `profile-${index}`); await mkdir(home, { mode: 0o700 }); await chmod(home, 0o700); await chown(home, uid, uid); const tokenFile = profile.token === undefined || profile.token === "" ? null : nodePath.join(home, "token"); if (tokenFile !== null && profile.token !== undefined) { await writePrivate(tokenFile, profile.token, uid, uid); } profiles.push({ index, type: profile.type, model: profile.model, uid, gid: uid, home, tokenFile, }); } const manifestPath = nodePath.join(runtimeDir, "manifest.json"); const manifest: RuntimeManifest = { runtimeDir, manifestPath, reviewerFile, outcomeFile: nodePath.join(runtimeDir, "outcome.json"), profiles, }; await writePrivate(manifestPath, JSON.stringify(manifest), 0, 0); return manifest; } catch (error) { await rm(runtimeDir, { recursive: true }).catch(() => {}); throw error; } } function isRuntimeManifest(value: unknown): value is RuntimeManifest { return ( isMapping(value) && typeof field(value, "runtimeDir") === "string" && typeof field(value, "manifestPath") === "string" && typeof field(value, "reviewerFile") === "string" && typeof field(value, "outcomeFile") === "string" && Array.isArray(field(value, "profiles")) ); } export async function readRuntimeManifest( path: string, ): Promise { const value: unknown = JSON.parse(await readFile(path, "utf8")); if (!isRuntimeManifest(value)) { throw new Error(`${path}: invalid runtime manifest`); } return value; }