@camstack/server 1.2.629 → 1.2.631

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.
@@ -9,7 +9,7 @@
9
9
  The package copies (packages/addon-pipeline, packages/addon-post-analysis, packages/addon-ai, packages/addon-model-studio, server/backend) are byte-identical
10
10
  (npm cannot ship a file from outside the package); the Hugging Face mirror receives
11
11
  the same file from hf/camstack-models/upload.sh. Drift fails CI.
12
- Facts: docs/superpowers/specs/2026-09-26-model-license-compliance.md. Not legal advice.
12
+ Facts: planning repo superpowers/specs/2026-09-26-model-license-compliance.md. Not legal advice.
13
13
  -->
14
14
 
15
15
  # Third-party models
@@ -69,6 +69,7 @@ const derive_hub_url_js_1 = require("./derive-hub-url.js");
69
69
  Object.defineProperty(exports, "pickIpv4", { enumerable: true, get: function () { return derive_hub_url_js_1.pickIpv4; } });
70
70
  const agent_addon_runtime_js_1 = require("./agent-addon-runtime.js");
71
71
  const hubs_found_js_1 = require("./hubs-found.js");
72
+ const release_identity_js_1 = require("../core/release-identity.js");
72
73
  /** The pinned CA's fingerprint, or `null` when none is pinned or the PEM cannot be read. */
73
74
  function describePairedHub(pinnedCa, hubHttpsOrigin) {
74
75
  if (pinnedCa === null)
@@ -408,6 +409,10 @@ async function createAgentHttpServer(getBroker, config) {
408
409
  // Version of the Electron wrapper (injected by the desktop app); absent
409
410
  // when the agent runs headless / in Docker.
410
411
  const appVersion = process.env['CAMSTACK_AGENT_APP_VERSION'] ?? null;
412
+ const identity = (0, release_identity_js_1.releaseIdentityFrom)({
413
+ productVersionEnv: process.env[release_identity_js_1.PRODUCT_VERSION_ENV],
414
+ serverVersion: config.serverVersion ?? null,
415
+ });
411
416
  const discoveredNodes = mapDiscoveredNodes(nodes, broker.nodeID);
412
417
  // The hub this agent is paired with, for the agent UI's "Paired with" card (D857).
413
418
  const pairedHub = describePairedHub((0, agent_pairing_store_js_1.pinnedMeshCaCert)(config.configPath), hubHttpsOrigin);
@@ -420,6 +425,8 @@ async function createAgentHttpServer(getBroker, config) {
420
425
  resolvedHubAddress,
421
426
  hubHttpsOrigin,
422
427
  appVersion,
428
+ productVersion: identity.productVersion,
429
+ serverVersion: identity.serverVersion,
423
430
  hubConnected,
424
431
  hubConnectionState,
425
432
  paired: eff.paired,
@@ -436,6 +443,8 @@ async function createAgentHttpServer(getBroker, config) {
436
443
  resolvedHubAddress,
437
444
  hubHttpsOrigin,
438
445
  appVersion,
446
+ productVersion: identity.productVersion,
447
+ serverVersion: identity.serverVersion,
439
448
  hubConnected,
440
449
  hubConnectionState,
441
450
  paired: eff.paired,
@@ -28,7 +28,7 @@ exports.createAgentHubAddonControls = createAgentHubAddonControls;
28
28
  * (`remote-addon-update-grant.ts`): own node, the hub's recorded version, audited.
29
29
  * Any refusal reaches the UI verbatim as the update's failure, never as silence.
30
30
  */
31
- const index_js_1 = require("../server-root/index.js");
31
+ const system_1 = require("@camstack/system");
32
32
  /** Hub procedures this module calls, as `$core-caps` actions. */
33
33
  exports.HUB_ADDON_LIST_ACTION = '$core-caps.addons.list';
34
34
  exports.HUB_ADDON_UPDATE_ACTION = '$core-caps.addons.updatePackage';
@@ -57,9 +57,15 @@ function hubVersionsFromList(list) {
57
57
  }
58
58
  return out;
59
59
  }
60
- /** True when the hub's version is strictly newer than this node's. */
60
+ /**
61
+ * True when this node does not run the hub's version, in either direction.
62
+ * Unknown on either side is not a difference — an unreadable hub is not a
63
+ * reason to offer a move (D942, D49).
64
+ */
61
65
  function isBehindHub(installed, hub) {
62
- return installed !== null && hub !== null && (0, index_js_1.compareSemver)(hub, installed) > 0;
66
+ if (installed === null || hub === null)
67
+ return false;
68
+ return (0, system_1.decideAddonVersionMove)(installed, hub, system_1.ADDON_VERSION_REASON_HUB).move;
63
69
  }
64
70
  /** The hub answered `{ success: false, error }`, or something unreadable. */
65
71
  function failureOf(result) {
@@ -947,6 +947,7 @@ async function startAgent(configPath) {
947
947
  getHubConnectionState,
948
948
  getMeshTransport: () => (currentMeshTls !== null ? 'tls' : 'clear'),
949
949
  updateControls,
950
+ serverVersion: agentUpdateService.getRunningRootPackage().version,
950
951
  }).then((listener) => {
951
952
  agentHttpListener = listener;
952
953
  });
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.buildHubHealth = buildHubHealth;
24
24
  exports.isAuthorizedHealthRequest = isAuthorizedHealthRequest;
25
25
  exports.registerHealthRoutes = registerHealthRoutes;
26
+ const release_identity_js_1 = require("../../core/release-identity.js");
26
27
  const runner_boot_check_js_1 = require("../../core/runner-health/runner-boot-check.js");
27
28
  const session_cookie_js_1 = require("../../auth/session-cookie.js");
28
29
  const AGENT_HEALTH_TIMEOUT_MS = 3_000;
@@ -115,6 +116,14 @@ function registerHealthRoutes(fastify, deps) {
115
116
  : {}),
116
117
  ...(typeof req.headers.cookie === 'string' ? { cookie: req.headers.cookie } : {}),
117
118
  }, deps.verifyToken);
119
+ // PUBLIC versions only — the login footer and the sidebar. Not topology:
120
+ // `/health` stays liveness-only for that reason.
121
+ fastify.get('/api/release-identity', async () => {
122
+ return (0, release_identity_js_1.releaseIdentityFrom)({
123
+ productVersionEnv: deps.productVersion ?? undefined,
124
+ serverVersion: deps.hubVersion,
125
+ });
126
+ });
118
127
  // PUBLIC probe: liveness only. Everything beyond `ok` (version, uptime,
119
128
  // pid, agent topology) moved to the AUTHENTICATED /health/details — a
120
129
  // public endpoint must not enumerate the deployment.
@@ -690,14 +690,14 @@ class AddonPackageService {
690
690
  });
691
691
  return;
692
692
  }
693
- const latestVersion = lookup.latestVersion;
694
- if (latestVersion === null || !isVersionNewer(latestVersion, version))
693
+ const decision = (0, system_1.decideAddonVersionMove)(version, lookup.latestVersion, system_1.ADDON_VERSION_REASON_REGISTRY_LATEST);
694
+ if (!decision.move)
695
695
  return;
696
696
  const category = this.categorize(name);
697
697
  updates.push({
698
698
  name,
699
699
  currentVersion: version,
700
- latestVersion,
700
+ latestVersion: decision.to,
701
701
  category,
702
702
  requiresRestart: category === 'core',
703
703
  });
@@ -1465,10 +1465,10 @@ class AddonPackageService {
1465
1465
  * `applyServerUpdate` (single-copy collapse), so a swept framework target is
1466
1466
  * skipped by the engine.
1467
1467
  *
1468
- * Candidate selection (channel resolution + npm metadata fetch + "is there a
1469
- * newer version?" comparison) is unchanged from the previous per-item loop;
1470
- * only the EXECUTION moved from sequential `updatePackage` calls to a single
1471
- * `startJob`.
1468
+ * Candidate selection resolves dist-tag `latest` (or `beta` on that channel)
1469
+ * and moves when the installed version DIFFERS — higher or lower (D942).
1470
+ * A registry read that fails leaves that package alone. Execution is one
1471
+ * `startJob`, not a per-package `updatePackage`.
1472
1472
  */
1473
1473
  async runAutoUpdate() {
1474
1474
  this.logger.info('Running auto-update check...');
@@ -1496,36 +1496,38 @@ class AddonPackageService {
1496
1496
  const response = await fetch(url, {
1497
1497
  signal: AbortSignal.timeout(AddonPackageService.REGISTRY_TIMEOUT_MS),
1498
1498
  });
1499
- if (!response.ok)
1499
+ if (!response.ok) {
1500
+ this.logger.warn('Auto-update left package unchanged — registry unreadable', {
1501
+ meta: { name: pkg.name, status: response.status },
1502
+ });
1500
1503
  continue;
1504
+ }
1501
1505
  const data = await fetchJsonObject(response);
1502
1506
  const distTags = asRecord(data['dist-tags']);
1503
1507
  // Determine target version based on channel
1504
1508
  const targetVersion = effectiveChannel === 'beta'
1505
1509
  ? asString(distTags['beta']) || asString(distTags['latest'])
1506
1510
  : asString(distTags['latest']);
1507
- // FORWARD-ONLY: update only when the registry advertises a STRICTLY
1508
- // NEWER semver. The old `!==` comparison happily "updated" BACKWARD —
1509
- // on 2026-07-22 it replaced same-day CLI-deployed builds (labelled
1510
- // with the older workspace version) with the freshly published npm
1511
- // artifacts, rolling back live fixes. A dev build whose label equals
1512
- // or exceeds the npm tag is left alone.
1513
- if (!targetVersion || !isVersionNewer(targetVersion, pkg.version))
1511
+ // D942: follow the tag in either direction. An empty tag is an
1512
+ // unreadable authority — leave the installed copy (D49).
1513
+ const followed = (0, system_1.decideAddonVersionMove)(pkg.version, targetVersion ?? null, system_1.ADDON_VERSION_REASON_REGISTRY_LATEST);
1514
+ if (!followed.move)
1514
1515
  continue;
1515
- this.logger.info('Auto-update candidate', {
1516
+ this.logger.info('Moving addon to registry latest', {
1516
1517
  meta: {
1517
1518
  name: pkg.name,
1518
- currentVersion: pkg.version,
1519
- targetVersion,
1519
+ from: followed.from,
1520
+ to: followed.to,
1521
+ reason: followed.reason,
1520
1522
  channel: effectiveChannel,
1521
1523
  },
1522
1524
  });
1523
- targets.push({ name: pkg.name, version: targetVersion });
1525
+ targets.push({ name: pkg.name, version: followed.to });
1524
1526
  available.push({
1525
1527
  target: 'addon',
1526
1528
  packageName: pkg.name,
1527
1529
  currentVersion: pkg.version,
1528
- latestVersion: targetVersion,
1530
+ latestVersion: followed.to,
1529
1531
  });
1530
1532
  }
1531
1533
  catch (err) {
@@ -1692,18 +1694,16 @@ class AddonPackageService {
1692
1694
  });
1693
1695
  continue;
1694
1696
  }
1695
- const latestVersion = lookup.latestVersion;
1696
- if (!latestVersion)
1697
+ const decision = (0, system_1.decideAddonVersionMove)(version, lookup.latestVersion, system_1.ADDON_VERSION_REASON_REGISTRY_LATEST);
1698
+ if (!decision.move)
1697
1699
  continue;
1698
- if (isVersionNewer(latestVersion, version)) {
1699
- updates.push({
1700
- name,
1701
- currentVersion: version,
1702
- latestVersion,
1703
- category: this.categorize(name),
1704
- requiresRestart: this.categorize(name) === 'core',
1705
- });
1706
- }
1700
+ updates.push({
1701
+ name,
1702
+ currentVersion: version,
1703
+ latestVersion: decision.to,
1704
+ category: this.categorize(name),
1705
+ requiresRestart: this.categorize(name) === 'core',
1706
+ });
1707
1707
  }
1708
1708
  catch (error) {
1709
1709
  // The package could not be evaluated at all (unreadable manifest, …).
@@ -24,11 +24,12 @@
24
24
  * both mutations are attributable actions rather than heuristics.
25
25
  *
26
26
  * THE INTENDED VERSION
27
- * The hub's own installed version, and only that. Never `latest` — resolving
28
- * `latest` per node is exactly the silent per-node drift the bootstrap
29
- * installer already suffers from (`addon-installer.ts` installs unpinned and
30
- * `ensureRequiredPackages` never re-installs, so whatever `latest` meant at a
31
- * node's first boot is what that node runs forever).
27
+ * The hub's own installed version, and only that — including when the node
28
+ * is AHEAD of the hub (D942). Never npm `latest` directly: the hub follows
29
+ * the registry, and every node follows the hub. A package the hub's version
30
+ * map does not name is left as it is when it is already installed, and is
31
+ * never delivered when it is missing (`notOnHub`). An unreadable hub version
32
+ * is not a reason to move.
32
33
  *
33
34
  * A package the hub does NOT install is never delivered, whatever the node last
34
35
  * reported. It used to fall back to the node's last-reported version, which
@@ -55,6 +56,20 @@ exports.triggerMayDeliver = triggerMayDeliver;
55
56
  exports.computeBackfillPlan = computeBackfillPlan;
56
57
  exports.confirmBackfillPlan = confirmBackfillPlan;
57
58
  exports.executeBackfill = executeBackfill;
59
+ /**
60
+ * Why a reconcile is running. Named on every back-fill decision, and an
61
+ * allowlist: a future caller has to CHOOSE a trigger, and a trigger that must
62
+ * not deliver bytes is refused here rather than in a comment.
63
+ *
64
+ * `node-registration` covers both a first registration and a rejoin — the
65
+ * distinction is not the discriminator it looks like. The case the back-fill
66
+ * exists for (a node that lost `<dataDir>/addons` and came back with 4 of 14
67
+ * packages) presents as a rejoin, so excluding rejoins would delete the only
68
+ * legitimate purpose. The transient-roster-gap case is excluded by the delivery
69
+ * ledger and the confirmation read instead, which discriminate on FACTS rather
70
+ * than on which callback fired.
71
+ */
72
+ const system_1 = require("@camstack/system");
58
73
  /** Triggers permitted to install packages onto a node. */
59
74
  const DELIVERING_TRIGGERS = new Set([
60
75
  'node-registration',
@@ -70,34 +85,56 @@ function triggerMayDeliver(trigger) {
70
85
  * Pure — no I/O, no clock, no randomness.
71
86
  */
72
87
  function computeBackfillPlan(input) {
73
- const liveNames = new Set(input.live.map((p) => p.name));
88
+ const liveByName = new Map(input.live.map((p) => [p.name, p.version]));
74
89
  const targets = [];
75
90
  const retired = [];
76
91
  const notOnHub = [];
77
92
  for (const entry of input.recorded) {
78
- if (liveNames.has(entry.name))
79
- continue;
80
93
  if (input.bootstrap.has(entry.name))
81
94
  continue;
82
95
  if (input.undeployed.has(entry.name))
83
96
  continue;
84
- if (input.retired.has(entry.name)) {
85
- retired.push(entry.name);
86
- continue;
87
- }
88
97
  const hubVersion = input.hubVersions.get(entry.name);
98
+ const present = liveByName.has(entry.name);
99
+ const liveVersion = present ? (liveByName.get(entry.name) ?? null) : null;
89
100
  if (hubVersion === undefined) {
90
- notOnHub.push(entry.name);
101
+ if (!present && input.retired.has(entry.name))
102
+ retired.push(entry.name);
103
+ else if (!present)
104
+ notOnHub.push(entry.name);
105
+ continue;
106
+ }
107
+ const decision = (0, system_1.decideAddonVersionMove)(present ? liveVersion : null, hubVersion, system_1.ADDON_VERSION_REASON_HUB);
108
+ if (!decision.move)
109
+ continue;
110
+ if (input.retired.has(entry.name)) {
111
+ retired.push(entry.name);
91
112
  continue;
92
113
  }
93
114
  targets.push({ name: entry.name, version: hubVersion, versionSource: 'hub-installed' });
94
115
  }
95
- return { targets, retired, notOnHub, liveCount: liveNames.size };
116
+ return { targets, retired, notOnHub, liveCount: liveByName.size };
96
117
  }
97
118
  function confirmBackfillPlan(plan, secondRead) {
98
- const present = new Set(secondRead.map((p) => p.name));
99
- const targets = plan.targets.filter((t) => !present.has(t.name));
100
- const contradicted = plan.targets.filter((t) => present.has(t.name));
119
+ const seen = new Map(secondRead.map((p) => [p.name, p.version]));
120
+ const targets = [];
121
+ const contradicted = [];
122
+ for (const target of plan.targets) {
123
+ if (!seen.has(target.name)) {
124
+ targets.push(target);
125
+ continue;
126
+ }
127
+ const version = seen.get(target.name) ?? null;
128
+ if (version === target.version) {
129
+ contradicted.push(target);
130
+ continue;
131
+ }
132
+ if (version === null) {
133
+ contradicted.push(target);
134
+ continue;
135
+ }
136
+ targets.push(target);
137
+ }
101
138
  return { plan: { ...plan, targets }, contradicted };
102
139
  }
103
140
  /**
@@ -845,7 +845,10 @@ class AgentRegistryService {
845
845
  }
846
846
  if (outcome.delivered.length > 0) {
847
847
  console.log(`[agent-registry] Back-fill ${agentId}: delivered ${outcome.delivered
848
- .map((d) => `${d.name}@${d.version} (pin from ${d.versionSource})`)
848
+ .map((d) => {
849
+ const from = live.find((p) => p.name === d.name)?.version ?? 'absent';
850
+ return `${d.name} ${from} → ${d.version} (agent follows the hub installed version)`;
851
+ })
849
852
  .join(', ')} — trigger=${trigger}, node reported ${plan.liveCount} package(s) and its ` +
850
853
  `declared roster has ${recorded.length}`);
851
854
  this.eventBus.emit({
@@ -901,9 +904,9 @@ class AgentRegistryService {
901
904
  }
902
905
  const { plan: confirmed, contradicted } = (0, agent_addon_backfill_1.confirmBackfillPlan)(plan, rosterFromAddons(secondAddons));
903
906
  if (contradicted.length > 0) {
904
- console.warn(`[agent-registry] Back-fill ${agentId}: two roster reads DISAGREE — ` +
905
- `${contradicted.map((t) => t.name).join(', ')} reported absent then present. ` +
906
- `Not installing; the node was mid-reload, not missing packages`);
907
+ console.warn(`[agent-registry] Back-fill ${agentId}: confirmation read already has ` +
908
+ `${contradicted.map((t) => `${t.name}@${t.version}`).join(', ')} ` +
909
+ `(or could not read the version). Not installing over that read`);
907
910
  }
908
911
  return confirmed;
909
912
  }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * What the About line shows: the product release (image / desktop app) and
4
+ * the running `@camstack/server` package. They are different lines (D943).
5
+ *
6
+ * The product version is declared by the shell — `CAMSTACK_PRODUCT_VERSION`,
7
+ * set by the image and by the desktop app. A dev workspace leaves it unset;
8
+ * that is absence, not a failure. The server version is the package this
9
+ * process loaded.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PRODUCT_VERSION_ENV = void 0;
13
+ exports.blankToNull = blankToNull;
14
+ exports.releaseIdentityFrom = releaseIdentityFrom;
15
+ exports.PRODUCT_VERSION_ENV = 'CAMSTACK_PRODUCT_VERSION';
16
+ function blankToNull(value) {
17
+ const trimmed = value?.trim() ?? '';
18
+ return trimmed.length > 0 ? trimmed : null;
19
+ }
20
+ function releaseIdentityFrom(input) {
21
+ return {
22
+ productVersion: blankToNull(input.productVersionEnv),
23
+ serverVersion: blankToNull(input.serverVersion ?? undefined),
24
+ };
25
+ }
package/dist/main.js CHANGED
@@ -67,6 +67,7 @@ const request_census_settings_1 = require("./api/core/request-census-settings");
67
67
  const register_cors_policy_1 = require("./api/cors/register-cors-policy");
68
68
  const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
69
69
  const health_routes_1 = require("./api/health/health.routes");
70
+ const release_identity_1 = require("./core/release-identity");
70
71
  const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
71
72
  const hub_cluster_url_1 = require("./api/hub-cluster-url");
72
73
  const model_distributor_js_1 = require("./api/model-distributor.js");
@@ -731,6 +732,7 @@ async function bootstrap() {
731
732
  moleculer,
732
733
  agentRegistry: app.get(agent_registry_service_1.AgentRegistryService),
733
734
  hubVersion,
735
+ productVersion: (0, release_identity_1.blankToNull)(process.env[release_identity_1.PRODUCT_VERSION_ENV]),
734
736
  // Read the live flag at request time — flips to true once the tRPC
735
737
  // router finishes registering (see `trpcRegistered = true` below).
736
738
  isReady: () => trpcRegistered,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.629",
3
+ "version": "1.2.631",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.466",
42
- "@camstack/addon-agent-ui": "1.2.266",
41
+ "@camstack/addon-admin-ui": "1.2.468",
42
+ "@camstack/addon-agent-ui": "1.2.267",
43
43
  "@camstack/addon-auth": "1.2.166",
44
44
  "@camstack/addon-benchmark": "1.2.266",
45
45
  "@camstack/addon-decoder-nodeav": "1.2.141",
46
46
  "@camstack/addon-notifiers": "1.2.619",
47
- "@camstack/addon-pipeline": "1.2.624",
47
+ "@camstack/addon-pipeline": "1.2.625",
48
48
  "@camstack/addon-pipeline-orchestrator": "1.2.627",
49
- "@camstack/addon-post-analysis": "1.2.633",
50
- "@camstack/develop": "0.1.33",
49
+ "@camstack/addon-post-analysis": "1.2.634",
50
+ "@camstack/develop": "0.1.34",
51
51
  "@camstack/sdk": "1.2.151",
52
- "@camstack/system": "1.2.480",
53
- "@camstack/types": "1.2.412",
52
+ "@camstack/system": "1.2.482",
53
+ "@camstack/types": "1.2.414",
54
54
  "@camstack/ui-library": "1.2.323",
55
55
  "@fastify/compress": "^9.0.0",
56
56
  "@fastify/cookie": "^11.0.2",