/** * Activation-side supervisor for the Mosaic lease broker (issue #869, Point-1 * C3). #828 shipped fail-closed enforcement hooks (`mutator-gate.py`, * `receipt-observer-client.py`) with nothing that guaranteed `daemon.py` was * running or that its socket existed before a gated runtime started. This * module: * * - resolves the broker socket/state paths and the on-disk locations of the * supervisor artifacts, deterministically and consistently with * `defaultLeaseBrokerSocket` in `../commands/launch.ts`; * - idempotently applies (materializes) a systemd `--user` unit plus the * wrapper script and daemon sources it execs, mirroring the tmux fleet * unit convention in `framework/systemd/user/`; * - exposes a health predicate other cards (e.g. the C1 activation probe) * can call to learn whether a broker supervisor is present and healthy. * * `applyBrokerSupervisor` only writes files under the paths it is given. It * never runs `systemctl`, never starts `daemon.py`, and never touches a real * host's `~/.config` unless the caller explicitly resolves paths there. * Enabling/starting the unit is a separate, later, out-of-scope step. */ import { chmod, copyFile, mkdir, stat } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; const UNIT_NAME = 'mosaic-lease-broker.service'; const WRAPPER_SCRIPT_NAME = 'start-lease-broker.sh'; /** Co-located modules `daemon.py` imports at runtime; kept alongside it. */ const DAEMON_SOURCE_FILE_NAMES = [ 'daemon.py', 'lease_generation.py', 'normative_fragments.py', 'receipt_challenge.py', 'receipt_observer.py', ] as const; export interface ResolveBrokerSupervisorPathsOptions { /** `~/.config/mosaic` (or an override) — where installed tool copies live. */ mosaicHome: string; /** Root of the checked-out `framework/` directory (canonical file source). */ frameworkRoot: string; /** Defaults to `process.env`; pass a fake for tests. */ env?: NodeJS.ProcessEnv; /** Defaults to `os.homedir()`; pass a temp dir in tests. */ homeDir?: string; /** Defaults to `process.getuid()` (or 0); pass a fake for tests. */ uid?: number; } export interface BrokerSupervisorPaths { readonly mosaicHome: string; readonly frameworkRoot: string; readonly systemdUserDir: string; readonly leaseBrokerToolsDir: string; readonly unitSourcePath: string; readonly unitTargetPath: string; readonly wrapperSourcePath: string; readonly wrapperTargetPath: string; readonly daemonSourcePaths: readonly string[]; readonly daemonTargetPaths: readonly string[]; /** * Resolved with the same precedence as `defaultLeaseBrokerSocket` in * `../commands/launch.ts`: an explicit `MOSAIC_LEASE_BROKER_SOCKET`, else * `$XDG_RUNTIME_DIR/mosaic-lease/broker.sock`, else * `/run/user//mosaic-lease/broker.sock`. */ readonly socketPath: string; /** Colocated next to the socket, matching the broker's own generation-file convention. */ readonly statePath: string; } /** * Resolve the lease broker socket path alone, with the same precedence as * `defaultLeaseBrokerSocket` in `../commands/launch.ts`. Exported so callers * (and tests) can assert the two stay in agreement without importing the CLI * command module. */ export function resolveLeaseBrokerSocketPath( env: NodeJS.ProcessEnv = process.env, uid: number = typeof process.getuid === 'function' ? process.getuid() : 0, ): string { const explicit = env['MOSAIC_LEASE_BROKER_SOCKET']; if (explicit) return explicit; const runtimeDir = env['XDG_RUNTIME_DIR']; if (runtimeDir) return join(runtimeDir, 'mosaic-lease', 'broker.sock'); return join('/run/user', String(uid), 'mosaic-lease', 'broker.sock'); } /** Resolve every path the supervisor apply/health functions need, deterministically. */ export function resolveBrokerSupervisorPaths( options: ResolveBrokerSupervisorPathsOptions, ): BrokerSupervisorPaths { const { mosaicHome, frameworkRoot } = options; const env = options.env ?? process.env; const homeDir = options.homeDir ?? homedir(); const systemdUserDir = join(homeDir, '.config', 'systemd', 'user'); const leaseBrokerToolsDir = join(mosaicHome, 'tools', 'lease-broker'); const frameworkLeaseBrokerDir = join(frameworkRoot, 'tools', 'lease-broker'); const socketPath = resolveLeaseBrokerSocketPath(env, options.uid); const statePath = join(dirname(socketPath), 'state.json'); return { mosaicHome, frameworkRoot, systemdUserDir, leaseBrokerToolsDir, unitSourcePath: join(frameworkRoot, 'systemd', 'user', UNIT_NAME), unitTargetPath: join(systemdUserDir, UNIT_NAME), wrapperSourcePath: join(frameworkLeaseBrokerDir, WRAPPER_SCRIPT_NAME), wrapperTargetPath: join(leaseBrokerToolsDir, WRAPPER_SCRIPT_NAME), daemonSourcePaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(frameworkLeaseBrokerDir, name)), daemonTargetPaths: DAEMON_SOURCE_FILE_NAMES.map((name) => join(leaseBrokerToolsDir, name)), socketPath, statePath, }; } export interface ApplyBrokerSupervisorResult { readonly installedFiles: readonly string[]; } /** * Idempotently materialize the supervisor unit, its wrapper script, and the * daemon sources it execs. Safe to call on every reseed: every write is a * deterministic overwrite of the same target path from the same source, so a * second call reproduces identical bytes/modes and never errors. * * Never runs `systemctl`; the caller decides separately whether/when to * `daemon-reload`/`enable`/`start` the installed unit. */ export async function applyBrokerSupervisor( paths: BrokerSupervisorPaths, ): Promise { await mkdir(paths.leaseBrokerToolsDir, { recursive: true }); await mkdir(paths.systemdUserDir, { recursive: true }); const installedFiles: string[] = []; for (let index = 0; index < paths.daemonSourcePaths.length; index += 1) { const source = paths.daemonSourcePaths[index]; const target = paths.daemonTargetPaths[index]; if (source === undefined || target === undefined) continue; await copyFile(source, target); await chmod(target, 0o644); installedFiles.push(target); } await copyFile(paths.wrapperSourcePath, paths.wrapperTargetPath); await chmod(paths.wrapperTargetPath, 0o755); installedFiles.push(paths.wrapperTargetPath); await copyFile(paths.unitSourcePath, paths.unitTargetPath); await chmod(paths.unitTargetPath, 0o644); installedFiles.push(paths.unitTargetPath); return { installedFiles }; } export interface BrokerSupervisorHealth { /** Whether the systemd unit file has been materialized at its target path. */ readonly unitInstalled: boolean; /** Whether a Unix domain socket currently exists at the resolved socket path. */ readonly socketPresent: boolean; /** * The signal other cards (e.g. C1's activation probe) should treat as * "a broker supervisor is present and healthy". Presence of a live socket * is the authoritative signal: a gated runtime can only ever succeed by * connecting to it, so this is what fail-closed callers must check. */ readonly healthy: boolean; } /** * Report the supervisor's on-disk/health signals. Never throws for an * absent unit or socket — both simply report `false`; unexpected filesystem * errors (permission issues, etc.) still propagate. */ export async function checkBrokerSupervisorHealth( paths: Pick, ): Promise { const [unitInstalled, socketPresent] = await Promise.all([ pathExists(paths.unitTargetPath), isUnixSocket(paths.socketPath), ]); return { unitInstalled, socketPresent, healthy: socketPresent }; } /** Convenience boolean form of {@link checkBrokerSupervisorHealth} for simple call sites. */ export async function isBrokerSupervisorHealthy( paths: Pick, ): Promise { return (await checkBrokerSupervisorHealth(paths)).healthy; } async function pathExists(path: string): Promise { try { await stat(path); return true; } catch (error) { if (isEnoent(error)) return false; throw error; } } async function isUnixSocket(path: string): Promise { try { const info = await stat(path); return info.isSocket(); } catch (error) { if (isEnoent(error)) return false; throw error; } } function isEnoent(error: unknown): boolean { return ( typeof error === 'object' && error !== null && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT' ); }