@camstack/server 1.2.93 → 1.2.95

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,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BUILTINS_PACKAGE = void 0;
37
+ exports.resolveAddonPackageDirs = resolveAddonPackageDirs;
38
+ exports.planAgentAddonScan = planAgentAddonScan;
39
+ /**
40
+ * Which directories the agent scans for addons — decided as pure data.
41
+ *
42
+ * ── The single-copy collapse, finally on the agent ─────────────────────────
43
+ * The hub stopped loading framework builtins from a physical
44
+ * `<addonsDir>/@camstack/system` on 2026-07-18: it loads them from the ACTIVE
45
+ * CLOSURE it is itself running (`addon-registry.service.ts`, `loadFromAddonDir`
46
+ * on the closure dir, then `loadFromDirectory(addonsDir, ['@camstack/system'])`
47
+ * so the copy is skipped). The reason is in that file's comment — *"that copy
48
+ * drifted because no update channel refreshed it"*.
49
+ *
50
+ * The agent never got the change, and drifted exactly as predicted. Measured
51
+ * 2026-08-11: little-unraid carried `@camstack/system@1.2.61` and macmini
52
+ * `1.2.62` in their addon roots while both ran a `1.2.78` closure. Nothing
53
+ * refreshed them, because `seedBuiltinsFromClosure` (now deleted) only wrote
54
+ * the copy when it was ABSENT — a self-heal that guaranteed a copy existed and
55
+ * never that it was current. It also contradicted
56
+ * `AddonInstaller.shouldSkipClosureProvidedSeed`, which already refused to
57
+ * plant that copy for this very reason.
58
+ *
59
+ * ── Why the skip is correctness, not tidiness ──────────────────────────────
60
+ * `AddonLoader` keys its map on `declaration.id`, so loading the closure AND a
61
+ * stale copy through one loader is not a duplicate — it is LAST-LOAD-WINS. The
62
+ * builtins a node runs would depend on directory iteration order. The copy must
63
+ * be skipped, not merely deprioritized.
64
+ *
65
+ * ── The fallback ──────────────────────────────────────────────────────────
66
+ * If the closure cannot be resolved, the copy is NOT skipped and the node boots
67
+ * from it. In a running agent this is near-impossible — `agent/main.ts`
68
+ * statically imports `@camstack/system`, so if the process exists the walk-up
69
+ * resolves — but a node with neither source must fail through the D87 guard
70
+ * (`assertRequiredInfraResolved`, which names every missing required cap),
71
+ * never through a silent "no addon packages found" early return.
72
+ */
73
+ const fs = __importStar(require("node:fs"));
74
+ const path = __importStar(require("node:path"));
75
+ /** The framework builtins package. Loaded from the closure, never from a copy. */
76
+ exports.BUILTINS_PACKAGE = '@camstack/system';
77
+ function isDir(p) {
78
+ try {
79
+ return fs.statSync(p).isDirectory();
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Every installed addon package under `addonsDir`, minus `skipPackages`.
87
+ *
88
+ * Matching mirrors `AddonLoader.loadFromDirectory`'s skip list exactly —
89
+ * `<scope>/<name>` for a scoped directory, the bare name for a flat one — so
90
+ * the hub and the agent exclude the same thing given the same list.
91
+ */
92
+ function resolveAddonPackageDirs(addonsDir, skipPackages = []) {
93
+ const dirs = [];
94
+ if (!fs.existsSync(addonsDir))
95
+ return dirs;
96
+ const skip = new Set(skipPackages);
97
+ for (const name of fs.readdirSync(addonsDir)) {
98
+ const full = path.join(addonsDir, name);
99
+ if (name.startsWith('@') && isDir(full)) {
100
+ // Scoped packages: @camstack/addon-xyz
101
+ for (const sub of fs.readdirSync(full)) {
102
+ const subFull = path.join(full, sub);
103
+ if (skip.has(`${name}/${sub}`))
104
+ continue;
105
+ if (isDir(subFull) && fs.existsSync(path.join(subFull, 'package.json'))) {
106
+ dirs.push(subFull);
107
+ }
108
+ }
109
+ }
110
+ else if (isDir(full) && fs.existsSync(path.join(full, 'package.json'))) {
111
+ if (skip.has(name))
112
+ continue;
113
+ dirs.push(full);
114
+ }
115
+ }
116
+ return dirs;
117
+ }
118
+ /**
119
+ * The ordered directory list `bootCoreAddons` loads, closure first.
120
+ *
121
+ * `closureSystemDir` is `resolveHubClosurePackageDir(BUILTINS_PACKAGE)` — null
122
+ * only when the builtins are not resolvable from the running closure at all.
123
+ */
124
+ function planAgentAddonScan(addonsDir, closureSystemDir) {
125
+ if (closureSystemDir === null) {
126
+ // No closure copy: fall back to whatever is on disk, including the
127
+ // addon-root builtins, rather than booting with no infra at all.
128
+ return resolveAddonPackageDirs(addonsDir);
129
+ }
130
+ return [closureSystemDir, ...resolveAddonPackageDirs(addonsDir, [exports.BUILTINS_PACKAGE])];
131
+ }
@@ -47,7 +47,8 @@ const agent_update_service_js_1 = require("./agent-update-service.js");
47
47
  const server_management_provider_js_1 = require("../api/core/server-management.provider.js");
48
48
  const infra_boot_guard_js_1 = require("./infra-boot-guard.js");
49
49
  const infra_boot_plan_js_1 = require("./infra-boot-plan.js");
50
- const builtins_seed_js_1 = require("./builtins-seed.js");
50
+ const package_dir_utils_js_1 = require("../core/addon/package-dir-utils.js");
51
+ const addon_scan_js_1 = require("./addon-scan.js");
51
52
  const agent_service_js_1 = require("./agent-service.js");
52
53
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
53
54
  const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.js");
@@ -769,13 +770,21 @@ async function startAgent(configPath) {
769
770
  // Core infra addons to load on agent — all infra including log-destination (hub-forwarder)
770
771
  const AGENT_INFRA = system_1.INFRA_CAPABILITIES;
771
772
  async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFactory) {
772
- // Self-heal FIRST: a missing builtins package is answerable from the
773
- // running closure (see `builtins-seed.ts`)without this, the D87 guard
774
- // below turns a recoverable gap into a crash-loop.
775
- (0, builtins_seed_js_1.seedBuiltinsFromClosure)(config.addonsDir);
776
- // Scan every installed addon package infra providers may live outside
777
- // `@camstack/system` (e.g. `@camstack/addon-platform-probe-native`).
778
- const packageDirs = resolveAddonPackageDirs(config.addonsDir);
773
+ // Builtins come from the CLOSURE this process is running, exactly as the hub
774
+ // has done since 2026-07-18never from a `<addonsDir>/@camstack/system`
775
+ // copy, which nothing refreshes and which therefore drifts (both agents were
776
+ // 17 versions behind on 2026-08-11). `planAgentAddonScan` owns the decision
777
+ // and the fallback; see `addon-scan.ts`. Non-framework infra providers
778
+ // (e.g. `@camstack/addon-platform-probe-native`) still come from the scan.
779
+ const closureSystemDir = (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(addon_scan_js_1.BUILTINS_PACKAGE);
780
+ if (closureSystemDir === null) {
781
+ // Louder than the hub's warn: on an agent the builtins ARE the infra, so
782
+ // this is pre-fatal. Boot continues to the D87 guard, which names every
783
+ // required cap it could not resolve.
784
+ console.error(`[Agent] ${addon_scan_js_1.BUILTINS_PACKAGE} not resolvable from the running closure — ` +
785
+ 'falling back to the addon-root copy, if there is one');
786
+ }
787
+ const packageDirs = (0, addon_scan_js_1.planAgentAddonScan)(config.addonsDir, closureSystemDir);
779
788
  if (packageDirs.length === 0) {
780
789
  console.warn('[Agent] No addon packages found — running without infrastructure addons');
781
790
  return;
@@ -876,7 +885,13 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
876
885
  // Phase 1.5: Load cluster-capable addon packages
877
886
  // ---------------------------------------------------------------------------
878
887
  async function loadClusterCapableAddons(broker, config, capabilityRegistry, loadedAddons) {
879
- const addonPackageDirs = resolveAddonPackageDirs(config.addonsDir);
888
+ // Builtins are in-process infra, never group-spawned, and they come from the
889
+ // closure — so the addon-root copy is skipped here too. Leaving it in would
890
+ // import a stale entry module during the scan below, which is the whole thing
891
+ // the single-copy collapse removes. The closure dir is deliberately NOT added:
892
+ // adding it would newly expose `hub-forwarder` / `platform-probe` (the only
893
+ // builtins carrying `execution`) as spawn candidates.
894
+ const addonPackageDirs = (0, addon_scan_js_1.resolveAddonPackageDirs)(config.addonsDir, systemSkipList());
880
895
  if (addonPackageDirs.length === 0)
881
896
  return;
882
897
  // ── Phase 0 (cross-package): collect every group-eligible addon
@@ -974,7 +989,9 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
974
989
  // the agent MAIN thread is what wedged `$agent.reload` and dropped the node
975
990
  // from topology. A group-runner addon only needs its declaration to be
976
991
  // (re)dispatched to a subprocess, which loads the real module itself.
977
- const packageDirs = resolveAddonPackageDirs(addonsDir);
992
+ // Same skip: a `$agent.deploy` of `@camstack/system` still lands bytes in the
993
+ // addon root, but a reload must not adopt them over the closure.
994
+ const packageDirs = (0, addon_scan_js_1.resolveAddonPackageDirs)(addonsDir, systemSkipList());
978
995
  const groupCandidates = [];
979
996
  // Dirs holding a deployable NON-group addon — and ONLY these — need the
980
997
  // importing loader. None exist under the default `hub-only` placement
@@ -1118,36 +1135,14 @@ function readAgentVersion() {
1118
1135
  }
1119
1136
  return 'unknown';
1120
1137
  }
1121
- /** Check if path is a directory (follows symlinks) */
1122
- function isDir(p) {
1123
- try {
1124
- return fs.statSync(p).isDirectory();
1125
- }
1126
- catch {
1127
- return false;
1128
- }
1129
- }
1130
- /** Scan addonsDir for addon packages (scoped and unscoped, follows symlinks) */
1131
- function resolveAddonPackageDirs(addonsDir) {
1132
- const dirs = [];
1133
- if (!fs.existsSync(addonsDir))
1134
- return dirs;
1135
- for (const name of fs.readdirSync(addonsDir)) {
1136
- const full = path.join(addonsDir, name);
1137
- if (name.startsWith('@') && isDir(full)) {
1138
- // Scoped packages: @camstack/addon-xyz
1139
- for (const sub of fs.readdirSync(full)) {
1140
- const subFull = path.join(full, sub);
1141
- if (isDir(subFull) && fs.existsSync(path.join(subFull, 'package.json'))) {
1142
- dirs.push(subFull);
1143
- }
1144
- }
1145
- }
1146
- else if (isDir(full) && fs.existsSync(path.join(full, 'package.json'))) {
1147
- dirs.push(full);
1148
- }
1149
- }
1150
- return dirs;
1138
+ /**
1139
+ * Skip the addon-root builtins copy — but ONLY when the closure can supply
1140
+ * them. Same condition `planAgentAddonScan` applies, kept in one place so the
1141
+ * infra scan and the two deployable-addon scans can never disagree about which
1142
+ * copy is authoritative.
1143
+ */
1144
+ function systemSkipList() {
1145
+ return (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(addon_scan_js_1.BUILTINS_PACKAGE) === null ? [] : [addon_scan_js_1.BUILTINS_PACKAGE];
1151
1146
  }
1152
1147
  // ---------------------------------------------------------------------------
1153
1148
  // E1 helper — agent-local UDS child manifest adaptation
@@ -1176,14 +1176,24 @@ function isHubNode(nodeId) {
1176
1176
  * agent reports its addon roster (id + status + version + packageName);
1177
1177
  * we keep only entries that carry both a package name and a version so
1178
1178
  * the hub can diff them against npm.
1179
+ *
1180
+ * `@camstack/system` is KEPT, and it is one of the most important entries here.
1181
+ * On an agent, `addons/@camstack/system` is the only copy of the builtin infra
1182
+ * ([D87](../../../../docs/decisions/adr-0087.md)), so a pending update on it is
1183
+ * a true, actionable signal: the node's infra is behind the closure it runs
1184
+ * against. Both agents sat 17 versions behind that way until 2026-08-11. Do not
1185
+ * filter it out to make the list look clean — the number reaching zero is the
1186
+ * point of the number.
1179
1187
  */
1180
1188
  async function fetchAgentInstalledPackages(broker, nodeId) {
1181
1189
  const status = await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: 5_000 });
1182
1190
  const out = [];
1183
1191
  for (const a of status.addons ?? []) {
1184
- if (typeof a.packageName === 'string' && typeof a.version === 'string') {
1185
- out.push({ name: a.packageName, version: a.version });
1186
- }
1192
+ if (typeof a.packageName !== 'string' || typeof a.version !== 'string')
1193
+ continue;
1194
+ if ((0, addon_package_service_js_1.isFrameworkPackage)(a.packageName))
1195
+ continue;
1196
+ out.push({ name: a.packageName, version: a.version });
1187
1197
  }
1188
1198
  return out;
1189
1199
  }
@@ -1253,6 +1263,22 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1253
1263
  }
1254
1264
  return { success: false, error: task?.error ?? 'update failed', jobId };
1255
1265
  }
1266
+ // `@camstack/system` DELIBERATELY travels this way to an agent, and it is
1267
+ // the only way it can. On the hub, `addons/@camstack/system` is a shadow
1268
+ // of the closure; on an AGENT it is THE ONLY COPY of the builtin infra —
1269
+ // `bootCoreAddons` scans the addon root and nothing else, and the image
1270
+ // seed excludes the framework set on purpose ([D87](../../../../docs/decisions/adr-0087.md)).
1271
+ // Removing it on 2026-08-07 left the node with no `filesystem-storage`,
1272
+ // `sqlite-settings`, `storage-orchestrator`, `hub-forwarder`,
1273
+ // `metrics-native` or `platform-probe`, and the outage was unobservable
1274
+ // because the log shipper was in the same package.
1275
+ //
1276
+ // So do NOT add a framework guard here. A 2026-08-11 attempt to do so read
1277
+ // the launcher's `resolved @camstack/system@… from <activeRoot>` line as
1278
+ // proof that nothing loads the addon-root copy. That line answers which
1279
+ // copy the LAUNCHER resolves for its own imports — a different consumer
1280
+ // from `bootCoreAddons`. Same trap D45 names: a version is a property of a
1281
+ // path, and a path is only evidence about the consumer that reads it.
1256
1282
  // Agent target: the hub packs the resolved version and ships the
1257
1283
  // tarball over `$agent.deploy` — the agent has no npm runtime.
1258
1284
  // The reload re-instantiates the changed package's addons; for a large
@@ -1349,12 +1375,30 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1349
1375
  return { success: true };
1350
1376
  },
1351
1377
  // ── Lifecycle job engine ─────────────────────────────────────────
1352
- startJob: async (input) => lifecycleRunner.startJob({
1353
- kind: input.kind,
1354
- targets: input.targets,
1355
- nodeIds: input.nodeIds,
1356
- createdBy: lifecycleCreatedBy,
1357
- }),
1378
+ startJob: async (input) => {
1379
+ // The engine is HUB-LOCAL: `StartJobInput.nodeIds` is declared
1380
+ // "reserved for future cluster jobs" and nothing reads it. Passing an
1381
+ // agent id therefore used to apply every target to the HUB while the
1382
+ // job, its per-task `nodeId` and the UI all reported the agent — so
1383
+ // "Update all" on a selected agent silently updated the wrong node and
1384
+ // left the agent pending forever. That is how two agents sat at 8
1385
+ // pending addons across several sessions (2026-08-11). Refuse instead:
1386
+ // an agent converges one package at a time through `updatePackage`,
1387
+ // which has a real `$agent.deploy` path.
1388
+ const offNode = (input.nodeIds ?? []).filter((id) => !isHubNode(id));
1389
+ if (offNode.length > 0) {
1390
+ throw new server_1.TRPCError({
1391
+ code: 'BAD_REQUEST',
1392
+ message: `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Update an agent's packages via updatePackage({ nodeId }).`,
1393
+ });
1394
+ }
1395
+ return lifecycleRunner.startJob({
1396
+ kind: input.kind,
1397
+ targets: input.targets,
1398
+ nodeIds: input.nodeIds,
1399
+ createdBy: lifecycleCreatedBy,
1400
+ });
1401
+ },
1358
1402
  getJob: async (input) => lifecycleRunner.getJob(input.jobId),
1359
1403
  listJobs: async (input) => lifecycleRunner.listJobs({ activeOnly: input.activeOnly }),
1360
1404
  cancelJob: async (input) => lifecycleRunner.cancelJob(input.jobId),
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 145 Skipped (legacy): 3
5
+ // Mounted: 146 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -406,6 +406,8 @@ function mountAllCaps(services) {
406
406
  const entries = reg.getCollectionEntries('storage-evictable');
407
407
  return entries[0]?.[1] ?? null;
408
408
  }, remoteCapProxy),
409
+ storageMigration: (0, generated_cap_routers_js_1.createCapRouter_storageMigration)((_ctx) => reg?.getSingleton('storage-migration') ??
410
+ null, remoteCapProxy),
409
411
  storageProvider: (0, generated_cap_routers_js_1.createCapRouter_storageProvider)((_ctx, addonId) => {
410
412
  if (!reg)
411
413
  return null;