@camstack/agent 1.1.33 → 1.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../node-root/src/index.ts","../../node-root/src/root-package-spec.ts","../../node-root/src/root-state.ts","../../node-root/src/boot-plan.ts","../../node-root/src/semver-compare.ts","../../node-root/src/workspace-detect.ts","../../node-root/src/starter-core.ts","../../node-root/src/root-update-service.ts"],"sourcesContent":["/**\n * @camstack/node-root — the shared, zero-runtime-dep node-root boot core.\n *\n * PRIVATE workspace package: it is BUNDLED at build time into the hub's\n * `dist/server-root/index.js` (tsup) and into the agent's tsup output —\n * never published, never resolved from node_modules at runtime. Both node\n * starters and both update services are thin per-node adapters over these\n * modules, so the boot-plan / probation / rollback semantics can never\n * drift between the hub and the agent.\n *\n * Spec: docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md\n */\nexport * from './root-package-spec.js'\nexport * from './root-state.js'\nexport * from './boot-plan.js'\nexport * from './semver-compare.js'\nexport * from './workspace-detect.js'\nexport * from './starter-core.js'\nexport * from './root-update-service.js'\n","/**\n * Root-package spec — the per-node-type parameters of the shared node-root\n * boot machinery. Everything else (the `server-root/` data-dir layout, the\n * `state.json` schema, the probation/rollback boot plan, the update service)\n * is IDENTICAL between the hub and the agent; only the root package's name\n * and its loadable entry differ.\n *\n * Spec: docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md\n */\n\nexport interface RootPackageSpec {\n /** The npm name of the node's root package. */\n readonly packageName: string\n /**\n * Path segments of the loadable entry INSIDE the root package dir\n * (`node_modules/<packageName>/<...entryRelPath>`).\n */\n readonly entryRelPath: readonly string[]\n}\n\n/** The hub's root package — the starter loads its launcher. */\nexport const HUB_ROOT_SPEC: RootPackageSpec = {\n packageName: '@camstack/server',\n entryRelPath: ['dist', 'launcher.js'],\n}\n\n/** The agent's root package — the starter loads its CLI entry. */\nexport const AGENT_ROOT_SPEC: RootPackageSpec = {\n packageName: '@camstack/agent',\n entryRelPath: ['dist', 'cli.js'],\n}\n","/**\n * Data-dir server-root layout + state file — ZERO-DEP (node:fs/node:path only).\n *\n * Loaded by each node's baked STARTER before any node_modules resolution is\n * trusted, so this module must never import from `@camstack/*` (runtime) or\n * any npm package. The hub's `ServerUpdateService` and the agent's update\n * service consume the same module — one source of truth for the layout and\n * the state shape. The layout is IDENTICAL on both node types (the dir is\n * named `server-root/` everywhere); only the root package name + entry differ\n * and arrive via {@link RootPackageSpec}.\n *\n * Layout (`<dataDir>/server-root/`):\n * versions/<semver>/ npm-installed closure for one version\n * node_modules/<pkg>/<entry> ← the loadable entry (spec-defined)\n * state.json pointer file (ServerRootState, atomic)\n *\n * A POINTER FILE (state.json) is used instead of `current`/`previous`\n * symlinks: atomic via tmp+rename, works on every fs, and carries the\n * probation/rollback bookkeeping in the same durable record.\n *\n * Spec: docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md\n */\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport type { RootPackageSpec } from './root-package-spec.js'\n\nexport const SERVER_ROOT_DIRNAME = 'server-root'\nexport const SERVER_ROOT_STATE_FILE = 'state.json'\n/**\n * Restart-intent marker filename. Written by the update service moments before\n * a graceful self-restart (apply/rollback), then read + consumed by an\n * external process supervisor so it can tell an INTENTIONAL restart-for-update\n * apart from a crash — a crash-backoff policy must never penalise an update\n * restart. Lives in the server-root dir next to `state.json`.\n *\n * (Phase 3 — the Electron shell supervisor reads it; the docker restart policy\n * relaunches unconditionally so the marker is a harmless no-op there.)\n */\nexport const RESTART_INTENT_FILE = '.restart-intent'\n\n// ---------------------------------------------------------------------------\n// State shape\n// ---------------------------------------------------------------------------\n\nexport interface ServerRootPendingBoot {\n /** The freshly-staged version awaiting its probation boot. */\n readonly version: string\n /** The version that was current when the update was requested (null = baked). */\n readonly fromVersion: string | null\n readonly requestedAtMs: number\n /** 0 = not yet booted; >=1 = a probation boot started and never confirmed. */\n readonly bootAttempts: number\n}\n\nexport interface ServerRootRollbackInfo {\n readonly fromVersion: string\n /** null = rolled back all the way to the baked seed. */\n readonly toVersion: string | null\n readonly atMs: number\n readonly reason: string\n}\n\nexport interface ServerRootState {\n readonly schemaVersion: 1\n /** The confirmed-healthy active version (null = boot from the baked seed). */\n readonly currentVersion: string | null\n /** N-1 kept for rollback (its versions/ dir is retained). */\n readonly previousVersion: string | null\n readonly pendingBoot: ServerRootPendingBoot | null\n /** Set when the most recent update attempt was rolled back. */\n readonly rolledBack: ServerRootRollbackInfo | null\n}\n\nexport function emptyServerRootState(): ServerRootState {\n return {\n schemaVersion: 1,\n currentVersion: null,\n previousVersion: null,\n pendingBoot: null,\n rolledBack: null,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Layout helpers\n// ---------------------------------------------------------------------------\n\nexport function serverRootDir(dataDir: string): string {\n return path.join(dataDir, SERVER_ROOT_DIRNAME)\n}\n\nexport function versionsDir(rootDir: string): string {\n return path.join(rootDir, 'versions')\n}\n\nexport function versionDir(rootDir: string, version: string): string {\n return path.join(versionsDir(rootDir), version)\n}\n\n/** `versions/<v>/node_modules/<packageName>` — the installed root package dir. */\nexport function rootPackageDir(versionDirPath: string, spec: RootPackageSpec): string {\n return path.join(versionDirPath, 'node_modules', ...spec.packageName.split('/'))\n}\n\n/** The loadable entry inside the installed root package dir. */\nexport function rootEntryPath(versionDirPath: string, spec: RootPackageSpec): string {\n return path.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath)\n}\n\nexport function stateFilePath(rootDir: string): string {\n return path.join(rootDir, SERVER_ROOT_STATE_FILE)\n}\n\n// ---------------------------------------------------------------------------\n// State IO (atomic, corruption-tolerant)\n// ---------------------------------------------------------------------------\n\nfunction isNullableString(v: unknown): v is string | null {\n return v === null || typeof v === 'string'\n}\n\nfunction isPendingBoot(v: unknown): v is ServerRootPendingBoot {\n if (typeof v !== 'object' || v === null) return false\n const p = v as Record<string, unknown>\n return (\n typeof p['version'] === 'string' &&\n isNullableString(p['fromVersion']) &&\n typeof p['requestedAtMs'] === 'number' &&\n typeof p['bootAttempts'] === 'number'\n )\n}\n\nfunction isRollbackInfo(v: unknown): v is ServerRootRollbackInfo {\n if (typeof v !== 'object' || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r['fromVersion'] === 'string' &&\n isNullableString(r['toVersion']) &&\n typeof r['atMs'] === 'number' &&\n typeof r['reason'] === 'string'\n )\n}\n\nexport function isServerRootState(v: unknown): v is ServerRootState {\n if (typeof v !== 'object' || v === null) return false\n const s = v as Record<string, unknown>\n if (s['schemaVersion'] !== 1) return false\n if (!isNullableString(s['currentVersion'])) return false\n if (!isNullableString(s['previousVersion'])) return false\n if (s['pendingBoot'] !== null && !isPendingBoot(s['pendingBoot'])) return false\n if (s['rolledBack'] !== null && !isRollbackInfo(s['rolledBack'])) return false\n return true\n}\n\n/** Read + validate the state file. Returns null when missing/corrupt/mismatched. */\nexport function readServerRootState(rootDir: string): ServerRootState | null {\n try {\n const raw: unknown = JSON.parse(fs.readFileSync(stateFilePath(rootDir), 'utf-8'))\n return isServerRootState(raw) ? raw : null\n } catch {\n return null\n }\n}\n\n/** Write the state file atomically (tmp sibling + rename). Creates rootDir. */\nexport function writeServerRootState(rootDir: string, state: ServerRootState): void {\n fs.mkdirSync(rootDir, { recursive: true })\n const target = stateFilePath(rootDir)\n const tmp = `${target}.tmp`\n fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf-8')\n fs.renameSync(tmp, target)\n}\n\n// ---------------------------------------------------------------------------\n// Restart-intent marker (supervisor: update-restart vs crash discrimination)\n// ---------------------------------------------------------------------------\n\n/**\n * Payload of the restart-intent marker. `requestedAtMs` lets a reader ignore a\n * STALE marker (e.g. one left behind by a hard kill between write and\n * relaunch) via a freshness window instead of mis-classifying a much-later\n * crash as an update restart. `reason` carries the human `requestedBy` string.\n */\nexport interface RestartIntentMarker {\n readonly requestedAtMs: number\n readonly reason: string\n}\n\nexport function restartIntentMarkerPath(rootDir: string): string {\n return path.join(rootDir, RESTART_INTENT_FILE)\n}\n\nfunction isRestartIntentMarker(v: unknown): v is RestartIntentMarker {\n if (typeof v !== 'object' || v === null) return false\n const m = v as Record<string, unknown>\n return typeof m['requestedAtMs'] === 'number' && typeof m['reason'] === 'string'\n}\n\n/** Write the marker atomically (tmp sibling + rename). Creates rootDir. */\nexport function writeRestartIntentMarker(rootDir: string, marker: RestartIntentMarker): void {\n fs.mkdirSync(rootDir, { recursive: true })\n const target = restartIntentMarkerPath(rootDir)\n const tmp = `${target}.tmp`\n fs.writeFileSync(tmp, JSON.stringify(marker, null, 2), 'utf-8')\n fs.renameSync(tmp, target)\n}\n\n/** Read + validate the marker. Returns null when missing/corrupt. */\nexport function readRestartIntentMarker(rootDir: string): RestartIntentMarker | null {\n try {\n const raw: unknown = JSON.parse(fs.readFileSync(restartIntentMarkerPath(rootDir), 'utf-8'))\n return isRestartIntentMarker(raw) ? raw : null\n } catch {\n return null\n }\n}\n\n/** Delete the marker if present (idempotent — a missing file is not an error). */\nexport function clearRestartIntentMarker(rootDir: string): void {\n try {\n fs.rmSync(restartIntentMarkerPath(rootDir), { force: true })\n } catch {\n /* best-effort */\n }\n}\n\n// ---------------------------------------------------------------------------\n// Version-dir validation\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the minimum required Node major from an `engines.node` range like\n * `>=22`, `>= 22.12`, `^24.0.0`, `22.x`. Returns null when unparseable\n * (validation is then permissive — a bogus range must not brick boot).\n */\nexport function minNodeMajorOf(enginesNode: string): number | null {\n const match = /(\\d+)/.exec(enginesNode)\n if (match === null) return null\n const major = Number.parseInt(match[1] ?? '', 10)\n return Number.isNaN(major) ? null : major\n}\n\n/**\n * Validate that `versions/<version>` holds a loadable root-package closure\n * for `spec`. Returns null when valid, else a human-readable reason. Checks:\n * 1. the entry file exists,\n * 2. the package.json parses with the right `name` + `version`,\n * 3. `engines.node` (when parseable) is satisfied by the running runtime —\n * the starter refuses a native-ABI mismatch with a clear error instead\n * of segfaulting (design risk: node bump vs shm-ring prebuilds).\n */\nexport function validateVersionDir(\n rootDir: string,\n version: string,\n nodeMajor: number,\n spec: RootPackageSpec,\n): string | null {\n const vDir = versionDir(rootDir, version)\n const entry = rootEntryPath(vDir, spec)\n if (!fs.existsSync(entry)) {\n return `root entry missing: ${entry}`\n }\n\n const pkgJsonPath = path.join(rootPackageDir(vDir, spec), 'package.json')\n let pkg: Record<string, unknown>\n try {\n const raw: unknown = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n return `package.json malformed: ${pkgJsonPath}`\n }\n pkg = raw as Record<string, unknown>\n } catch {\n return `package.json unreadable: ${pkgJsonPath}`\n }\n\n if (pkg['name'] !== spec.packageName) {\n return `package name mismatch: expected ${spec.packageName}, got ${String(pkg['name'])}`\n }\n if (pkg['version'] !== version) {\n return `package version mismatch: dir says ${version}, package.json says ${String(pkg['version'])}`\n }\n\n const engines = pkg['engines']\n if (typeof engines === 'object' && engines !== null) {\n const enginesNode = (engines as Record<string, unknown>)['node']\n if (typeof enginesNode === 'string') {\n const minMajor = minNodeMajorOf(enginesNode)\n if (minMajor !== null && nodeMajor < minMajor) {\n return `engines.node \"${enginesNode}\" requires Node >= ${minMajor}, runtime is ${nodeMajor}`\n }\n }\n }\n\n return null\n}\n","/**\n * Pure boot-plan decision for the server-root starter — ZERO-DEP.\n *\n * Given the durable `ServerRootState` and a validity predicate for staged\n * version dirs, decide WHICH closure this boot should load and what the\n * state file must be updated to. All filesystem access is behind the\n * injected `isValidVersion` callback so the decision itself is unit-testable.\n *\n * Probation / rollback protocol (mirrors `launcher-framework-swap.ts`):\n * - `pendingBoot.bootAttempts === 0` → this is the probation boot: bump the\n * counter durably BEFORE loading the new version. The hub clears the\n * pending record (promoting the version to `currentVersion`) only once it\n * reaches ready (`ServerUpdateService.confirmBootHealthy`).\n * - `pendingBoot.bootAttempts >= 1` → the previous probation boot died\n * before confirming: auto-rollback to `currentVersion` (or baked) and\n * record `rolledBack` so the status cap can surface it.\n */\nimport type { ServerRootState } from './root-state.js'\n\nexport type BootPlan =\n | {\n readonly kind: 'baked'\n readonly reason: string\n readonly stateToWrite: ServerRootState | null\n }\n | {\n readonly kind: 'data-root'\n readonly version: string\n /** True when this boot must be confirmed healthy (freshly-staged version). */\n readonly probation: boolean\n readonly stateToWrite: ServerRootState | null\n }\n\ntype IsValidVersion = (version: string) => boolean\ntype NowFn = () => number\n\n/**\n * Decide the boot target. Never throws; never mutates `state`.\n * `stateToWrite` is non-null whenever the durable state must change\n * (probation counter bump, rollback, invalid-version eviction).\n */\nexport function planBoot(\n state: ServerRootState | null,\n isValidVersion: IsValidVersion,\n now: NowFn,\n): BootPlan {\n if (state === null) {\n return { kind: 'baked', reason: 'no server-root state', stateToWrite: null }\n }\n\n let next: ServerRootState = state\n let changed = false\n\n // ── Pending (freshly-staged) version ──────────────────────────────────\n if (next.pendingBoot !== null) {\n const pending = next.pendingBoot\n if (pending.bootAttempts >= 1) {\n // Previous probation boot never confirmed healthy — roll back.\n next = {\n ...next,\n pendingBoot: null,\n rolledBack: {\n fromVersion: pending.version,\n toVersion: next.currentVersion,\n atMs: now(),\n reason: 'probation boot did not reach ready — rolled back',\n },\n }\n changed = true\n } else if (!isValidVersion(pending.version)) {\n next = {\n ...next,\n pendingBoot: null,\n rolledBack: {\n fromVersion: pending.version,\n toVersion: next.currentVersion,\n atMs: now(),\n reason: 'staged version failed validation',\n },\n }\n changed = true\n } else {\n // Probation boot: bump the attempt counter durably, keep currentVersion\n // untouched until the hub confirms the boot healthy.\n return {\n kind: 'data-root',\n version: pending.version,\n probation: true,\n stateToWrite: {\n ...next,\n pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 },\n },\n }\n }\n }\n\n // ── Confirmed current version ──────────────────────────────────────────\n if (next.currentVersion !== null) {\n if (isValidVersion(next.currentVersion)) {\n return {\n kind: 'data-root',\n version: next.currentVersion,\n probation: false,\n stateToWrite: changed ? next : null,\n }\n }\n\n // Current version dir is broken — try N-1, else baked.\n const broken = next.currentVersion\n if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {\n const fallback = next.previousVersion\n return {\n kind: 'data-root',\n version: fallback,\n probation: false,\n stateToWrite: {\n ...next,\n currentVersion: fallback,\n previousVersion: null,\n rolledBack: {\n fromVersion: broken,\n toVersion: fallback,\n atMs: now(),\n reason: 'active version failed validation',\n },\n },\n }\n }\n\n return {\n kind: 'baked',\n reason: `active version ${broken} failed validation and no valid previous version exists`,\n stateToWrite: {\n ...next,\n currentVersion: null,\n rolledBack: {\n fromVersion: broken,\n toVersion: null,\n atMs: now(),\n reason: 'active version failed validation',\n },\n },\n }\n }\n\n return {\n kind: 'baked',\n reason: 'no active data-root version',\n stateToWrite: changed ? next : null,\n }\n}\n","/**\n * Minimal semver comparison — zero-dep (used by the starter, which must never\n * import from node_modules). Handles `MAJOR.MINOR.PATCH[-prerelease]`:\n * numeric segment compare, missing segments = 0, prerelease < its release,\n * prereleases of the same base compare lexically. Malformed segments compare\n * as 0 (best effort — never throws).\n */\n\ntype SemverParts = {\n readonly nums: readonly number[]\n readonly prerelease: string | null\n}\n\nfunction parse(version: string): SemverParts {\n const dashIdx = version.indexOf('-')\n const base = dashIdx === -1 ? version : version.slice(0, dashIdx)\n const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1)\n const nums = base.split('.').map((seg) => {\n const n = Number.parseInt(seg, 10)\n return Number.isNaN(n) ? 0 : n\n })\n return { nums, prerelease }\n}\n\n/** Returns -1 when a < b, 0 when equal, 1 when a > b. */\nexport function compareSemver(a: string, b: string): -1 | 0 | 1 {\n const pa = parse(a)\n const pb = parse(b)\n const len = Math.max(pa.nums.length, pb.nums.length)\n for (let i = 0; i < len; i++) {\n const na = pa.nums[i] ?? 0\n const nb = pb.nums[i] ?? 0\n if (na < nb) return -1\n if (na > nb) return 1\n }\n if (pa.prerelease === null && pb.prerelease === null) return 0\n if (pa.prerelease === null) return 1\n if (pb.prerelease === null) return -1\n if (pa.prerelease < pb.prerelease) return -1\n if (pa.prerelease > pb.prerelease) return 1\n return 0\n}\n","/**\n * Workspace-checkout detection for the starter — ZERO-DEP.\n *\n * The starter must defer to plain resolution in a dev checkout (`npm run\n * dev:full` / `npm start` from the monorepo): the data-dir server root is a\n * production mechanism and must never shadow freshly-built workspace code.\n * Detection = any ancestor package.json declaring `workspaces` (the monorepo\n * root has one; the baked image run-dir's `npm init -y` package.json and the\n * data-dir version closures never do).\n */\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\n\n/**\n * Walk up from `fromDir` looking for a package.json with a `workspaces`\n * field. Returns the directory containing it, or null when none exists.\n */\nexport function detectWorkspaceRoot(fromDir: string): string | null {\n let dir = path.resolve(fromDir)\n for (;;) {\n const pkgPath = path.join(dir, 'package.json')\n try {\n const raw: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n if (\n typeof raw === 'object' &&\n raw !== null &&\n (raw as Record<string, unknown>)['workspaces'] !== undefined\n ) {\n return dir\n }\n } catch {\n // missing / unreadable package.json — keep walking\n }\n const parent = path.dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n","/**\n * Shared STARTER core — the boot flow both node starters (hub + agent) run.\n * ZERO-DEP: bundled into each starter at build time; must never resolve\n * anything from node_modules at runtime.\n *\n * Boot sequence (identical on both node types; per-node names arrive via\n * {@link NodeStarterOptions}):\n * 1. Workspace checkout? → defer to plain resolution (load the sibling\n * seed entry; the dev loop never runs the built starter at all).\n * 2. Read `<dataDir>/server-root/state.json`, run the pure boot plan\n * (probation counter / auto-rollback / validity fallback) and persist\n * any state change BEFORE loading code.\n * 3. Data-root chosen → register in-process resolver hooks so the node's\n * MAIN process resolves the host-external packages (@camstack/system, …)\n * from the ACTIVE root's node_modules, then load that root's entry.\n * 4. Otherwise → load the baked seed entry next to the starter.\n *\n * Console usage is intentional: the starter runs before any logging exists.\n * Every line is prefixed `[starter]`.\n */\nimport * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { planBoot } from './boot-plan.js'\nimport type { RootPackageSpec } from './root-package-spec.js'\nimport {\n readServerRootState,\n rootEntryPath,\n serverRootDir,\n validateVersionDir,\n versionDir,\n writeServerRootState,\n} from './root-state.js'\nimport { detectWorkspaceRoot } from './workspace-detect.js'\n\n/**\n * Mirror of the host-external list in\n * `packages/system/src/kernel/moleculer/framework-resolver-hook.mjs` — kept\n * inline because the starter must stay zero-dep. Only these specifiers are\n * redirected to the active root; everything else resolves normally.\n */\nexport const HOST_EXTERNAL_SPECIFIERS: readonly string[] = [\n '@camstack/system',\n '@camstack/shm-ring',\n '@camstack/types',\n '@camstack/sdk',\n 'zod',\n '@trpc/server',\n '@trpc/client',\n 'sharp',\n 'node-av',\n]\n\nexport function isHostExternal(specifier: string): boolean {\n return HOST_EXTERNAL_SPECIFIERS.some(\n (name) => specifier === name || specifier.startsWith(`${name}/`),\n )\n}\n\ntype ResolveContext = { readonly parentURL?: string }\ntype NextResolve = (specifier: string, context?: ResolveContext) => unknown\ntype RegisterHooksFn = (hooks: {\n resolve: (specifier: string, context: ResolveContext, nextResolve: NextResolve) => unknown\n}) => void\n\n/**\n * Register in-process resolver hooks (Node >= 22.15 `module.registerHooks`,\n * covers BOTH require and import) that anchor host-external resolution inside\n * the active root's node_modules. Failure-tolerant: if the anchored resolve\n * fails (package absent from the closure, e.g. sharp/node-av), fall back to\n * the default walk (NODE_PATH /data/framework still covers those).\n */\nexport function registerActiveRootResolver(activeRootDir: string): void {\n // Documented type boundary: `module.registerHooks` is not yet in the\n // bundled @types/node — feature-detect it off the untyped module surface.\n const moduleApi: Record<string, unknown> = require('node:module')\n const registerHooks = moduleApi['registerHooks']\n if (typeof registerHooks !== 'function') {\n console.warn(\n '[starter] module.registerHooks unavailable (Node < 22.15) — host-external redirect skipped',\n )\n return\n }\n const anchorURL = pathToFileURL(\n path.join(activeRootDir, 'node_modules', '__camstack_starter_anchor__.js'),\n ).href\n const hooks: Parameters<RegisterHooksFn>[0] = {\n resolve: (specifier, context, nextResolve) => {\n if (isHostExternal(specifier)) {\n try {\n return nextResolve(specifier, { ...context, parentURL: anchorURL })\n } catch {\n // Not in the active root's closure — fall through to default.\n }\n }\n return nextResolve(specifier, context)\n },\n }\n ;(registerHooks as RegisterHooksFn)(hooks)\n}\n\n/** Per-node env-var names the starter writes / reads. */\nexport interface StarterEnvNames {\n /** Boot-mode marker read by the node's update service (`workspace`/`baked`/`data-root`). */\n readonly bootMode: string\n /** Absolute path of the active data-root version dir (data-root boots only). */\n readonly activeRoot: string\n /** Active data-root version (data-root boots only). */\n readonly activeVersion: string\n /** Kill switch: `<killSwitch>=off` forces the baked seed closure. */\n readonly killSwitch: string\n}\n\nexport interface NodeStarterOptions {\n readonly spec: RootPackageSpec\n readonly envNames: StarterEnvNames\n /** The node's resolved data dir (owner-specific env/argv resolution). */\n readonly dataDir: string\n /** Absolute path of the baked seed entry sibling to the starter. */\n readonly seedEntry: string\n /** The starter's own dir (`__dirname`) — anchors workspace detection. */\n readonly starterDir: string\n /** Load an entry file (a plain CJS `require` in the real starters). */\n readonly loadEntry: (entryPath: string) => void\n /** Test seams. Defaults: `process.env`, `Date.now`, running node major. */\n readonly env?: NodeJS.ProcessEnv\n readonly now?: () => number\n readonly nodeMajor?: number\n readonly registerResolver?: (activeRootDir: string) => void\n}\n\n/**\n * Run the shared starter flow. Never throws for boot-plan reasons — every\n * failure path degrades to the baked seed. (A `loadEntry` throw propagates:\n * that is the caller's fatal handler's job.)\n */\nexport function runNodeStarter(options: NodeStarterOptions): void {\n const env = options.env ?? process.env\n const now = options.now ?? Date.now\n const registerResolver = options.registerResolver ?? registerActiveRootResolver\n\n // ── 1. Workspace checkout → plain resolution, dev loop untouched ────────\n const disabled = env[options.envNames.killSwitch] === 'off'\n const workspaceRoot = detectWorkspaceRoot(options.starterDir)\n if (workspaceRoot !== null) {\n console.log(\n `[starter] workspace checkout detected at ${workspaceRoot} — using plain resolution`,\n )\n env[options.envNames.bootMode] = 'workspace'\n options.loadEntry(options.seedEntry)\n return\n }\n if (disabled) {\n console.log(`[starter] ${options.envNames.killSwitch}=off — booting the baked seed closure`)\n env[options.envNames.bootMode] = 'baked'\n options.loadEntry(options.seedEntry)\n return\n }\n\n // ── 2. Boot plan from the durable server-root state ─────────────────────\n const rootDir = serverRootDir(options.dataDir)\n const state = readServerRootState(rootDir)\n const nodeMajor =\n options.nodeMajor ?? Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10)\n const isValidVersion = (version: string): boolean => {\n const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec)\n if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`)\n return reason === null\n }\n\n const plan = planBoot(state, isValidVersion, now)\n if (plan.stateToWrite !== null) {\n try {\n writeServerRootState(rootDir, plan.stateToWrite)\n } catch (err) {\n // A state write failure must not brick boot — fall back to the seed\n // (booting a probation version WITHOUT the durable attempt counter\n // would disarm the crash-loop rollback).\n console.error('[starter] FAILED to persist server-root state — booting baked seed:', err)\n env[options.envNames.bootMode] = 'baked'\n options.loadEntry(options.seedEntry)\n return\n }\n if (\n plan.stateToWrite.rolledBack !== null &&\n (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack\n ) {\n const rb = plan.stateToWrite.rolledBack\n console.warn(\n `[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? 'baked seed'}: ${rb.reason}`,\n )\n }\n }\n\n // ── 3. Load the chosen closure ───────────────────────────────────────────\n if (plan.kind === 'data-root') {\n const activeRootDir = versionDir(rootDir, plan.version)\n const entry = rootEntryPath(activeRootDir, options.spec)\n console.log(\n `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` +\n (plan.probation ? ' (probation boot — health-check armed)' : ''),\n )\n env[options.envNames.bootMode] = 'data-root'\n env[options.envNames.activeRoot] = activeRootDir\n env[options.envNames.activeVersion] = plan.version\n registerResolver(activeRootDir)\n options.loadEntry(entry)\n return\n }\n\n console.log(`[starter] booting baked seed closure (${plan.reason})`)\n env[options.envNames.bootMode] = 'baked'\n options.loadEntry(options.seedEntry)\n}\n","/**\n * RootUpdateService — the shared install/update/rollback engine for a node's\n * runtime-updatable ROOT package (`@camstack/server` on the hub,\n * `@camstack/agent` on agents). Phase 2 of the runtime-updatable node\n * packages design: the hub's `ServerUpdateService` and the agent's\n * `AgentUpdateService` are thin adapters over this one class, so the\n * security-review fixes (rollback reentrancy guards, fresh-transient prune\n * protection, corrupt-state visibility) exist in exactly one place.\n *\n * Responsibilities:\n * - stage a new `<pkg>@X` closure into\n * `<dataDir>/server-root/versions/<X>/` (npm install into a same-fs\n * staging dir + atomic rename activation),\n * - keep exactly one previous version (N-1) for rollback,\n * - drive the probation-boot protocol via the durable `state.json`\n * (`pendingBoot` armed here, consumed by the STARTER on the next boot,\n * promoted by `confirmBootHealthy()` once the node reaches ready),\n * - back the `server-management` cap methods on every node type.\n *\n * The starter core (starter-core.ts) owns the OTHER half: choosing the\n * closure at boot, bumping the probation counter, auto-rolling-back a\n * version whose probation boot never confirmed.\n *\n * Spec: docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md\n */\nimport { execFile } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { promisify } from 'node:util'\nimport type {\n ServerBootMode,\n ServerPackageStatus,\n ServerUpdateActionResult,\n ServerUpdateCheckResult,\n ServerUpdateState,\n} from '@camstack/types'\nimport type { RootPackageSpec } from './root-package-spec.js'\nimport { compareSemver } from './semver-compare.js'\nimport {\n emptyServerRootState,\n readServerRootState,\n rootEntryPath,\n rootPackageDir,\n serverRootDir,\n stateFilePath,\n validateVersionDir,\n versionDir,\n versionsDir,\n writeServerRootState,\n type ServerRootState,\n} from './root-state.js'\nimport { detectWorkspaceRoot } from './workspace-detect.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** npm registry override args — mirrors addon-package.service (module-local there). */\nfunction buildNpmRegistryArgs(registry: string | undefined): readonly string[] {\n if (registry === undefined || registry.length === 0) return []\n return ['--registry', registry, `--@camstack:registry=${registry}`]\n}\n\nexport type NpmExecFn = (\n args: readonly string[],\n opts: { readonly cwd?: string; readonly timeoutMs: number },\n) => Promise<{ stdout: string }>\n\n/**\n * Structural logger surface — satisfied by the hub's `IScopedLogger` AND the\n * agent's console logger, without a runtime dependency on `@camstack/types`.\n */\nexport interface RootUpdateLogger {\n debug(message: string, context?: { readonly meta?: Record<string, unknown> }): void\n info(message: string, context?: { readonly meta?: Record<string, unknown> }): void\n warn(message: string, context?: { readonly meta?: Record<string, unknown> }): void\n error(message: string, context?: { readonly meta?: Record<string, unknown> }): void\n}\n\n/** Per-node env-var names the update service reads (written by the starter). */\nexport interface RootUpdateEnvNames {\n /** `workspace` / `baked` / `data-root` marker set by the starter. */\n readonly bootMode: string\n /** Active data-root version (data-root boots only). */\n readonly activeVersion: string\n /** Directory of the immutable baked seed root package (image-set). */\n readonly seedDir: string\n}\n\nexport interface RootUpdateServiceOptions {\n readonly spec: RootPackageSpec\n readonly envNames: RootUpdateEnvNames\n readonly logger: RootUpdateLogger\n /** Graceful restart seam (hub: AddonPackageService.restartServer; agent: SIGTERM self). */\n readonly restartServer: (requestedBy: string) => void\n /** The node's resolved data dir (adapters own the per-node default). */\n readonly dataDir: string\n /** package.json of the RUNNING root-package copy (adapters own the default). */\n readonly runningPackageJsonPath: string\n /** Anchor for the boot-mode fallback classification (adapter `__dirname`). */\n readonly workspaceProbeDir: string\n /** Overrides for tests. */\n readonly execNpm?: NpmExecFn\n readonly env?: NodeJS.ProcessEnv\n readonly now?: () => number\n}\n\ninterface CheckCache {\n readonly latestVersion: string | null\n readonly checkedAtMs: number\n readonly error: string | null\n}\n\nconst NPM_VIEW_TIMEOUT_MS = 20_000\nconst NPM_INSTALL_TIMEOUT_MS = 15 * 60_000\nconst RESTART_REASON_PREFIX = 'server-update'\n\nfunction readPackageVersion(pkgJsonPath: string): string | null {\n try {\n const raw: unknown = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))\n if (typeof raw === 'object' && raw !== null) {\n const version = (raw as Record<string, unknown>)['version']\n if (typeof version === 'string') return version\n }\n } catch {\n // missing / malformed — treated as unknown\n }\n return null\n}\n\nexport class RootUpdateService {\n private readonly spec: RootPackageSpec\n private readonly envNames: RootUpdateEnvNames\n private readonly logger: RootUpdateLogger\n private readonly restartServerFn: (requestedBy: string) => void\n private readonly dataDir: string\n private readonly execNpm: NpmExecFn\n private readonly env: NodeJS.ProcessEnv\n private readonly now: () => number\n private readonly runningPackageJsonPath: string\n private readonly workspaceProbeDir: string\n\n private checkCache: CheckCache | null = null\n private inFlight: 'idle' | 'checking' | 'staging' = 'idle'\n\n constructor(options: RootUpdateServiceOptions) {\n this.spec = options.spec\n this.envNames = options.envNames\n this.logger = options.logger\n this.restartServerFn = options.restartServer\n this.dataDir = path.resolve(options.dataDir)\n this.execNpm =\n options.execNpm ??\n (async (args, opts) => {\n const { stdout } = await execFileAsync('npm', [...args], {\n cwd: opts.cwd,\n timeout: opts.timeoutMs,\n })\n return { stdout }\n })\n this.env = options.env ?? process.env\n this.now = options.now ?? Date.now\n this.runningPackageJsonPath = options.runningPackageJsonPath\n this.workspaceProbeDir = options.workspaceProbeDir\n }\n\n // ── Status ────────────────────────────────────────────────────────────\n\n resolveBootMode(): ServerBootMode {\n const raw = this.env[this.envNames.bootMode]\n if (raw === 'workspace' || raw === 'baked' || raw === 'data-root') return raw\n // The starter didn't run (dev tsx entry, `npm start`, or a pre-starter\n // image) — classify by checkout shape.\n return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? 'workspace' : 'baked'\n }\n\n private runningVersion(): string | null {\n return readPackageVersion(this.runningPackageJsonPath)\n }\n\n /** The running root package's identity — feeds registerNode / topology rows. */\n getRunningRootPackage(): { readonly name: string; readonly version: string | null } {\n return { name: this.spec.packageName, version: this.runningVersion() }\n }\n\n private seedVersion(): string | null {\n const seedDir = this.env[this.envNames.seedDir]\n if (seedDir === undefined || seedDir.length === 0) return null\n return readPackageVersion(path.join(seedDir, 'package.json'))\n }\n\n private rootDir(): string {\n return serverRootDir(this.dataDir)\n }\n\n private readState(): ServerRootState {\n return readServerRootState(this.rootDir()) ?? emptyServerRootState()\n }\n\n /**\n * Like `readState`, but distinguishes \"state.json missing\" (normal on a\n * fresh node) from \"state.json present but corrupt\" — in the corrupt case\n * the starter booted the baked seed while installed data-dir versions are\n * silently ignored, which the status surface must make visible.\n */\n private readStateInfo(): { state: ServerRootState; corrupt: boolean } {\n const rootDir = this.rootDir()\n const raw = readServerRootState(rootDir)\n if (raw !== null) return { state: raw, corrupt: false }\n return { state: emptyServerRootState(), corrupt: fs.existsSync(stateFilePath(rootDir)) }\n }\n\n private updateState(state: ServerRootState): ServerUpdateState {\n if (this.inFlight === 'checking') return 'checking'\n if (this.inFlight === 'staging') return 'staging'\n if (state.pendingBoot !== null) {\n // Distinguish \"staged, not yet restarted\" (still on the OLD version)\n // from \"restarted onto the staged version, awaiting its boot-health\n // confirmation\" — the latter is the probation boot: apply/rollback are\n // refused and the node must not be manually restarted (security review\n // #2). We ARE the probation boot when the active version the starter\n // booted equals the pending version.\n const activeVersion = this.env[this.envNames.activeVersion] ?? null\n if (activeVersion !== null && activeVersion === state.pendingBoot.version) {\n return 'awaiting-confirmation'\n }\n return 'pending-restart'\n }\n return 'idle'\n }\n\n async getServerPackageStatus(): Promise<ServerPackageStatus> {\n const { state, corrupt } = this.readStateInfo()\n const runningVersion = this.runningVersion()\n const latestVersion = this.checkCache?.latestVersion ?? null\n const updateAvailable =\n latestVersion !== null &&\n runningVersion !== null &&\n compareSemver(latestVersion, runningVersion) > 0\n return {\n packageName: this.spec.packageName,\n runningVersion,\n nodeRuntimeVersion: process.versions.node,\n activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,\n previousVersion: state.previousVersion,\n seedVersion: this.seedVersion(),\n latestVersion,\n updateAvailable,\n bootMode: this.resolveBootMode(),\n updateState: this.updateState(state),\n pendingVersion: state.pendingBoot?.version ?? null,\n rolledBack: state.rolledBack,\n stateFileCorrupt: corrupt,\n lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null,\n }\n }\n\n // ── Check ─────────────────────────────────────────────────────────────\n\n async checkServerUpdate(): Promise<ServerUpdateCheckResult> {\n const runningVersion = this.runningVersion()\n if (this.inFlight !== 'idle') {\n return {\n packageName: this.spec.packageName,\n runningVersion,\n latestVersion: this.checkCache?.latestVersion ?? null,\n updateAvailable: false,\n checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),\n error: `busy: ${this.inFlight}`,\n }\n }\n this.inFlight = 'checking'\n try {\n const registry = this.env['CAMSTACK_NPM_REGISTRY']\n const args = [\n 'view',\n `${this.spec.packageName}@latest`,\n 'version',\n ...buildNpmRegistryArgs(registry),\n ]\n const { stdout } = await this.execNpm(args, { timeoutMs: NPM_VIEW_TIMEOUT_MS })\n const latestVersion = stdout.trim().length > 0 ? stdout.trim() : null\n this.checkCache = { latestVersion, checkedAtMs: this.now(), error: null }\n const updateAvailable =\n latestVersion !== null &&\n runningVersion !== null &&\n compareSemver(latestVersion, runningVersion) > 0\n return {\n packageName: this.spec.packageName,\n runningVersion,\n latestVersion,\n updateAvailable,\n checkedAtMs: this.checkCache.checkedAtMs,\n error: null,\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n this.checkCache = {\n latestVersion: this.checkCache?.latestVersion ?? null,\n checkedAtMs: this.now(),\n error: message,\n }\n this.logger.warn('root package update check failed', { meta: { error: message } })\n return {\n packageName: this.spec.packageName,\n runningVersion,\n latestVersion: this.checkCache.latestVersion,\n updateAvailable: false,\n checkedAtMs: this.checkCache.checkedAtMs,\n error: message,\n }\n } finally {\n this.inFlight = 'idle'\n }\n }\n\n // ── Apply ─────────────────────────────────────────────────────────────\n\n async applyServerUpdate(input: { version?: string }): Promise<ServerUpdateActionResult> {\n const bootMode = this.resolveBootMode()\n if (bootMode === 'workspace') {\n return {\n accepted: false,\n targetVersion: input.version ?? null,\n restarting: false,\n message:\n 'Refused: running from a workspace checkout — the dev loop resolves code from the ' +\n 'workspace, a data-root update would never be loaded.',\n }\n }\n if (this.inFlight !== 'idle') {\n return {\n accepted: false,\n targetVersion: input.version ?? null,\n restarting: false,\n message: `Refused: another operation is in flight (${this.inFlight}).`,\n }\n }\n const stateBefore = this.readState()\n if (stateBefore.pendingBoot !== null) {\n return {\n accepted: false,\n targetVersion: input.version ?? null,\n restarting: false,\n message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`,\n }\n }\n\n // Resolve the target version (explicit or latest from the registry).\n let target = input.version ?? null\n if (target === null) {\n const check = await this.checkServerUpdate()\n if (check.error !== null || check.latestVersion === null) {\n return {\n accepted: false,\n targetVersion: null,\n restarting: false,\n message: `Refused: could not resolve the latest version (${check.error ?? 'no version returned'}).`,\n }\n }\n target = check.latestVersion\n }\n\n const runningVersion = this.runningVersion()\n if (target === runningVersion) {\n return {\n accepted: false,\n targetVersion: target,\n restarting: false,\n message: `Refused: ${this.spec.packageName}@${target} is already running.`,\n }\n }\n\n this.inFlight = 'staging'\n try {\n await this.stageAndActivate(target)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n this.logger.error('root package update staging failed', {\n meta: { targetVersion: target, error: message },\n })\n return {\n accepted: false,\n targetVersion: target,\n restarting: false,\n message: `Staging failed: ${message}`,\n }\n } finally {\n this.inFlight = 'idle'\n }\n\n // Arm the probation boot + schedule the graceful restart.\n const state = this.readState()\n writeServerRootState(this.rootDir(), {\n ...state,\n pendingBoot: {\n version: target,\n fromVersion: state.currentVersion,\n requestedAtMs: this.now(),\n bootAttempts: 0,\n },\n rolledBack: null,\n })\n this.logger.info('root package update staged — restarting to apply', {\n meta: { targetVersion: target, fromVersion: state.currentVersion },\n })\n this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`)\n return {\n accepted: true,\n targetVersion: target,\n restarting: true,\n message: `Staged ${this.spec.packageName}@${target} — restarting to apply (probation boot with auto-rollback).`,\n }\n }\n\n /**\n * npm-install the target closure into a SAME-FS staging dir inside\n * `versions/`, validate it, then atomically rename it into place.\n */\n private async stageAndActivate(target: string): Promise<void> {\n const rootDir = this.rootDir()\n const vDir = versionsDir(rootDir)\n fs.mkdirSync(vDir, { recursive: true })\n const stagingDir = path.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`)\n fs.mkdirSync(stagingDir, { recursive: true })\n\n try {\n fs.writeFileSync(\n path.join(stagingDir, 'package.json'),\n JSON.stringify({ name: 'camstack-node-root', private: true }, null, 2),\n 'utf-8',\n )\n const registry = this.env['CAMSTACK_NPM_REGISTRY']\n await this.execNpm(\n [\n 'install',\n '--omit=dev',\n '--no-audit',\n '--no-fund',\n '--loglevel=error',\n `${this.spec.packageName}@${target}`,\n ...buildNpmRegistryArgs(registry),\n ],\n { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS },\n )\n\n const entry = rootEntryPath(stagingDir, this.spec)\n if (!fs.existsSync(entry)) {\n throw new Error(`staged closure is missing the root entry (${entry})`)\n }\n const stagedVersion = readPackageVersion(\n path.join(rootPackageDir(stagingDir, this.spec), 'package.json'),\n )\n if (stagedVersion !== target) {\n throw new Error(\n `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? 'unknown'}`,\n )\n }\n\n // Activate: evict any existing dir for this version, then atomic rename.\n const dest = versionDir(rootDir, target)\n if (fs.existsSync(dest)) {\n const aside = `${dest}.evicted-${this.now()}`\n await fs.promises.rename(dest, aside)\n await fs.promises.rm(aside, { recursive: true, force: true }).catch(() => undefined)\n }\n await fs.promises.rename(stagingDir, dest)\n } catch (err) {\n await fs.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined)\n throw err\n }\n }\n\n // ── Rollback ──────────────────────────────────────────────────────────\n\n async rollbackServerUpdate(): Promise<ServerUpdateActionResult> {\n // Same reentrancy/precondition guards as applyServerUpdate: without them a\n // rollback racing an armed update (10s restart grace) silently overwrites\n // pendingBoot — last write wins with no error to the loser.\n if (this.inFlight !== 'idle') {\n return {\n accepted: false,\n targetVersion: null,\n restarting: false,\n message: `Refused: another operation is in flight (${this.inFlight}).`,\n }\n }\n const state = this.readState()\n if (state.pendingBoot !== null) {\n return {\n accepted: false,\n targetVersion: null,\n restarting: false,\n message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`,\n }\n }\n if (state.currentVersion === null || state.previousVersion === null) {\n return {\n accepted: false,\n targetVersion: state.previousVersion,\n restarting: false,\n message: 'Refused: no previous version available to roll back to.',\n }\n }\n const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10)\n const invalidReason = validateVersionDir(\n this.rootDir(),\n state.previousVersion,\n nodeMajor,\n this.spec,\n )\n if (invalidReason !== null) {\n return {\n accepted: false,\n targetVersion: state.previousVersion,\n restarting: false,\n message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`,\n }\n }\n\n writeServerRootState(this.rootDir(), {\n ...state,\n pendingBoot: {\n version: state.previousVersion,\n fromVersion: state.currentVersion,\n requestedAtMs: this.now(),\n bootAttempts: 0,\n },\n })\n this.logger.info('root package rollback requested — restarting', {\n meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion },\n })\n this.restartServerFn(\n `${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`,\n )\n return {\n accepted: true,\n targetVersion: state.previousVersion,\n restarting: true,\n message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} — restarting.`,\n }\n }\n\n // ── Plain restart ─────────────────────────────────────────────────────\n\n /**\n * Plain process restart — no version change. Backs the `restartServer` cap\n * method. Guards mirror apply/rollback: refuse while a stage is in flight\n * (a concurrent npm install must not be interrupted) or while a version is\n * already staged awaiting restart (a naive bounce would boot the OLD version\n * and orphan the pending one — the operator should apply/rollback instead).\n */\n restartNode(): ServerUpdateActionResult {\n const runningVersion = this.runningVersion()\n if (this.inFlight !== 'idle') {\n return {\n accepted: false,\n targetVersion: runningVersion,\n restarting: false,\n message: `Refused: another operation is in flight (${this.inFlight}).`,\n }\n }\n const state = this.readState()\n if (state.pendingBoot !== null) {\n return {\n accepted: false,\n targetVersion: runningVersion,\n restarting: false,\n message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart — apply or roll it back instead of a plain restart.`,\n }\n }\n this.logger.info('plain node restart requested', { meta: { runningVersion } })\n this.restartServerFn(`${RESTART_REASON_PREFIX}: manual restart`)\n return {\n accepted: true,\n targetVersion: runningVersion,\n restarting: true,\n message: 'Restarting the node — the supervisor relaunches it on the same version.',\n }\n }\n\n // ── Boot health confirmation ──────────────────────────────────────────\n\n /**\n * Called by the post-boot path once the node is READY (hub: post-boot\n * pipeline; agent: first acked `$hub.registerNode`). When THIS process is\n * the probation boot of a pending version, promote it: `currentVersion` ←\n * pending, `previousVersion` ← the version it replaced (N-1 retained for\n * rollback), clear `pendingBoot` + `rolledBack`, and prune every version\n * dir beyond {current, previous}. No-op otherwise.\n */\n confirmBootHealthy(): { promoted: string | null } {\n const state = readServerRootState(this.rootDir())\n if (state === null || state.pendingBoot === null) return { promoted: null }\n\n const activeVersion = this.env[this.envNames.activeVersion] ?? null\n if (activeVersion !== state.pendingBoot.version) {\n // We are NOT the probation boot of this pending version (e.g. the\n // starter never ran, or it booted a different closure). Leave the\n // marker for the starter's own rollback logic.\n return { promoted: null }\n }\n\n const promoted: ServerRootState = {\n ...state,\n currentVersion: state.pendingBoot.version,\n previousVersion: state.pendingBoot.fromVersion,\n pendingBoot: null,\n rolledBack: null,\n }\n writeServerRootState(this.rootDir(), promoted)\n this.pruneVersions(\n [promoted.currentVersion, promoted.previousVersion].filter(\n (v): v is string => typeof v === 'string',\n ),\n )\n this.logger.info('root package update confirmed healthy', {\n meta: {\n version: promoted.currentVersion,\n previousVersion: promoted.previousVersion,\n },\n })\n return { promoted: promoted.currentVersion }\n }\n\n /**\n * Remove version dirs under `versions/` not in `keep`.\n *\n * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless\n * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts\n * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent\n * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir\n * when this prune fires — deleting it would corrupt that install. Fresh\n * transient dirs are owned by their in-flight operation (which cleans up on\n * failure); the staleness sweep only collects dirs orphaned by a crash.\n */\n private pruneVersions(keep: readonly string[]): void {\n const vDir = versionsDir(this.rootDir())\n let entries: string[]\n try {\n entries = fs.readdirSync(vDir)\n } catch {\n return\n }\n for (const entry of entries) {\n if (keep.includes(entry)) continue\n const full = path.join(vDir, entry)\n if (entry.startsWith('.')) {\n try {\n const ageMs = this.now() - fs.statSync(full).mtimeMs\n if (ageMs < RootUpdateService.STALE_TRANSIENT_MS) continue\n } catch {\n // stat failed (already gone / unreadable) — skip it\n continue\n }\n }\n try {\n fs.rmSync(full, { recursive: true, force: true })\n this.logger.debug('pruned server-root version dir', { meta: { entry } })\n } catch (err) {\n this.logger.warn('failed to prune server-root version dir', {\n meta: { entry, error: err instanceof Error ? err.message : String(err) },\n })\n }\n }\n }\n\n /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */\n private static readonly STALE_TRANSIENT_MS = 24 * 60 * 60 * 1000\n}\n"],"mappings":"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","names":["__toESM","fs","path"]}
@@ -1,4 +1,8 @@
1
1
  import { createRequire as __cr } from 'node:module'; const require = globalThis.require ?? __cr(import.meta.url);
2
+ import {
3
+ __toESM,
4
+ require_dist
5
+ } from "./chunk-L34OOQI3.mjs";
2
6
 
3
7
  // src/agent-config.ts
4
8
  import * as fs from "fs";
@@ -197,8 +201,8 @@ function getLocalIps() {
197
201
  }
198
202
  async function withTimeout(p, ms, fallback) {
199
203
  let timer;
200
- const timeout = new Promise((resolve5) => {
201
- timer = setTimeout(() => resolve5(fallback), ms);
204
+ const timeout = new Promise((resolve6) => {
205
+ timer = setTimeout(() => resolve6(fallback), ms);
202
206
  });
203
207
  try {
204
208
  return await Promise.race([p, timeout]);
@@ -927,8 +931,8 @@ async function startAgentHttpServer(getBroker, config) {
927
931
  }
928
932
 
929
933
  // src/agent-bootstrap.ts
930
- import * as fs6 from "fs";
931
- import * as path6 from "path";
934
+ import * as fs7 from "fs";
935
+ import * as path7 from "path";
932
936
  import {
933
937
  createBroker,
934
938
  createAddonService,
@@ -976,19 +980,103 @@ import {
976
980
  pipelineRunnerCapability,
977
981
  audioAnalyzerCapability,
978
982
  platformProbeCapability,
983
+ serverManagementCapability,
979
984
  errMsg,
980
985
  ALL_CAPABILITY_DEFINITIONS
981
986
  } from "@camstack/types";
982
987
 
983
- // src/agent-group-runner.ts
988
+ // src/agent-update-service.ts
989
+ var import_node_root = __toESM(require_dist());
984
990
  import * as fs5 from "fs";
985
991
  import * as path5 from "path";
992
+ var AGENT_RUNTIME_ADDON_ID = "agent-runtime";
993
+ var AGENT_RESTART_GRACE_MS = 2e3;
994
+ var AGENT_LOCAL_CONFIRM_GRACE_MS = 9e4;
995
+ function armLocalConfirmFallback(onGrace, graceMs = AGENT_LOCAL_CONFIRM_GRACE_MS) {
996
+ const timer = setTimeout(() => onGrace(), graceMs);
997
+ const maybeUnref = timer.unref;
998
+ if (typeof maybeUnref === "function") maybeUnref.call(timer);
999
+ return () => clearTimeout(timer);
1000
+ }
1001
+ function resolveAgentPackageJsonPath(fromDir) {
1002
+ const candidates = [
1003
+ path5.resolve(fromDir, "..", "package.json"),
1004
+ path5.resolve(fromDir, "..", "..", "package.json")
1005
+ ];
1006
+ for (const candidate of candidates) {
1007
+ try {
1008
+ const raw = JSON.parse(fs5.readFileSync(candidate, "utf-8"));
1009
+ if (raw.name === import_node_root.AGENT_ROOT_SPEC.packageName) return candidate;
1010
+ } catch {
1011
+ }
1012
+ }
1013
+ return candidates[0] ?? path5.resolve(fromDir, "..", "package.json");
1014
+ }
1015
+ function scheduleAgentRestart(logger, requestedBy, dataDir) {
1016
+ if (dataDir !== void 0 && dataDir.length > 0) {
1017
+ try {
1018
+ (0, import_node_root.writeRestartIntentMarker)((0, import_node_root.serverRootDir)(dataDir), {
1019
+ requestedAtMs: Date.now(),
1020
+ reason: requestedBy
1021
+ });
1022
+ } catch (err) {
1023
+ logger.warn("failed to write restart-intent marker", {
1024
+ meta: { error: err instanceof Error ? err.message : String(err) }
1025
+ });
1026
+ }
1027
+ }
1028
+ logger.warn("agent restart requested \u2014 exiting for supervisor relaunch", {
1029
+ meta: { requestedBy, graceMs: AGENT_RESTART_GRACE_MS }
1030
+ });
1031
+ const grace = setTimeout(() => {
1032
+ process.kill(process.pid, "SIGTERM");
1033
+ const hard = setTimeout(() => process.exit(0), 15e3);
1034
+ hard.unref();
1035
+ }, AGENT_RESTART_GRACE_MS);
1036
+ grace.unref();
1037
+ }
1038
+ var AgentUpdateService = class extends import_node_root.RootUpdateService {
1039
+ constructor(options) {
1040
+ super({
1041
+ spec: import_node_root.AGENT_ROOT_SPEC,
1042
+ envNames: {
1043
+ bootMode: "CAMSTACK_AGENT_BOOT_MODE",
1044
+ activeVersion: "CAMSTACK_AGENT_ACTIVE_VERSION",
1045
+ seedDir: "CAMSTACK_SEED_AGENT_DIR"
1046
+ },
1047
+ logger: options.logger,
1048
+ restartServer: options.restartAgent ?? ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
1049
+ dataDir: options.dataDir,
1050
+ runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
1051
+ workspaceProbeDir: __dirname,
1052
+ execNpm: options.execNpm,
1053
+ env: options.env,
1054
+ now: options.now
1055
+ });
1056
+ }
1057
+ };
1058
+ function buildAgentServerManagementProvider(service) {
1059
+ return {
1060
+ getServerPackageStatus: () => service.getServerPackageStatus(),
1061
+ checkServerUpdate: () => service.checkServerUpdate(),
1062
+ applyServerUpdate: (input) => service.applyServerUpdate(input),
1063
+ rollbackServerUpdate: () => service.rollbackServerUpdate(),
1064
+ restartServer: () => Promise.resolve(service.restartNode())
1065
+ };
1066
+ }
1067
+ function agentRuntimeManifestEntry() {
1068
+ return { addonId: AGENT_RUNTIME_ADDON_ID, capabilities: ["server-management"] };
1069
+ }
1070
+
1071
+ // src/agent-group-runner.ts
1072
+ import * as fs6 from "fs";
1073
+ import * as path6 from "path";
986
1074
  import { resolveAddonPlacement } from "@camstack/types";
987
1075
  function readPackageManifestAddons(dir) {
988
1076
  try {
989
- const pkgPath = path5.join(dir, "package.json");
990
- if (!fs5.existsSync(pkgPath)) return null;
991
- const raw = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1077
+ const pkgPath = path6.join(dir, "package.json");
1078
+ if (!fs6.existsSync(pkgPath)) return null;
1079
+ const raw = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
992
1080
  const packageName = typeof raw.name === "string" ? raw.name : "";
993
1081
  const packageVersion = typeof raw.version === "string" ? raw.version : "0.0.0";
994
1082
  const entries = Array.isArray(raw.camstack?.addons) ? raw.camstack.addons : [];
@@ -1159,7 +1247,7 @@ async function startAgent(configPath) {
1159
1247
  const bundledDir = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
1160
1248
  let workspaceDir = null;
1161
1249
  let resolvedSource = explicitSource;
1162
- if (bundledDir && fs6.existsSync(bundledDir)) {
1250
+ if (bundledDir && fs7.existsSync(bundledDir)) {
1163
1251
  workspaceDir = bundledDir;
1164
1252
  resolvedSource = "local";
1165
1253
  console.log(`[Agent] Using bundled addons from ${bundledDir}`);
@@ -1223,9 +1311,9 @@ async function startAgent(configPath) {
1223
1311
  return discoveryMode ? "searching" : "disconnected";
1224
1312
  }
1225
1313
  function readConfigFile2(p) {
1226
- if (!fs6.existsSync(p)) return {};
1314
+ if (!fs7.existsSync(p)) return {};
1227
1315
  try {
1228
- return JSON.parse(fs6.readFileSync(p, "utf-8"));
1316
+ return JSON.parse(fs7.readFileSync(p, "utf-8"));
1229
1317
  } catch {
1230
1318
  return {};
1231
1319
  }
@@ -1255,6 +1343,7 @@ async function startAgent(configPath) {
1255
1343
  const caps = addonCapMap.get(addonId) ?? [];
1256
1344
  result.push({ addonId, capabilities: caps });
1257
1345
  }
1346
+ result.push(agentRuntimeManifestEntry());
1258
1347
  return result;
1259
1348
  }
1260
1349
  function aggregateManifest() {
@@ -1271,11 +1360,13 @@ async function startAgent(configPath) {
1271
1360
  allNativeCaps.push(...childNativeCaps);
1272
1361
  }
1273
1362
  }
1363
+ const rootPackage = agentUpdateService.getRunningRootPackage();
1274
1364
  return buildNodeManifest(
1275
1365
  config.nodeId,
1276
1366
  allAddons,
1277
1367
  allNativeCaps.length > 0 ? allNativeCaps : void 0,
1278
- config.secret ? hashClusterSecret(config.secret) : void 0
1368
+ config.secret ? hashClusterSecret(config.secret) : void 0,
1369
+ rootPackage.version === null ? void 0 : { name: rootPackage.name, version: rootPackage.version }
1279
1370
  );
1280
1371
  }
1281
1372
  function triggerUpwardRegistration() {
@@ -1287,7 +1378,9 @@ async function startAgent(configPath) {
1287
1378
  signal: registerAbortController.signal,
1288
1379
  log: (msg) => console.log(`[Agent] ${msg}`)
1289
1380
  }
1290
- ).catch((err) => {
1381
+ ).then(() => {
1382
+ confirmAgentBootHealthy("hub-ack");
1383
+ }).catch((err) => {
1291
1384
  if (isClusterSecretMismatchError(err)) {
1292
1385
  consoleLogger.error(
1293
1386
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
@@ -1317,11 +1410,41 @@ async function startAgent(configPath) {
1317
1410
  pipelineExecutorCapability,
1318
1411
  pipelineRunnerCapability,
1319
1412
  audioAnalyzerCapability,
1320
- platformProbeCapability
1413
+ platformProbeCapability,
1414
+ serverManagementCapability
1321
1415
  ];
1322
1416
  for (const cap of agentCapabilities) {
1323
1417
  capabilityRegistry.declareCapability(cap);
1324
1418
  }
1419
+ const agentUpdateService = new AgentUpdateService({
1420
+ logger: consoleLogger,
1421
+ dataDir: config.dataDir
1422
+ });
1423
+ capabilityRegistry.registerProvider(
1424
+ "server-management",
1425
+ AGENT_RUNTIME_ADDON_ID,
1426
+ buildAgentServerManagementProvider(agentUpdateService)
1427
+ );
1428
+ let agentBootConfirmed = false;
1429
+ let cancelLocalConfirmFallback = null;
1430
+ const confirmAgentBootHealthy = (source) => {
1431
+ if (agentBootConfirmed) return;
1432
+ agentBootConfirmed = true;
1433
+ cancelLocalConfirmFallback?.();
1434
+ cancelLocalConfirmFallback = null;
1435
+ try {
1436
+ const confirm = agentUpdateService.confirmBootHealthy();
1437
+ if (confirm.promoted !== null) {
1438
+ consoleLogger.info(
1439
+ `agent root package update confirmed healthy (${source}): @camstack/agent@${confirm.promoted}`
1440
+ );
1441
+ }
1442
+ } catch (err) {
1443
+ consoleLogger.warn(
1444
+ `agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`
1445
+ );
1446
+ }
1447
+ };
1325
1448
  const loggerFactory = (addonId) => agentLogManager.createLogger().withTags({ addonId });
1326
1449
  const agentServiceSchema = createAgentService({
1327
1450
  addonsDir: config.addonsDir,
@@ -1535,6 +1658,7 @@ async function startAgent(configPath) {
1535
1658
  capabilityRegistry
1536
1659
  );
1537
1660
  triggerUpwardRegistration();
1661
+ cancelLocalConfirmFallback = armLocalConfirmFallback(() => confirmAgentBootHealthy("local-grace"));
1538
1662
  let hubReachableFired = false;
1539
1663
  broker.localBus.on("$node.connected", (data) => {
1540
1664
  const node = data.node;
@@ -1558,6 +1682,8 @@ async function startAgent(configPath) {
1558
1682
  const shutdown = async () => {
1559
1683
  console.log("[Agent] Shutting down...");
1560
1684
  registerAbortController.abort();
1685
+ cancelLocalConfirmFallback?.();
1686
+ cancelLocalConfirmFallback = null;
1561
1687
  udsEventBridgeDispose?.();
1562
1688
  udsEventBridgeDispose = null;
1563
1689
  for (const [, entry] of loadedAddons) {
@@ -1718,7 +1844,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1718
1844
  }
1719
1845
  }
1720
1846
  async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1721
- if (!fs6.existsSync(addonsDir)) return;
1847
+ if (!fs7.existsSync(addonsDir)) return;
1722
1848
  const packageDirs = resolveAddonPackageDirs(addonsDir);
1723
1849
  const groupCandidates = [];
1724
1850
  const inProcessDirs = /* @__PURE__ */ new Set();
@@ -1778,7 +1904,7 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
1778
1904
  }
1779
1905
  }
1780
1906
  async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1781
- const modelsDir = path6.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
1907
+ const modelsDir = path7.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
1782
1908
  const contextOptions = {
1783
1909
  storageProvider,
1784
1910
  addonConfig: { modelsDir },
@@ -1828,13 +1954,13 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
1828
1954
  }
1829
1955
  function readAgentVersion() {
1830
1956
  const candidates = [
1831
- path6.resolve(__dirname, "..", "package.json"),
1832
- path6.resolve(__dirname, "..", "..", "package.json")
1957
+ path7.resolve(__dirname, "..", "package.json"),
1958
+ path7.resolve(__dirname, "..", "..", "package.json")
1833
1959
  ];
1834
1960
  for (const candidate of candidates) {
1835
1961
  try {
1836
- if (!fs6.existsSync(candidate)) continue;
1837
- const raw = JSON.parse(fs6.readFileSync(candidate, "utf-8"));
1962
+ if (!fs7.existsSync(candidate)) continue;
1963
+ const raw = JSON.parse(fs7.readFileSync(candidate, "utf-8"));
1838
1964
  if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
1839
1965
  } catch {
1840
1966
  }
@@ -1843,24 +1969,24 @@ function readAgentVersion() {
1843
1969
  }
1844
1970
  function isDir(p) {
1845
1971
  try {
1846
- return fs6.statSync(p).isDirectory();
1972
+ return fs7.statSync(p).isDirectory();
1847
1973
  } catch {
1848
1974
  return false;
1849
1975
  }
1850
1976
  }
1851
1977
  function resolveAddonPackageDirs(addonsDir) {
1852
1978
  const dirs = [];
1853
- if (!fs6.existsSync(addonsDir)) return dirs;
1854
- for (const name of fs6.readdirSync(addonsDir)) {
1855
- const full = path6.join(addonsDir, name);
1979
+ if (!fs7.existsSync(addonsDir)) return dirs;
1980
+ for (const name of fs7.readdirSync(addonsDir)) {
1981
+ const full = path7.join(addonsDir, name);
1856
1982
  if (name.startsWith("@") && isDir(full)) {
1857
- for (const sub of fs6.readdirSync(full)) {
1858
- const subFull = path6.join(full, sub);
1859
- if (isDir(subFull) && fs6.existsSync(path6.join(subFull, "package.json"))) {
1983
+ for (const sub of fs7.readdirSync(full)) {
1984
+ const subFull = path7.join(full, sub);
1985
+ if (isDir(subFull) && fs7.existsSync(path7.join(subFull, "package.json"))) {
1860
1986
  dirs.push(subFull);
1861
1987
  }
1862
1988
  }
1863
- } else if (isDir(full) && fs6.existsSync(path6.join(full, "package.json"))) {
1989
+ } else if (isDir(full) && fs7.existsSync(path7.join(full, "package.json"))) {
1864
1990
  dirs.push(full);
1865
1991
  }
1866
1992
  }
@@ -1898,4 +2024,4 @@ export {
1898
2024
  startAgentHttpServer,
1899
2025
  startAgent
1900
2026
  };
1901
- //# sourceMappingURL=chunk-POIXEKWT.mjs.map
2027
+ //# sourceMappingURL=chunk-MMKRM4RT.mjs.map