@camstack/server 1.2.676 → 1.2.677

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.677] - 2026-10-28
11
+
12
+ ### Changed
13
+
14
+ - An agent now keeps its own server closure current: unpaired it follows the npm latest, paired it follows the hub's version, checked at boot and every 6 hours (turn off with CAMSTACK_AGENT_SELF_UPDATE=off).
15
+
10
16
  ## [1.2.676] - 2026-10-28
11
17
 
12
18
  ### Fixed
@@ -50,9 +50,9 @@ function createAgentHubRegistration(opts) {
50
50
  opts.onCertificateRefused?.(err);
51
51
  },
52
52
  })
53
- .then(() => {
53
+ .then((ack) => {
54
54
  opts.log(`[register-node] the hub acked this node's capabilities: ${manifest.addons.length} addon(s), ${bindings} binding(s) (${reason})`);
55
- opts.onAcked?.();
55
+ opts.onAcked?.((0, system_1.hubServerVersionOf)(ack));
56
56
  })
57
57
  .catch(() => {
58
58
  // Aborted: shutting down.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideAgentSelfUpdate = decideAgentSelfUpdate;
4
+ exports.agentSelfUpdateEnabled = agentSelfUpdateEnabled;
5
+ exports.runAgentSelfUpdate = runAgentSelfUpdate;
6
+ /**
7
+ * An agent keeps its own `@camstack/server` closure current, with nobody driving it.
8
+ *
9
+ * Until now a closure reached an agent only when the HUB called `applyServerUpdate` on it. A
10
+ * freshly installed desktop agent boots the seed closure baked into the app and, being unpaired,
11
+ * had no hub to receive a fix from — not even the fix for its own unpaired boot (D857).
12
+ *
13
+ * The target version:
14
+ * - UNPAIRED: the npm `latest` of `@camstack/server` — there is no hub to follow;
15
+ * - PAIRED: the version the HUB runs (carried on the registration ack), never npm latest, so
16
+ * the cluster has no hub/agent skew.
17
+ *
18
+ * It reuses the one updater (`AgentUpdateService` → stage → `scheduleAgentRestart`, lifecycle =
19
+ * respawn, D5), never downgrades, and treats a failed read as "unknown", never as "up to date"
20
+ * (D49, D393): the reason is logged and nothing changes. The cadence is the update-check
21
+ * constant; there is no retry loop (D3).
22
+ *
23
+ * Off switch: `CAMSTACK_AGENT_SELF_UPDATE=off` (or `0`/`false`).
24
+ */
25
+ const index_js_1 = require("../server-root/index.js");
26
+ function skip(reason, failedRead = false) {
27
+ return { action: 'skip', reason, failedRead };
28
+ }
29
+ function decideAgentSelfUpdate(input) {
30
+ if (input.running === null)
31
+ return skip('the running version is unknown', true);
32
+ let target;
33
+ let source;
34
+ if (input.paired) {
35
+ if (input.hubVersion === null) {
36
+ return skip('paired, but the hub version is unknown (no registration ack carrying it yet, or an older hub)', true);
37
+ }
38
+ target = input.hubVersion;
39
+ source = 'hub';
40
+ }
41
+ else {
42
+ if (!input.npmLatest.ok) {
43
+ return skip(`the registry read failed: ${input.npmLatest.reason}`, true);
44
+ }
45
+ target = input.npmLatest.version;
46
+ source = 'npm-latest';
47
+ }
48
+ const order = (0, index_js_1.compareSemver)(target, input.running);
49
+ if (order === 0)
50
+ return skip(`already running ${target} (${source})`);
51
+ if (order < 0)
52
+ return skip(`target ${target} (${source}) is older than running ${input.running}: never a downgrade`);
53
+ if (input.pendingVersion === target)
54
+ return skip(`${target} is already staged, awaiting restart`);
55
+ return { action: 'stage', version: target, source };
56
+ }
57
+ function agentSelfUpdateEnabled(env) {
58
+ const raw = env['CAMSTACK_AGENT_SELF_UPDATE']?.trim().toLowerCase();
59
+ return !(raw === 'off' || raw === '0' || raw === 'false' || raw === 'no');
60
+ }
61
+ /** One self-update pass. Every decision is logged with the versions; it never throws. */
62
+ async function runAgentSelfUpdate(ports) {
63
+ const { log } = ports;
64
+ try {
65
+ if (!ports.enabled()) {
66
+ log.info('agent self-update: off (CAMSTACK_AGENT_SELF_UPDATE) — nothing checked');
67
+ return;
68
+ }
69
+ const paired = ports.isPaired();
70
+ const running = ports.runningVersion();
71
+ const hubVersion = ports.getHubServerVersion();
72
+ let npmLatest = { ok: false, reason: 'not needed' };
73
+ let pendingVersion = null;
74
+ if (!paired) {
75
+ const check = await ports.checkLatest();
76
+ pendingVersion = check.pendingVersion;
77
+ npmLatest =
78
+ check.error === null && check.latestVersion !== null
79
+ ? { ok: true, version: check.latestVersion }
80
+ : {
81
+ ok: false,
82
+ reason: check.error ?? 'the registry returned no version',
83
+ };
84
+ }
85
+ log.info(`agent self-update: checked (${paired ? 'paired' : 'unpaired'}) running=${running ?? 'unknown'} ` +
86
+ `hub=${hubVersion ?? 'unknown'} npmLatest=${npmLatest.ok ? npmLatest.version : 'n/a'}`);
87
+ const decision = decideAgentSelfUpdate({
88
+ paired,
89
+ running,
90
+ npmLatest,
91
+ hubVersion,
92
+ pendingVersion,
93
+ });
94
+ if (decision.action === 'skip') {
95
+ const line = `agent self-update: skip — ${decision.reason}`;
96
+ if (decision.failedRead)
97
+ log.warn(line);
98
+ else
99
+ log.info(line);
100
+ return;
101
+ }
102
+ log.info(`agent self-update: staging ${decision.version} (${decision.source}) over ${running ?? 'unknown'}`);
103
+ const result = await ports.apply(decision.version);
104
+ if (result.accepted) {
105
+ log.info(`agent self-update: applied ${decision.version} (${result.outcome}) — restarting to swap the closure`);
106
+ }
107
+ else {
108
+ log.warn(`agent self-update: ${decision.version} not applied (${result.outcome}): ${result.message}`);
109
+ }
110
+ }
111
+ catch (err) {
112
+ log.warn(`agent self-update: failed — ${err instanceof Error ? err.message : String(err)}; nothing changed`);
113
+ }
114
+ }
@@ -57,6 +57,8 @@ const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
57
57
  const agent_reconnect_js_1 = require("./agent-reconnect.js");
58
58
  const serial_queue_js_1 = require("./serial-queue.js");
59
59
  const agent_boot_mode_js_1 = require("./agent-boot-mode.js");
60
+ const agent_self_update_js_1 = require("./agent-self-update.js");
61
+ const update_check_scheduler_js_1 = require("../core/updates/update-check-scheduler.js");
60
62
  const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
61
63
  const agent_service_js_1 = require("./agent-service.js");
62
64
  const agent_hub_addon_update_js_1 = require("./agent-hub-addon-update.js");
@@ -306,6 +308,9 @@ async function startAgent(configPath) {
306
308
  // AbortController for the upward hub registration retry loop — cancelled on
307
309
  // agent shutdown so we don't leak the retry loop after stop().
308
310
  const registerAbortController = new AbortController();
311
+ // The hub's `@camstack/server` version from the last `registerNode` ack (agent self-update).
312
+ let hubServerVersion = null;
313
+ let selfUpdateOnFirstAck = null;
309
314
  /**
310
315
  * Build the manifest for the agent's OWN in-process addons (those loaded by
311
316
  * bootCoreAddons and loadDeployedAddons that ended up in `loadedAddons` WITH
@@ -395,10 +400,15 @@ async function startAgent(configPath) {
395
400
  log: (msg) => console.log(`[Agent] ${msg}`),
396
401
  // The hub acked the manifest — the agent reached its registered state.
397
402
  // Run the single-copy GC sweep once (first ack only; no-op after).
398
- onAcked: () => {
403
+ onAcked: (ackedHubServerVersion) => {
399
404
  if (hubConnectionStateOverride === 'certificate-rejected')
400
405
  hubConnectionStateOverride = null;
406
+ hubServerVersion = ackedHubServerVersion;
401
407
  confirmAgentBootHealthy();
408
+ // Paired: the first ack is the first moment the hub's version is known.
409
+ const firstAck = selfUpdateOnFirstAck;
410
+ selfUpdateOnFirstAck = null;
411
+ firstAck?.();
402
412
  },
403
413
  onCertificateRefused: () => {
404
414
  if (hubConnectionStateOverride === 'certificate-rejected')
@@ -902,6 +912,43 @@ async function startAgent(configPath) {
902
912
  });
903
913
  };
904
914
  const addonsBooted = (0, agent_boot_mode_js_1.decideAgentBootMode)({ paired: (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath) }) === 'full';
915
+ // ── Self-update (agent-self-update.ts): keep this closure current with nobody driving it.
916
+ // Unpaired -> npm latest, at boot; paired -> the hub's version, at the first registration ack.
917
+ // Then on the update-check cadence (a periodic check, no retry loop). Lifecycle = respawn (D5).
918
+ const selfUpdatePorts = {
919
+ enabled: () => (0, agent_self_update_js_1.agentSelfUpdateEnabled)(process.env),
920
+ isPaired: () => (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath),
921
+ getHubServerVersion: () => hubServerVersion,
922
+ runningVersion: () => agentUpdateService.getRunningRootPackage().version,
923
+ checkLatest: async () => {
924
+ const check = await agentUpdateService.checkServerUpdate();
925
+ return {
926
+ latestVersion: check.latestVersion,
927
+ error: check.error,
928
+ pendingVersion: check.pendingVersion ?? null,
929
+ };
930
+ },
931
+ apply: async (version) => {
932
+ const result = await agentUpdateService.applyServerUpdate({ version });
933
+ return {
934
+ accepted: result.accepted,
935
+ outcome: result.outcome ?? 'unknown',
936
+ message: result.message,
937
+ };
938
+ },
939
+ log: {
940
+ info: (message) => consoleLogger.info(message, { tags: { nodeId: config.nodeId } }),
941
+ warn: (message) => consoleLogger.warn(message, { tags: { nodeId: config.nodeId } }),
942
+ },
943
+ };
944
+ if ((0, agent_pairing_store_js_1.isAgentPaired)(config.configPath)) {
945
+ selfUpdateOnFirstAck = () => void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts);
946
+ }
947
+ else {
948
+ void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts);
949
+ }
950
+ const selfUpdateTimer = setInterval(() => void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts), update_check_scheduler_js_1.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS * 1000);
951
+ selfUpdateTimer.unref();
905
952
  const localPairing = (0, agent_pairing_local_js_1.createLocalPairingAnnouncer)(config.dataDir, consoleLogger);
906
953
  const pairing = new system_1.AgentPairing({
907
954
  nodeId: config.nodeId,
@@ -0,0 +1,68 @@
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.readServerVersionFrom = readServerVersionFrom;
37
+ exports.readHubServerVersion = readHubServerVersion;
38
+ /**
39
+ * The `@camstack/server` version this hub runs, carried on every `$hub.registerNode` ack so an
40
+ * agent can align its own closure to it (agent self-update). Read once from the running
41
+ * package.json (`dist/core/moleculer/` -> `../../../package.json`); null when it cannot be read —
42
+ * the ack then simply omits it and the agent says "hub version unknown" instead of guessing.
43
+ */
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const index_js_1 = require("../../server-root/index.js");
47
+ function readServerVersionFrom(packageJsonPath) {
48
+ try {
49
+ const raw = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
50
+ if (typeof raw !== 'object' || raw === null)
51
+ return null;
52
+ const name = Reflect.get(raw, 'name');
53
+ const version = Reflect.get(raw, 'version');
54
+ if (name !== index_js_1.SERVER_ROOT_PACKAGE_NAME)
55
+ return null;
56
+ return typeof version === 'string' && version.length > 0 ? version : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ let cached;
63
+ function readHubServerVersion() {
64
+ if (cached === undefined) {
65
+ cached = readServerVersionFrom(path.resolve(__dirname, '..', '..', '..', 'package.json'));
66
+ }
67
+ return cached;
68
+ }
@@ -11,6 +11,7 @@ const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
11
11
  const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
12
12
  const agent_registration_request_js_1 = require("./agent-registration-request.js");
13
13
  const hub_mesh_guard_js_1 = require("./hub-mesh-guard.js");
14
+ const hub_server_version_js_1 = require("./hub-server-version.js");
14
15
  const cap_call_fn_js_1 = require("./cap-call-fn.js");
15
16
  const cap_route_authority_js_1 = require("./cap-route-authority.js");
16
17
  class MoleculerService {
@@ -270,6 +271,8 @@ class MoleculerService {
270
271
  onUnregisterNode: (nodeId) => {
271
272
  this.removeNodeFromRegistry(nodeId);
272
273
  },
274
+ // The ack carries the hub's closure version so an agent aligns to it (agent self-update).
275
+ getServerVersion: () => (0, hub_server_version_js_1.readHubServerVersion)(),
273
276
  // D857: a node registers only itself, and only with a certificate identity.
274
277
  meshIdentity: this.meshGuard.gate,
275
278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.676",
3
+ "version": "1.2.677",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  "@camstack/addon-post-analysis": "1.2.649",
61
61
  "@camstack/develop": "0.1.38",
62
62
  "@camstack/sdk": "1.2.154",
63
- "@camstack/system": "1.2.507",
63
+ "@camstack/system": "1.2.508",
64
64
  "@camstack/types": "1.2.436",
65
65
  "@camstack/ui-library": "1.2.345",
66
66
  "@fastify/compress": "^9.0.0",