@camstack/server 1.2.72 → 1.2.74

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.
@@ -545,6 +545,13 @@ function createAgentService(deps) {
545
545
  return { success: false, loaded: [] };
546
546
  }
547
547
  const loaded = await deps.reloadDeployedAddons();
548
+ // The reload just changed this node's capability set. Re-run the
549
+ // atomic registerNode so the hub's routing authority learns it now,
550
+ // instead of at the next restart / reconnect. Unconditional: an
551
+ // empty `loaded` still covers a RE-load of an existing addon whose
552
+ // manifest gained a capability, and registerNode is an idempotent
553
+ // replace (D3), so a no-op re-register costs one acked RPC.
554
+ deps.reconcileNodeRegistration?.();
548
555
  return { success: true, loaded };
549
556
  },
550
557
  },
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * infra-boot-guard.ts — the agent's "did my infrastructure actually load?" gate.
4
+ *
5
+ * `bootCoreAddons` resolves each {@link INFRA_CAPABILITIES} entry to an addon
6
+ * package found under `<dataDir>/addons`. Two things can go wrong, and until
7
+ * 2026-08-08 only ONE of them was fatal:
8
+ *
9
+ * - the addon was found but its `initialize()` threw → boot aborted (correct);
10
+ * - the addon was NOT FOUND at all → `console.error`, continue.
11
+ *
12
+ * The second branch is strictly worse than the first and it was the quiet one.
13
+ * On `little-unraid` the whole `@camstack/system` package was renamed out of
14
+ * `/data/addons/@camstack/` at 16:40 on 2026-08-07 (a builtin-shadow cleanup).
15
+ * On an AGENT that directory is the SOLE source of the builtins — there is no
16
+ * seed fallback — so the next boot came up with ZERO infra addons: no storage,
17
+ * no settings-store, no metrics, no `platform-probe`, and no `hub-forwarder`.
18
+ *
19
+ * The node then reported `healthy`, registered its 14 forked addons with the
20
+ * hub, and kept running. Nothing said otherwise, because the ONE addon that
21
+ * ships an agent's logs to the hub (`hub-forwarder`, a `log-destination`) is in
22
+ * the same missing package: the failure was unobservable BY CONSTRUCTION. It
23
+ * surfaced five hours later, and only indirectly, as
24
+ * `getNodeInferenceDevices → reachable:false` on the hub — because the node's
25
+ * `registerNode` manifest legitimately no longer contained `platform-probe`,
26
+ * so `CapRouteResolver.nodeKnowsCap` correctly answered "no provider".
27
+ *
28
+ * `InfraCapability.required` is documented as "boot aborts when this
29
+ * capability's addon fails to initialize". A capability whose addon is not
30
+ * even present has failed harder. This guard makes both branches agree.
31
+ *
32
+ * It reports EVERY missing required capability in one throw, not just the
33
+ * first — an operator staring at a crash-loop needs the whole list, and the
34
+ * four that vanished together on little-unraid vanished for one single reason.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.assertRequiredInfraResolved = assertRequiredInfraResolved;
38
+ /**
39
+ * Throw when any `required` infra capability resolved to no addon at all.
40
+ *
41
+ * Called AFTER the resolution pass so the message names every missing
42
+ * capability at once. Non-required misses (`platform-probe`,
43
+ * `metrics-provider`, `log-destination`) are the caller's business — they are
44
+ * already logged individually and are legitimately absent on some nodes.
45
+ */
46
+ function assertRequiredInfraResolved(resolutions, addonsDir) {
47
+ const missing = resolutions.filter((r) => r.required && r.addonId === null).map((r) => r.name);
48
+ if (missing.length === 0)
49
+ return;
50
+ throw new Error(`Required infrastructure capabilities have no addon under "${addonsDir}": ` +
51
+ `${missing.join(', ')}. The agent's builtins package (@camstack/system) is ` +
52
+ `missing or unreadable — this node cannot store, configure or probe itself, ` +
53
+ `and every hub routing decision that consults its manifest would be wrong. ` +
54
+ `Refusing to boot degraded.`);
55
+ }
@@ -45,6 +45,7 @@ const types_2 = require("@camstack/types");
45
45
  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
+ const infra_boot_guard_js_1 = require("./infra-boot-guard.js");
48
49
  const agent_service_js_1 = require("./agent-service.js");
49
50
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
50
51
  const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.js");
@@ -409,6 +410,13 @@ async function startAgent(configPath) {
409
410
  }
410
411
  return loaded;
411
412
  },
413
+ // The reload above changed this node's capability set — re-derive the
414
+ // manifest from the live registry and re-run the atomic `registerNode`
415
+ // so the hub's `CapRouteResolver` can route to what this node now has.
416
+ // Hung on the existing `$agent.reload` trigger; no timer (D3).
417
+ reconcileNodeRegistration: () => {
418
+ triggerUpwardRegistration();
419
+ },
412
420
  // D3 subtree aggregation: when a group-runner child delivers its manifest
413
421
  // via `$agent.registerNode`, merge it into the local subtree registry and
414
422
  // immediately re-register the complete union with the hub.
@@ -776,6 +784,10 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
776
784
  console.warn(`[Agent] Failed to scan ${dir}: ${(0, types_2.errMsg)(err)}`);
777
785
  }
778
786
  }
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 = [];
779
791
  for (const infra of AGENT_INFRA) {
780
792
  const candidates = loader.listAddons().filter((a) => a.declaration.capabilities?.some((c) => {
781
793
  const capName = typeof c === 'string' ? c : c.name;
@@ -786,12 +798,16 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
786
798
  ? (candidates.find((a) => a.declaration.id === 'hub-forwarder') ?? candidates[0])
787
799
  : candidates[0];
788
800
  if (!addon) {
789
- if (infra.required) {
790
- console.error(`[Agent] Required infrastructure addon for "${infra.name}" not found`);
791
- }
801
+ // 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 });
792
807
  continue;
793
808
  }
794
809
  const addonId = addon.declaration.id;
810
+ resolutions.push({ name: infra.name, required: infra.required, addonId });
795
811
  try {
796
812
  const instance = new addon.addonClass();
797
813
  // Seed ctx.kernel.storage from whatever storage provider is already
@@ -834,6 +850,12 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
834
850
  }
835
851
  }
836
852
  }
853
+ // A required infra capability with NO addon at all is strictly worse than
854
+ // one whose addon threw (which already aborted above) — and it used to be
855
+ // the SILENT branch. Abort here instead of booting a node that cannot
856
+ // store, configure or probe itself and whose manifest then makes every hub
857
+ // routing decision about it wrong. See `infra-boot-guard.ts`.
858
+ (0, infra_boot_guard_js_1.assertRequiredInfraResolved)(resolutions, config.addonsDir);
837
859
  }
838
860
  // ---------------------------------------------------------------------------
839
861
  // Phase 1.5: Load cluster-capable addon packages
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.markSseRequestNoCompression = markSseRequestNoCompression;
4
+ /** Mutates the request headers in place; returns whether it marked the request. */
5
+ function markSseRequestNoCompression(request) {
6
+ const accept = request.headers.accept;
7
+ if (typeof accept === 'string' && accept.includes('text/event-stream')) {
8
+ request.headers['x-no-compression'] = '1';
9
+ return true;
10
+ }
11
+ return false;
12
+ }
@@ -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: 143 Skipped (legacy): 3
5
+ // Mounted: 144 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -350,6 +350,7 @@ function mountAllCaps(services) {
350
350
  return entries[0]?.[1] ?? null;
351
351
  }, remoteCapProxy),
352
352
  osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
353
+ osdManager: (0, generated_cap_routers_js_1.createCapRouter_osdManager)((_ctx) => reg?.getSingleton('osd-manager') ?? null, remoteCapProxy),
353
354
  petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
354
355
  pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => reg?.getSingleton('pipeline-analytics') ?? null, remoteCapProxy),
355
356
  pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??