Files
stack/packages/cli/src/boot-status.mjs
T
jason.woltje 8fec494694 fix(organization): bus start and launch check org definitions against the held credentials (#1873)
T1 follow-up. A definition's text becomes an agent's contract, but a load ran the
held-secret check with an empty store. Bus start now checks every definitions entry
(the boot copy's org: entries and the file's own) with the live store, and the
launch check asks the bus the same question for the org: definition it just read,
before a contract exists. A hit refuses as definition-holds-credential, a new code
in the refusal table; the refusal names the definition and key, never the text or
the credential. bus start records the refusal in the boot status.
2026-11-11 08:49:15 -06:00

60 lines
3.2 KiB
JavaScript

// Why the bus isn't up (vikunja:19/302, REQ-EVT-1, REQ-ROLE-3). `mosaic bus start` fails closed on a
// missing or invalid organization file: no broker, no sessions, no verbs. Before it throws it writes
// this status, where the Console and `mosaic bus status` can read it with the bus down:
// <dataRoot>/bus-host/boot-status.json the latest outcome (0600), replaced each start
// <dataRoot>/bus-host/boot-events.jsonl one event per start (0600), appended
// An event is { event: "bus.boot.refused" | "bus.boot.ok", at, business, file, reason }. The
// reason is the validator's own message, which names the key or line; it holds references and
// paths, never a token value. Nothing here retries: a fixed file comes back on the next start.
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const REASON_MAX = 1000;
const dir = (dataRoot) => join(dataRoot, "bus-host");
export const bootStatusFile = (dataRoot) => join(dir(dataRoot), "boot-status.json");
export const bootEventsFile = (dataRoot) => join(dir(dataRoot), "boot-events.jsonl");
// The status file, or null when no start has been recorded. An unreadable file is reported as
// null, not thrown: a status read never takes the Console or the CLI down.
export function readBootStatus(dataRoot) {
const file = bootStatusFile(dataRoot);
if (!existsSync(file)) return null;
try {
const s = JSON.parse(readFileSync(file, "utf8"));
if (!s || typeof s !== "object" || typeof s.at !== "string" || typeof s.ok !== "boolean") return null;
return s;
} catch {
return null;
}
}
function write(dataRoot, status, event) {
mkdirSync(dir(dataRoot), { recursive: true, mode: 0o700 });
const file = bootStatusFile(dataRoot);
const tmp = `${file}.${process.pid}.tmp`;
writeFileSync(tmp, `${JSON.stringify(status, null, 2)}\n`, { mode: 0o600 });
renameSync(tmp, file);
appendFileSync(bootEventsFile(dataRoot), `${JSON.stringify(event)}\n`, { mode: 0o600 });
}
// A start refused: keeps the time of the last good load from the status it replaces. `prior` is the
// status from before this start, for a refusal that comes after the start recorded its load (org-defscan:
// the broker's held-secret check on the definitions).
export function recordBootRefused(dataRoot, { business, file, reason, at = new Date().toISOString(), prior = readBootStatus(dataRoot) }) {
const lastGood = prior?.ok ? prior.at : prior?.lastGood ?? null;
const text = String(reason).slice(0, REASON_MAX);
write(dataRoot, { ok: false, at, business, file, reason: text, lastGood }, { event: "bus.boot.refused", at, business, file, reason: text });
}
// A start that loaded the file: clears any refusal.
export function recordBootOk(dataRoot, { business, file, at = new Date().toISOString() }) {
write(dataRoot, { ok: true, at, business, file, reason: null, lastGood: at }, { event: "bus.boot.ok", at, business, file });
}
// One line for the CLI.
export function bootLine(s) {
if (!s) return "boot: no start recorded";
if (s.ok) return `boot: organization file loaded at ${s.at}`;
return `boot: REFUSED at ${s.at}: bus down, organization file invalid: ${s.reason} (file ${s.file}; last good load ${s.lastGood ?? "none"})`;
}