Files
bot-agents/src/runtime/runtime.ts
T
Danny Kim 063eb09750 Replace NodeJS dict aliases with maps and named field reads
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
2026-10-20 05:01:49 +00:00

138 lines
3.6 KiB
TypeScript

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<void> {
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<RuntimeManifest> {
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<RuntimeManifest> {
const value: unknown = JSON.parse(await readFile(path, "utf8"));
if (!isRuntimeManifest(value)) {
throw new Error(`${path}: invalid runtime manifest`);
}
return value;
}