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.
60 lines
3.2 KiB
JavaScript
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"})`;
|
|
}
|