@camstack/server 1.2.74 → 1.2.75

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,84 @@
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.seedBuiltinsFromClosure = seedBuiltinsFromClosure;
37
+ /**
38
+ * Self-heal the agent's builtins package before the infra scan.
39
+ *
40
+ * `bootCoreAddons` scans `config.addonsDir` and nothing else, the image seed
41
+ * ships no `@camstack/system`, and the running closure's copy is NOT loaded
42
+ * as an addon — so an agent whose `/data/addons/@camstack/system` is missing
43
+ * (greenfield container, an operator cleanup, a failed deploy) boots with
44
+ * zero infrastructure. Before D87 that produced a silently gutted node; with
45
+ * the D87 guard it produces a crash-loop. Both are wrong answers to a
46
+ * question the node can answer itself: the closure it is RUNNING carries the
47
+ * exact builtins package it needs.
48
+ *
49
+ * The copy is files-list shaped — `package.json` + `dist` — and NEVER
50
+ * `node_modules`: a nested `node_modules/@camstack/*` carries its own
51
+ * `package.json`s, the addon scan discovers those too, and the same addon
52
+ * initializing twice is a boot abort (measured live 2026-08-08, twice, two
53
+ * different vehicles).
54
+ */
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ function seedBuiltinsFromClosure(addonsDir, log = console.log, resolveClosurePkg = () => require.resolve('@camstack/system/package.json')) {
58
+ const target = path.join(addonsDir, '@camstack', 'system');
59
+ if (fs.existsSync(path.join(target, 'package.json')))
60
+ return 'present';
61
+ let closurePkgJson;
62
+ try {
63
+ closurePkgJson = resolveClosurePkg();
64
+ }
65
+ catch {
66
+ log('[Agent] builtins seed: @camstack/system not resolvable from the closure — cannot self-heal');
67
+ return 'unavailable';
68
+ }
69
+ const closureRoot = path.dirname(closurePkgJson);
70
+ const closureDist = path.join(closureRoot, 'dist');
71
+ if (!fs.existsSync(closureDist)) {
72
+ log(`[Agent] builtins seed: closure copy at ${closureRoot} has no dist — cannot self-heal`);
73
+ return 'unavailable';
74
+ }
75
+ fs.mkdirSync(target, { recursive: true });
76
+ fs.copyFileSync(closurePkgJson, path.join(target, 'package.json'));
77
+ fs.cpSync(closureDist, path.join(target, 'dist'), {
78
+ recursive: true,
79
+ filter: (src) => !src.split(path.sep).includes('node_modules'),
80
+ });
81
+ log(`[Agent] builtins seed: @camstack/system was missing under ${addonsDir} — ` +
82
+ `seeded package.json + dist from the running closure (${closureRoot})`);
83
+ return 'seeded';
84
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planInfraBoot = planInfraBoot;
4
+ exports.planToResolutions = planToResolutions;
5
+ /**
6
+ * `log-destination` prefers `hub-forwarder` (ships agent logs to the hub)
7
+ * over a local winston — mirrors the historical inline choice.
8
+ */
9
+ function pickCandidate(capName, candidates) {
10
+ if (capName === 'log-destination') {
11
+ return candidates.find((a) => a.id === 'hub-forwarder') ?? candidates[0];
12
+ }
13
+ return candidates[0];
14
+ }
15
+ function planInfraBoot(addons, infraList) {
16
+ const initialized = new Set();
17
+ const steps = [];
18
+ for (const infra of infraList) {
19
+ const candidates = addons.filter((a) => a.capabilities.includes(infra.name));
20
+ const chosen = pickCandidate(infra.name, candidates);
21
+ if (!chosen) {
22
+ steps.push({
23
+ capName: infra.name,
24
+ required: infra.required,
25
+ addonId: null,
26
+ initialize: false,
27
+ });
28
+ continue;
29
+ }
30
+ const first = !initialized.has(chosen.id);
31
+ if (first)
32
+ initialized.add(chosen.id);
33
+ steps.push({
34
+ capName: infra.name,
35
+ required: infra.required,
36
+ addonId: chosen.id,
37
+ initialize: first,
38
+ });
39
+ }
40
+ return steps;
41
+ }
42
+ /** Project a plan onto the guard's resolution shape. */
43
+ function planToResolutions(steps) {
44
+ return steps.map((s) => ({ name: s.capName, required: s.required, addonId: s.addonId }));
45
+ }
@@ -46,6 +46,8 @@ const agent_config_js_1 = require("./agent-config.js");
46
46
  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
+ const infra_boot_plan_js_1 = require("./infra-boot-plan.js");
50
+ const builtins_seed_js_1 = require("./builtins-seed.js");
49
51
  const agent_service_js_1 = require("./agent-service.js");
50
52
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
51
53
  const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.js");
@@ -767,6 +769,10 @@ async function startAgent(configPath) {
767
769
  // Core infra addons to load on agent — all infra including log-destination (hub-forwarder)
768
770
  const AGENT_INFRA = system_1.INFRA_CAPABILITIES;
769
771
  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);
770
776
  // Scan every installed addon package — infra providers may live outside
771
777
  // `@camstack/system` (e.g. `@camstack/addon-platform-probe-native`).
772
778
  const packageDirs = resolveAddonPackageDirs(config.addonsDir);
@@ -784,30 +790,39 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
784
790
  console.warn(`[Agent] Failed to scan ${dir}: ${(0, types_2.errMsg)(err)}`);
785
791
  }
786
792
  }
787
- // Every infra capability and the addon that will serve it — collected as we
788
- // go so `assertRequiredInfraResolved` can name ALL missing required caps in
789
- // one throw once the pass is done (see `infra-boot-guard.ts`).
790
- const resolutions = [];
791
- for (const infra of AGENT_INFRA) {
792
- const candidates = loader.listAddons().filter((a) => a.declaration.capabilities?.some((c) => {
793
- const capName = typeof c === 'string' ? c : c.name;
794
- return capName === infra.name;
795
- }));
796
- // For log-destination, prefer hub-forwarder over winston-logging
797
- const addon = infra.name === 'log-destination'
798
- ? (candidates.find((a) => a.declaration.id === 'hub-forwarder') ?? candidates[0])
799
- : candidates[0];
800
- if (!addon) {
793
+ // Resolution is per-CAPABILITY (the guard names every missing required
794
+ // cap), initialization is per-ADDON: since D44 one addon legitimately
795
+ // serves several infra caps (`storage-orchestrator` owns both `storage`
796
+ // and `settings-store`), and initializing it once per capability
797
+ // re-registers the same provider — a boot abort by the registry's own
798
+ // double-registration error. That combination took both agents down on
799
+ // 2026-08-08 the moment D87 promoted infra failures to fatal. The plan
800
+ // makes the dedupe explicit and testable (see `infra-boot-plan.ts`).
801
+ const byId = new Map(loader.listAddons().map((a) => [a.declaration.id, a]));
802
+ const plan = (0, infra_boot_plan_js_1.planInfraBoot)(loader.listAddons().map((a) => ({
803
+ id: a.declaration.id,
804
+ capabilities: (a.declaration.capabilities ?? []).map((c) => typeof c === 'string' ? c : c.name),
805
+ })), AGENT_INFRA);
806
+ const resolutions = (0, infra_boot_plan_js_1.planToResolutions)(plan);
807
+ for (const step of plan) {
808
+ if (step.addonId === null) {
801
809
  // Say it per-capability (an OPTIONAL miss is the only thing this line
802
- // will ever report once the guard below is in place), then record the
803
- // miss so the required ones abort boot together rather than one at a
804
- // time.
805
- console.error(`[Agent] Infrastructure addon for "${infra.name}" not found`);
806
- resolutions.push({ name: infra.name, required: infra.required, addonId: null });
810
+ // will ever report given the guard below); the required ones abort
811
+ // boot together after the pass.
812
+ console.error(`[Agent] Infrastructure addon for "${step.capName}" not found`);
807
813
  continue;
808
814
  }
815
+ if (!step.initialize) {
816
+ // Served by an addon an earlier capability already booted — the
817
+ // entry exists so a deployment missing it fails loudly, not to boot
818
+ // the addon twice.
819
+ console.log(`[Agent] Infra capability "${step.capName}" served by already-booted "${step.addonId}"`);
820
+ continue;
821
+ }
822
+ const addon = byId.get(step.addonId);
823
+ if (!addon)
824
+ continue;
809
825
  const addonId = addon.declaration.id;
810
- resolutions.push({ name: infra.name, required: infra.required, addonId });
811
826
  try {
812
827
  const instance = new addon.addonClass();
813
828
  // Seed ctx.kernel.storage from whatever storage provider is already
@@ -845,7 +860,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
845
860
  catch (err) {
846
861
  const msg = (0, types_2.errMsg)(err);
847
862
  console.error(`[Agent] Failed to initialize core addon "${addonId}": ${msg}`);
848
- if (infra.required) {
863
+ if (step.required) {
849
864
  throw new Error(`Required infrastructure addon "${addonId}" failed: ${msg}`, { cause: err });
850
865
  }
851
866
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.74",
3
+ "version": "1.2.75",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",