// lib/incarnation.ts — process-launch incarnation identity (DESIGN HAZARD). // // Mercer-measured hazard, carried by the NG-1 tasking: Pi documents // PI_SESSION_ID as SESSION identity, not incarnation, and Mercer's // environment carries no incarnation variable. Per L2-D01/D05 we therefore // do NOT relabel PI_SESSION_ID as an incarnation — the R6 journal keys on // the identity minted here. // // Resolution order (pure function of process state + injectable rng): // 1. Launcher claim: MOSAIC_LAUNCH_INCARNATION, VALIDATED (NG-7 F2): the // claim becomes a FILENAME COMPONENT in the journal and goal-state // paths, so anything outside ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ is // IGNORED (no path separators, no leading dot, no traversal shape) and // resolution falls through — an invalid claim must never reach a path. // 2. Process-global: globalThis.__mosaicCoreIncarnation — set on first // mint. globalThis is per-process, so the identity survives extension // /reload (module re-import) and session switches WITHIN the process. // Deliberately NOT exported via process.env: child shells must not // inherit a forgeable-looking claim. // 3. Mint: rng() — DEFAULTS to crypto.randomUUID (NG-7 F1: the first cut // required an injectable rng with no default, and both production // call sites invoked incarnationIdentity() with no argument — a live // startup throw the test suites never caught because every arm // injected an rng or an explicit incarnationId). // // This function never reads PI_SESSION_ID. There is no code path here that // could conflate session identity with launch identity. import { randomUUID } from "node:crypto"; export const LAUNCH_CLAIM_ENV = "MOSAIC_LAUNCH_INCARNATION"; const GLOBAL_SLOT = "__mosaicCoreIncarnation"; /** A claim is usable only if it is a safe single path component. */ export const CLAIM_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; type Rng = () => string; export function validClaim(claim: string | undefined): claim is string { return typeof claim === "string" && CLAIM_RE.test(claim); } export function incarnationIdentity(rng: Rng = () => randomUUID()): string { const claim = process.env[LAUNCH_CLAIM_ENV]; if (validClaim(claim)) { return claim; } const g = globalThis as Record; const existing = g[GLOBAL_SLOT]; if (typeof existing === "string" && existing.length > 0) { return existing; } const minted = rng(); g[GLOBAL_SLOT] = minted; return minted; } /** Test/adapter helper: clear the process-global slot (never touches claims). */ export function resetIncarnationGlobal(): void { delete (globalThis as Record)[GLOBAL_SLOT]; }