launch.max becomes launch.models plus launch.limits (total, providers, exact provider/model). The launcher re-reads limits on every attempt. Settings and `mosaic limits` write through one locked, journaled organization writer. `mosaic limits migrate` is print-only. Candidate 00b84e311475f97ced0d9a9599fba23a28442971ed15d8d489d9422d84a8812e, Rocko packet4b6e50636, Filbert round 2 approve (comment 28542). Landing gate at0c56cc2d4: 23 suites green. Decision 123 go from Mos. Co-Authored-By: Claude Opus 5.5 <[email protected]>
106 lines
4.6 KiB
JavaScript
106 lines
4.6 KiB
JavaScript
// What the host and the human commands read before they touch the bus:
|
|
// the system config (through scripts/mosaic-config.mjs, as `mosaic business`
|
|
// does), the business file, and the boot message for the broker process.
|
|
// Everything here reads and refuses; nothing writes.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { BusinessError, configDir, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs";
|
|
import { busBusiness } from "../../bus/src/business.mjs";
|
|
import { BusError } from "../../bus/src/broker.mjs";
|
|
import { CliError } from "./errors.mjs";
|
|
|
|
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
|
|
// The broker's socket. One broker per data root: the bus store takes
|
|
// `<dataRoot>/bus/writer.lock` (packages/bus/src/store.mjs).
|
|
export const socketPath = (dataRoot) => join(dataRoot, "bus", "broker.sock");
|
|
|
|
export function loadSystem({ env = process.env } = {}) {
|
|
const proc = spawnSync(process.execPath, [join(REPO, "scripts", "mosaic-config.mjs"), "validate"], {
|
|
encoding: "utf8",
|
|
maxBuffer: 1024 * 1024,
|
|
env,
|
|
});
|
|
if (proc.status !== 0) throw new CliError(`system config problem (mosaic-config exit ${proc.status}): ${proc.stderr.trim()}`, 3);
|
|
try {
|
|
return JSON.parse(proc.stdout);
|
|
} catch {
|
|
throw new CliError("system config: mosaic-config printed something that isn't JSON", 3);
|
|
}
|
|
}
|
|
|
|
export function rolesDir(env = process.env) {
|
|
return resolve(env.MOSAIC_ROLES_DIR || join(REPO, "roles"));
|
|
}
|
|
|
|
// A business-package or adapter refusal is a config problem: exit 3.
|
|
function business(fn) {
|
|
try {
|
|
return fn();
|
|
} catch (error) {
|
|
if (error instanceof BusinessError) throw new CliError(error.message, 3);
|
|
if (error instanceof BusError) throw new CliError(`business adapter refused: ${error.code}`, 3);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// config.trackers[<business>] (lead decision 70): plain data from the
|
|
// tracker.* variables. tracker.project is a project-layer variable, so the
|
|
// entry comes from the one declared project whose file sets it. No
|
|
// tracker.baseUrl, or no project naming a tracker project, means no entry
|
|
// and no task verbs; two projects naming one each refuse, since the boot
|
|
// shape holds one tracker project per business.
|
|
function trackerFor(system, b, warn) {
|
|
const first = Object.keys(b.roles)[0];
|
|
const base = resolveInstance({ system, business: b, instance: first }).vars;
|
|
if (base["tracker.baseUrl"] === undefined) return null;
|
|
const named = [];
|
|
for (const [id, entry] of Object.entries(b.projects)) {
|
|
if (!existsSync(projectFilePath(entry.root))) continue;
|
|
const project = loadProject(entry.root);
|
|
if (project.id !== id) throw new CliError(`project file ${project.file} has id ${project.id}, but business ${b.id} declares it as ${id}`, 3);
|
|
const vars = resolveInstance({ system, business: b, project, instance: first }).vars;
|
|
if (vars["tracker.project"] !== undefined) named.push({ id, vars });
|
|
}
|
|
if (named.length > 1) {
|
|
throw new CliError(`business ${b.id}: projects ${named.map((n) => n.id).join(", ")} each set tracker.project; the broker takes one tracker project per business`, 3);
|
|
}
|
|
if (named.length === 0) {
|
|
warn(`business ${b.id} sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs`);
|
|
return null;
|
|
}
|
|
const v = named[0].vars;
|
|
return {
|
|
baseUrl: v["tracker.baseUrl"],
|
|
project: v["tracker.project"],
|
|
pollSeconds: v["tracker.pollSeconds"],
|
|
reconcileMinutes: v["tracker.reconcileMinutes"],
|
|
descendants: v["tracker.descendants"],
|
|
};
|
|
}
|
|
|
|
// The `{op: 'boot'}` config for packages/bus/src/process.mjs, for one
|
|
// business. The host holds a reader capability for the notifier and binds
|
|
// launches later through bindLaunch, so `launches` starts empty.
|
|
export function bootConfig({ system, businessId, env = process.env, warn = () => {} }) {
|
|
return business(() => {
|
|
const vars = systemVars(system);
|
|
const b = loadBusiness(businessId, { dir: configDir(env), rolesDir: rolesDir(env) });
|
|
const resolved = {};
|
|
for (const instance of Object.keys(b.roles)) resolved[instance] = resolveInstance({ system: vars, business: b, instance });
|
|
const config = {
|
|
dataRoot: system.dataRoot,
|
|
businesses: { [b.id]: busBusiness(b, resolved) },
|
|
organization: {dir:configDir(env),rolesDir:rolesDir(env),system:vars},
|
|
launches: [],
|
|
readers: [b.id],
|
|
};
|
|
const tracker = trackerFor(vars, b, warn);
|
|
if (tracker) config.trackers = { [b.id]: tracker };
|
|
return config;
|
|
});
|
|
}
|