import { chmod, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { randomUUID } from "node:crypto"; import { DEFAULT_CREDITS_PER_USD, defaultConfig, DEFAULT_PRICE_CATALOG } from "./defaults.js"; import type { PriceCatalog, UltraConfig } from "../types.js"; import { migrateConfig } from "./migrations.js"; import { assertConfig } from "./schema.js"; import { assertAutoModeProfile } from "../security/profile-marker.js"; export interface UltraPaths { root: string; events: string; database: string; rawVault: string; exports: string; configs: string; experiments: string; ledgers: string; priceCatalog: string; champion: string; autoAdmissions: string; autoAdmissionClaims: string; hmacKey: string; vaultKey: string; } export function pathsFor(agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")): UltraPaths { const root = join(agentDir, "ultrapi"); return { root, events: join(root, "events"), database: join(root, "ultrapi.db"), rawVault: join(root, "raw-vault"), exports: join(root, "exports"), configs: join(root, "configs"), experiments: join(root, "experiments"), ledgers: join(root, "ledgers"), priceCatalog: join(root, "price-catalog.json"), champion: join(root, "champion.json"), autoAdmissions: join(root, "auto-admissions.json"), autoAdmissionClaims: join(root, "auto-admission-claims"), hmacKey: join(root, "hmac.key"), vaultKey: join(root, "vault.key") }; } async function readJson(file: string): Promise { try { return JSON.parse(await readFile(file, "utf8")) as T; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; } } export async function writeJsonAtomic(file: string, value: unknown, mode = 0o600): Promise { await mkdir(dirname(file), { recursive: true }); const temp = `${file}.${randomUUID()}.tmp`; await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode }); await rename(temp, file); } async function assertConfigStorageAccess(paths: UltraPaths, ...configs: UltraConfig[]): Promise { for (const config of configs) await assertAutoModeProfile(dirname(paths.root), config.mode); } export async function ensureStorage(paths: UltraPaths, profile: UltraConfig["profile"] = "private"): Promise { await Promise.all([paths.root, paths.events, paths.rawVault, paths.exports, paths.configs, paths.experiments, paths.ledgers, paths.autoAdmissionClaims].map((path) => mkdir(path, { recursive: true, mode: 0o700 }))); await chmod(paths.autoAdmissionClaims, 0o700); if (!(await readJson(paths.priceCatalog))) await writeJsonAtomic(paths.priceCatalog, DEFAULT_PRICE_CATALOG); if (!(await readJson(join(paths.configs, "1.0.0.json")))) await writeJsonAtomic(join(paths.configs, "1.0.0.json"), defaultConfig(profile)); if (!(await readJson<{ version: string }>(paths.champion))) await writeJsonAtomic(paths.champion, { version: "1.0.0" }); } export async function tryClaimAutoAdmission(paths: UltraPaths, version: string): Promise { if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new Error(`Invalid config version ${version}`); try { const legacy = JSON.parse(await readFile(paths.autoAdmissions, "utf8")) as unknown; if (!Array.isArray(legacy) || legacy.some((entry) => typeof entry !== "string" || !entry.trim())) throw new Error("invalid admission ledger"); if (legacy.includes(version)) return false; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { if (error instanceof SyntaxError || (error instanceof Error && error.message === "invalid admission ledger")) throw new Error("Auto admission ledger is invalid"); throw error; } } try { await mkdir(join(paths.autoAdmissionClaims, version), { mode: 0o700 }); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") return false; throw error; } } export async function loadConfig(paths: UltraPaths): Promise { await ensureStorage(paths); const champion = await readJson<{ version: string }>(paths.champion); if (!champion) throw new Error("UltraPi champion config is missing"); return await loadConfigVersion(paths, champion.version); } export async function loadConfigVersion(paths: UltraPaths, version: string): Promise { if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new Error(`Invalid config version ${version}`); const config = await readJson(join(paths.configs, `${version}.json`)); if (!config) throw new Error(`Unknown config version ${version}`); const migrated = migrateConfig(config); if (migrated.configVersion !== version) throw new Error(`Config version ${migrated.configVersion} does not match immutable file ${version}`); return migrated; } export async function loadPriceCatalog(paths: UltraPaths): Promise { await ensureStorage(paths); const catalog = await readJson(paths.priceCatalog); if (!catalog) throw new Error("UltraPi price catalog is missing"); // Normalised on read rather than on write: every install predating the field has a file without // it, and ensureStorage never rewrites an existing catalogue. const declared = catalog.creditsPerUsd; if (declared !== undefined && (!Number.isFinite(declared) || declared <= 0)) throw new Error("UltraPi price catalog creditsPerUsd must be a positive number"); return { ...catalog, creditsPerUsd: declared ?? DEFAULT_CREDITS_PER_USD }; } export async function saveConfigVersion(paths: UltraPaths, config: UltraConfig, reason: string): Promise { assertConfig(config); await assertConfigStorageAccess(paths, config); const existing = await readJson(join(paths.configs, `${config.configVersion}.json`)); if (existing) throw new Error(`Config version ${config.configVersion} is immutable`); await writeJsonAtomic(join(paths.configs, `${config.configVersion}.json`), { ...config, changeReason: reason, createdAt: new Date().toISOString() }); } export async function setChampion(paths: UltraPaths, version: string): Promise { const current = await loadConfig(paths); const target = await loadConfigVersion(paths, version); await assertConfigStorageAccess(paths, current, target); await writeJsonAtomic(paths.champion, { version }); } export async function rollbackConfig(paths: UltraPaths, version: string): Promise { const current = await loadConfig(paths); const target = await loadConfigVersion(paths, version); if (current.mode !== "auto") await assertAutoModeProfile(dirname(paths.root), target.mode); await setChampion(paths, version); return await loadConfig(paths); } export async function createConfigVersion(paths: UltraPaths, current: UltraConfig, change: (config: UltraConfig) => UltraConfig, reason: string): Promise { await assertConfigStorageAccess(paths, current); const parse = (version: string): [number, number, number] | undefined => { const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+][A-Za-z0-9.-]+)?$/.exec(version); const parsed = match ? [Number(match[1]), Number(match[2]), Number(match[3])] as [number, number, number] : undefined; return parsed?.every(Number.isSafeInteger) ? parsed : undefined; }; const currentVersion = parse(current.configVersion); if (!currentVersion) throw new Error(`Config version is not semver: ${current.configVersion}`); const [major, minor, patch] = [currentVersion, ...(await readdir(paths.configs)) .flatMap((file) => file.endsWith(".json") ? [parse(file.slice(0, -5))] : []) .filter((version): version is [number, number, number] => Boolean(version))] .sort((left, right) => right[0] - left[0] || right[1] - left[1] || right[2] - left[2])[0]!; const nextVersion = `${major}.${minor}.${patch + 1}`; const changed = change(structuredClone(current)); changed.configVersion = nextVersion; changed.policyVersion = nextVersion; const next = migrateConfig(changed); await assertConfigStorageAccess(paths, next); await saveConfigVersion(paths, next, reason); return next; } export async function updateConfig(paths: UltraPaths, current: UltraConfig, change: (config: UltraConfig) => UltraConfig, reason: string): Promise { const next = await createConfigVersion(paths, current, change, reason); await setChampion(paths, next.configVersion); return next; }