#!/usr/bin/env node /** * Mosaic development configuration: bootstrap, validate, resolve. * * Operations: * bootstrap Create the default config ONLY if absent; otherwise validate * the existing file without writing (idempotent). * validate Load and strictly validate; print resolved config on stdout. * env Print shell-safe exports for launcher scripts: * MOSAIC_DATA_ROOT, MOSAIC_PROVIDER, MOSAIC_MODEL. * instance Print shell-safe exports naming the selected stack instance * (row 190): MOSAIC_INSTANCE, MOSAIC_INSTANCE_CONFIG, * MOSAIC_INSTANCE_CONFIG_DIR, MOSAIC_INSTANCE_DATA_ROOT and * MOSAIC_UNIT_SUFFIX. scripts/instance.sh reads them. * * Each operation takes an optional `--instance NAME`. NAME matches * ^[a-z][a-z0-9]{1,15}$ and selects ~/.config/mosaic-NAME/config.json; * `dev` is the default instance, so `--instance dev` is the same as no flag. * Without the flag the config is $MOSAIC_CONFIG or * ~/.config/mosaic-dev/config.json, as before. * * A named instance's data root carries /instance.json, written by * `bootstrap --instance NAME`, naming the instance. With the flag every * operation refuses a root whose marker is missing or names another * instance; `instance` without the flag refuses a root that a named * instance claims. * * Exit codes: * 0 success * 2 configuration exists but is invalid (never modified by this tool) * 3 configuration is missing for a read operation (validate/env/instance), * or the data root's instance marker refuses the selection * 4 usage: a bad instance name (instance-name-invalid), or --instance and * $MOSAIC_CONFIG naming different files (instance-config-mismatch) * * Invariants (docs/plans/2026-09-02_atomic-mosaic-foundation.md): * - Existing configuration is never overwritten. * - Validation failure modifies nothing. * - Secrets are never stored in configuration. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; const SUPPORTED_CONFIG_VERSION = 1; const SUPPORTED_ENVIRONMENTS = new Set(["development", "production"]); const SUPPORTED_BACKENDS = new Set(["docker"]); const SUPPORTED_ADAPTERS = new Set(["pi", "mock"]); // mock: test-only, see adapters/README.md const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/; // Row 190: stack instance names. No hyphen, so systemd's prefix drop-ins // for mosaic-bus-NAME@ stay a fixed list and no name prefixes another's units. const INSTANCE_PATTERN = /^[a-z][a-z0-9]{1,15}$/; const DEFAULT_INSTANCE = "dev"; const MARKER = "instance.json"; class Failure extends Error { constructor(exitCode, message) { super(message); this.exitCode = exitCode; } } function fail(exitCode, message) { throw new Failure(exitCode, message); } // #1873: a refusal names the config by where it came from, the data root and its marker by // their roles, and a key or argument only when it is a plain name; never a host path or a raw // error message. CONFIG is set when the selection is made. let CONFIG = "the default system config"; function configLabel(instance) { return isNamed(instance) ? `the config of instance ${instance}` : process.env.MOSAIC_CONFIG ? "the system config MOSAIC_CONFIG names" : "the default system config"; } const plain = (value, otherwise) => /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/.test(String(value)) ? JSON.stringify(String(value)) : otherwise; const MARKER_LABEL = "the data root's instance marker"; function instancePath(name) { return path.join(os.homedir(), ".config", `mosaic-${name}`, "config.json"); } function realOrNull(file) { try { return fs.realpathSync(file); } catch { return null; } } // The config file for a selection. Without --instance, as before. With it, // NAME's file; a $MOSAIC_CONFIG beside it must be the same file. function configPath(instance) { CONFIG = configLabel(instance); if (instance === null) { return process.env.MOSAIC_CONFIG ? path.resolve(process.env.MOSAIC_CONFIG) : instancePath(DEFAULT_INSTANCE); } const named = instancePath(instance); if (process.env.MOSAIC_CONFIG) { const given = path.resolve(process.env.MOSAIC_CONFIG); const same = given === named || (realOrNull(given) !== null && realOrNull(given) === realOrNull(named)); if (!same) { fail(4, `instance-config-mismatch: --instance ${instance} names its own config, but MOSAIC_CONFIG names another file`); } } return named; } // Operation arguments: nothing, or `--instance NAME` once. function parseArgs(args) { if (args.length === 0) return null; if (args.length !== 2 || args[0] !== "--instance") { fail(4, `usage: mosaic-config.mjs ${operation ?? "OP"} [--instance NAME]`); } const name = args[1]; if (!INSTANCE_PATTERN.test(name)) { fail(4, `instance-name-invalid: ${plain(name, "that name")} must match ${INSTANCE_PATTERN}`); } return name; } function isNamed(instance) { return instance !== null && instance !== DEFAULT_INSTANCE; } // The marker in a data root: null when absent, else its name. Anything else // at the path refuses. function readMarker(dataRoot) { const file = path.join(dataRoot, MARKER); let stat; try { stat = fs.lstatSync(file); } catch (error) { if (error.code === "ENOENT") return null; fail(3, `instance-root-unclaimed: cannot read ${MARKER_LABEL} (${error.code})`); } if (!stat.isFile() || stat.isSymbolicLink()) { fail(3, `instance-root-unclaimed: ${MARKER_LABEL} must be a regular, non-symbolic-link file`); } let marker; try { marker = JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { fail(3, `instance-root-unclaimed: ${MARKER_LABEL} is not a valid marker (${error instanceof SyntaxError ? 'not valid JSON' : `cannot read it, ${error.code ?? "error"}`})`); } if (!isPlainObject(marker) || Object.keys(marker).sort().join(",") !== "instanceVersion,name" || marker.instanceVersion !== 1 || typeof marker.name !== "string" || !INSTANCE_PATTERN.test(marker.name)) { fail(3, `instance-root-unclaimed: ${MARKER_LABEL} is not a valid marker ({"instanceVersion":1,"name":NAME})`); } return marker.name; } // The claim check (row 190 D2). A named instance needs its own marker; the // default needs none, or one naming dev. function checkClaim(dataRoot, instance) { const owner = readMarker(dataRoot); if (isNamed(instance)) { if (owner === null) { fail(3, `instance-root-unclaimed: the data root holds no ${MARKER} naming ${instance} (scripts/bootstrap.sh --instance ${instance} claims a new root)`); } if (owner !== instance) { fail(3, `instance-root-mismatch: the data root is claimed by instance ${owner}, not ${instance}`); } } else if (owner !== null && owner !== DEFAULT_INSTANCE) { fail(3, `instance-root-mismatch: the data root is claimed by instance ${owner}; select it with --instance ${owner}`); } } function readRaw(file) { let stat; try { stat = fs.lstatSync(file); } catch { return null; // missing } if (!stat.isFile() || stat.isSymbolicLink()) { fail(2, `configuration path must be a regular, non-symbolic-link file: ${CONFIG}`); } try { return fs.readFileSync(file, "utf8"); } catch { fail(2, `configuration file is not readable: ${CONFIG}`); } } function rejectUnknownKeys(object, allowed, where) { for (const key of Object.keys(object)) { if (!allowed.includes(key)) { fail(2, `unsupported ${where} key: ${plain(key, "that is not a plain name")}`); } } } function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function validateDataRoot(value, file) { if (typeof value !== "string" || value.length === 0) { fail(2, 'execution "dataRoot" must be a non-empty string'); } if (value.includes("\0")) { fail(2, 'execution "dataRoot" contains a NUL byte'); } if (!path.isAbsolute(value)) { fail(2, `execution "dataRoot" must be an absolute path`); } const resolved = path.resolve(value); if (resolved !== value) { fail(2, `execution "dataRoot" must be canonical without ".", "..", trailing slashes, or redundant separators`); } if (resolved === path.parse(resolved).root) { fail(2, 'execution "dataRoot" must not be the filesystem root'); } const home = path.resolve(os.homedir()); if (resolved === home || isAncestorOf(resolved, home)) { fail(2, 'execution "dataRoot" must not be or contain the home directory'); } const configDir = path.dirname(path.resolve(file)); if (resolved === configDir || isAncestorOf(resolved, configDir)) { fail(2, 'execution "dataRoot" must not be or contain the configuration directory'); } return resolved; } function isAncestorOf(ancestor, candidate) { const rel = path.relative(ancestor, candidate); return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); } // Q175 phase 1 (#1690): the optional `console` key, checked for shape only by the Console's own // checker. It is imported only when the key is there, so a config without it never needs the // Console's code, and a copy of this file alone still reads one. async function validateConsole(value) { let checkConsole; try { ({ checkConsole } = await import(pathToFileURL(path.join(import.meta.dirname, "..", "packages", "webui", "src", "sso-config.mjs")).href)); } catch { fail(2, 'the "console" key needs packages/webui/src/sso-config.mjs beside this loader'); } try { return checkConsole(value); } catch (error) { fail(2, error.message); } } // FED R2 (#1701, DESIGN.md section 3.1 D1): the optional `federation` key names this instance and its // peers. Ids and origins are labels, not trust: nothing here grants a peer anything (3.1, W1). The // origin must be exactly an https origin, the form `new URL(origin).origin` returns, so a path, a // trailing slash, a port that is the default, or userinfo refuses. const FEDERATION_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/; const FEDERATION_LABEL_MAX = 80; function validateFederationOrigin(value, where) { let url = null; try { url = new URL(value); } catch { url = null; } if (url === null || url.protocol !== "https:" || url.origin !== value) { fail(2, `${where} must be an https origin with no path, query, or userinfo`); } } function validateFederation(value) { if (!isPlainObject(value)) { fail(2, '"federation" must be a JSON object'); } rejectUnknownKeys(value, ["self", "peers"], "federation"); if (typeof value.self !== "string" || !FEDERATION_ID_PATTERN.test(value.self)) { fail(2, `federation.self must match ${FEDERATION_ID_PATTERN}`); } if (!Array.isArray(value.peers)) { fail(2, 'federation.peers must be an array'); } const ids = new Set([value.self]); const peers = value.peers.map((peer, index) => { const where = `federation.peers[${index}]`; if (!isPlainObject(peer)) { fail(2, `${where} must be a JSON object`); } rejectUnknownKeys(peer, ["id", "origin", "label"], where); if (typeof peer.id !== "string" || !FEDERATION_ID_PATTERN.test(peer.id)) { fail(2, `${where}.id must match ${FEDERATION_ID_PATTERN}`); } if (ids.has(peer.id)) { fail(2, `${where}.id ${JSON.stringify(peer.id)} is already named (by federation.self or an earlier peer)`); } ids.add(peer.id); validateFederationOrigin(peer.origin, `${where}.origin`); if (typeof peer.label !== "string" || peer.label.trim() === "" || peer.label.length > FEDERATION_LABEL_MAX) { fail(2, `${where}.label must be a non-empty string of at most ${FEDERATION_LABEL_MAX} characters`); } return { id: peer.id, origin: peer.origin, label: peer.label }; }); return { self: value.self, peers }; } async function validate(document, file) { if (!isPlainObject(document)) { fail(2, "configuration must be a JSON object"); } // Row 270 r2 and r3 (mos-1352, DECISIONS Q222, sage-3038): compactionReadBytes and // compactionStreams, optional and human-only, pass on as written; the launcher takes the // default for a missing or bad value (packages/seat/src/compactions.mjs readBudget and // readStreams). The first list stays as it was: // packages/cli/tests/stack-release-fixture.mjs edits it to make an older loader. rejectUnknownKeys( document, ["configVersion", "environment", "dataRoot", "execution", "console", "federation"].concat(["compactionReadBytes", "compactionStreams"]), "configuration", ); if (document.configVersion !== SUPPORTED_CONFIG_VERSION) { fail(2, `unsupported configVersion: (supported: ${SUPPORTED_CONFIG_VERSION})`); } if (!SUPPORTED_ENVIRONMENTS.has(document.environment)) { fail(2, `unsupported environment: (supported: ${[...SUPPORTED_ENVIRONMENTS].join(", ")})`); } const dataRoot = validateDataRoot(document.dataRoot, file); if (!isPlainObject(document.execution)) { fail(2, '"execution" must be a JSON object'); } rejectUnknownKeys(document.execution, ["backend", "provider", "model", "adapter"], '"execution"'); if (!SUPPORTED_BACKENDS.has(document.execution.backend)) { fail(2, `unsupported execution.backend: (supported: ${[...SUPPORTED_BACKENDS].join(", ")})`); } for (const key of ["provider", "model"]) { const value = document.execution[key]; if (typeof value !== "string" || !NAME_PATTERN.test(value)) { fail(2, `execution.${key} must match ${NAME_PATTERN}`); } } const adapter = document.execution.adapter === undefined || document.execution.adapter === null ? "pi" : document.execution.adapter; if (typeof adapter !== "string" || !SUPPORTED_ADAPTERS.has(adapter)) { fail(2, `unsupported execution.adapter: (supported: ${[...SUPPORTED_ADAPTERS].join(", ")})`); } // Last, so the older checks keep their order. The key is in the output only when the file has // it, so a config without it prints what it always printed. const consoleKey = Object.hasOwn(document, "console") ? await validateConsole(document.console) : undefined; // FED R2: the same rule as `console`. The key appears in the output only when the file has it. const federationKey = Object.hasOwn(document, "federation") ? validateFederation(document.federation) : undefined; let proxy; try { let present = false; try { fs.lstatSync(path.join(path.dirname(file), "proxy.json")); present = true; } catch (e) { if (e.code !== "ENOENT") throw e; } // Historical no-proxy copies have no model-proxy package. Do not load a new // dependency unless the sidecar exists; a broken symlink is present and refuses. if (present) { const { loadProxy } = await import("../packages/model-proxy/src/settings.mjs"); proxy = loadProxy(path.dirname(file), dataRoot); } } catch (error) { fail(2, ['proxy-rotate-grace-invalid', 'proxy-first-output-invalid'].includes(error.code) ? error.code : "proxy-config-invalid"); } return { configVersion: document.configVersion, ...(proxy === undefined ? {} : { proxy }), environment: document.environment, dataRoot, execution: { backend: document.execution.backend, provider: document.execution.provider, model: document.execution.model, adapter, }, ...(consoleKey === undefined ? {} : { console: consoleKey }), ...(federationKey === undefined ? {} : { federation: federationKey }), ...(Object.hasOwn(document, "compactionReadBytes") ? { compactionReadBytes: document.compactionReadBytes } : {}), ...(Object.hasOwn(document, "compactionStreams") ? { compactionStreams: document.compactionStreams } : {}), }; } async function load(file, instance = null) { const raw = readRaw(file); if (raw === null) { const bootstrap = isNamed(instance) ? `scripts/bootstrap.sh --instance ${instance}` : "scripts/bootstrap.sh"; // #1873: named by where it came from, never by host path. fail(3, `configuration not found: ${configLabel(instance)} (run ${bootstrap} to create it)`); } let document; try { document = JSON.parse(raw); } catch { fail(2, `configuration is not valid JSON (${CONFIG})`); } return await validate(document, file); } function shellQuote(value) { return `'${String(value).replaceAll("'", `'\\''`)}'`; } function defaultConfig(dataRoot) { return { configVersion: 1, environment: "development", dataRoot, execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", }, }; } const DEFAULT_CONFIG = defaultConfig(path.join(os.homedir(), ".mosaic-dev")); // 'wx': creation is exclusive; an existing file is never overwritten, and a // symbolic link at the path is never followed. function createExclusive(file, text, label) { let fd; try { fd = fs.openSync(file, "wx", 0o644); fs.writeFileSync(fd, text); return true; } catch (error) { if (error.code === "EEXIST") return false; fail(1, `unable to create ${label} (${error.code ?? "error"})`); } finally { if (fd !== undefined) fs.closeSync(fd); } } async function bootstrapDefault(file) { if (fs.existsSync(file)) { await load(file); // validate only; never rewrite process.stderr.write(`mosaic-config: configuration already present, validated without changes: ${file}\n`); return; } fs.mkdirSync(path.dirname(file), { recursive: true }); if (!createExclusive(file, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, "configuration")) { await load(file); return; } await load(file); process.stderr.write(`mosaic-config: created default configuration: ${file}\n`); } // Row 190 D2: NAME's config dir (0700), its config, then the claimed data // root. The root is checked before anything is written, so a refusal // writes nothing. async function bootstrapNamed(file, instance) { if (fs.existsSync(file)) { const resolved = await load(file, instance); // validate only; never rewrite checkClaim(resolved.dataRoot, instance); process.stderr.write(`mosaic-config: configuration already present, validated without changes: ${file}\n`); return; } const dataRoot = path.join(os.homedir(), `.mosaic-${instance}`); let entries = null; try { const stat = fs.lstatSync(dataRoot); if (!stat.isDirectory()) fail(3, `instance-root-unclaimed: the instance's data root exists and is not a directory; nothing was written`); entries = fs.readdirSync(dataRoot); } catch (error) { if (error instanceof Failure) throw error; if (error.code !== "ENOENT") fail(3, `instance-root-unclaimed: cannot read the instance's data root (${error.code}); nothing was written`); } if (entries !== null && entries.length > 0 && readMarker(dataRoot) !== instance) { fail(3, `instance-root-unclaimed: the instance's data root exists, is not empty and holds no ${MARKER} naming ${instance}; nothing was written`); } const configDir = path.dirname(file); fs.mkdirSync(path.dirname(configDir), { recursive: true }); try { fs.mkdirSync(configDir, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") fail(1, `unable to create the config directory of instance ${instance} (${error.code ?? "error"})`); } if (!createExclusive(file, `${JSON.stringify(defaultConfig(dataRoot), null, 2)}\n`, CONFIG)) { const resolved = await load(file, instance); checkClaim(resolved.dataRoot, instance); return; } const resolved = await load(file, instance); try { fs.mkdirSync(resolved.dataRoot, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") fail(1, `unable to create the data root (${error.code ?? "error"})`); } createExclusive(path.join(resolved.dataRoot, MARKER), `${JSON.stringify({ instanceVersion: 1, name: instance })}\n`, MARKER_LABEL); checkClaim(resolved.dataRoot, instance); process.stderr.write(`mosaic-config: created configuration for instance ${instance}: ${file}, data root ${resolved.dataRoot}\n`); } // Without --instance, a missing or invalid config is left to the caller's // own load, as before; only a loaded config's root is checked. async function selected(instance) { const file = configPath(instance); let dataRoot = ""; let proxy = false; if (isNamed(instance)) { const resolved = await load(file, instance); dataRoot = resolved.dataRoot; proxy = Boolean(resolved.proxy); checkClaim(dataRoot, instance); } else { let resolved = null; try { resolved = readRaw(file) === null ? null : await load(file); } catch (error) { if (!(error instanceof Failure) || error.message === "proxy-config-invalid") throw error; } if (resolved !== null) { dataRoot = resolved.dataRoot; proxy = Boolean(resolved.proxy); checkClaim(dataRoot, instance); } } const name = isNamed(instance) ? instance : DEFAULT_INSTANCE; return [ `MOSAIC_INSTANCE=${shellQuote(name)}`, `MOSAIC_INSTANCE_CONFIG=${shellQuote(path.resolve(file))}`, `MOSAIC_INSTANCE_CONFIG_DIR=${shellQuote(path.dirname(path.resolve(file)))}`, `MOSAIC_INSTANCE_DATA_ROOT=${shellQuote(dataRoot)}`, `MOSAIC_UNIT_SUFFIX=${shellQuote(isNamed(instance) ? `-${instance}` : "")}`, ...(proxy ? ["MOSAIC_INSTANCE_PROXY_CONFIGURED=1"] : []), "", ].join("\n"); } async function loadSelected(instance) { const file = configPath(instance); const resolved = await load(file, instance); if (isNamed(instance)) checkClaim(resolved.dataRoot, instance); return resolved; } const operation = process.argv[2]; try { if (!["bootstrap", "validate", "env", "instance"].includes(operation)) { fail(1, `unknown operation: ${plain(operation ?? "", "that is not one")} (expected bootstrap | validate | env | instance)`); } const instance = parseArgs(process.argv.slice(3)); switch (operation) { case "bootstrap": { const file = configPath(instance); if (isNamed(instance)) await bootstrapNamed(file, instance); else await bootstrapDefault(file); process.exit(0); } case "validate": { const resolved = await loadSelected(instance); process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`); process.exit(0); } case "env": { const resolved = await loadSelected(instance); process.stdout.write( [ `MOSAIC_DATA_ROOT=${shellQuote(resolved.dataRoot)}`, `MOSAIC_PROVIDER=${shellQuote(resolved.execution.provider)}`, `MOSAIC_MODEL=${shellQuote(resolved.execution.model)}`, `MOSAIC_ADAPTER=${shellQuote(resolved.execution.adapter)}`, "", ].join("\n"), ); process.exit(0); } case "instance": { process.stdout.write(await selected(instance)); process.exit(0); } } } catch (error) { if (!(error instanceof Failure)) throw error; process.stderr.write(`mosaic-config: ${error.message}\n`); process.exit(error.exitCode); }