@camstack/server 1.2.564 → 1.2.566

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.
@@ -50,6 +50,9 @@ const agent_config_js_1 = require("./agent-config.js");
50
50
  const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
51
51
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
52
52
  const agent_http_js_1 = require("./agent-http.js");
53
+ const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
54
+ const agent_reconnect_js_1 = require("./agent-reconnect.js");
55
+ const serial_queue_js_1 = require("./serial-queue.js");
53
56
  const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
54
57
  const agent_service_js_1 = require("./agent-service.js");
55
58
  const agent_hub_addon_update_js_1 = require("./agent-hub-addon-update.js");
@@ -65,6 +68,8 @@ const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.
65
68
  // entries flow through it (console output + hub forwarding).
66
69
  // ---------------------------------------------------------------------------
67
70
  const system_2 = require("@camstack/system");
71
+ /** A reconnect (stop + rebuild + start) that takes longer than this is abandoned. */
72
+ const RECONNECT_DEADLINE_MS = 90_000;
68
73
  const agentLogManager = new system_2.LogManager(5000);
69
74
  /**
70
75
  * Caps whose `nodeId` arg is provider DATA (`nodeIdMode: 'data'` —
@@ -205,6 +210,8 @@ async function startAgent(configPath) {
205
210
  // Mesh mTLS (Release A): with a certificate the broker dials the hub's TLS
206
211
  // port; without one it keeps dialing clear 6000.
207
212
  let currentMeshTls = config.meshTls ?? null;
213
+ // The HTTP/HTTPS listener on the agent port (D856); set once it is up.
214
+ let agentHttpListener = null;
208
215
  console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls' : 'clear'}`);
209
216
  let broker = (0, system_1.createBroker)({
210
217
  nodeID: config.nodeId,
@@ -220,45 +227,74 @@ async function startAgent(configPath) {
220
227
  * create a new broker with the updated hub/secret, and restart.
221
228
  * The HTTP server stays alive throughout.
222
229
  */
223
- const reconnect = async () => {
224
- console.log('[Agent] Reconnecting with updated config...');
225
- try {
226
- await broker.stop();
227
- }
228
- catch {
229
- /* already stopped */
230
- }
231
- // Reload config from file (UI may have written new hubAddress/secret)
232
- const fresh = (0, agent_config_js_1.loadAgentConfig)(undefined, config.dataDir);
233
- console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'none'}, secret=${fresh.secret ? 'yes' : 'none'}, ` +
234
- `mesh=${fresh.meshTls !== undefined ? 'tls' : 'clear'}`);
235
- // Re-derive CAMSTACK_HUB_URL when the dashboard has just written a hubAddress. Explicit operator values
236
- // still win via the boot-captured `hubUrlWasExplicit` flag.
237
- // KNOWN LIMITATION (Stage-0): runners already spawned before this reconnect
238
- // keep the env they inherited; picking up a changed hub address in live
239
- // children would need a runner respawn. Boot-time — the actual trap — is
240
- // fully covered.
241
- const freshHubUrl = (0, derive_hub_url_js_1.deriveHubUrlForExport)(hubUrlWasExplicit, fresh.hubAddress);
242
- if (freshHubUrl !== undefined && process.env['CAMSTACK_HUB_URL'] !== freshHubUrl) {
243
- process.env['CAMSTACK_HUB_URL'] = freshHubUrl;
244
- console.log(`[Agent] Derived CAMSTACK_HUB_URL=${freshHubUrl} from hub address "${fresh.hubAddress}"`);
245
- }
246
- currentSecret = fresh.secret ?? null;
247
- currentMeshTls = fresh.meshTls ?? null;
248
- broker = (0, system_1.createBroker)({
249
- nodeID: config.nodeId,
250
- mode: 'agent',
251
- hubAddress: fresh.hubAddress,
252
- logLevel: fresh.logLevel,
253
- middlewares: meshJoin.middlewares,
254
- ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
255
- });
256
- meshJoin.attach(broker);
230
+ // ONE reconnect at a time (serial-queue.ts): the UI, a mesh leaf swap and its rollback can
231
+ // all ask for one, and two interleaved stop/rebuild cycles would leave two brokers or none.
232
+ const reconnect = (0, serial_queue_js_1.createSerialQueue)(() => reconnectNow(), {
233
+ deadlineMs: RECONNECT_DEADLINE_MS,
234
+ onTimeout: () => {
235
+ // A broker whose start() never settles must not hold every later reconnect (including
236
+ // the rollback of a failed leaf swap): tear down what was half-built and move on.
237
+ consoleLogger.error('Reconnect did not finish in time - destroying the half-built broker', {
238
+ meta: { deadlineMs: RECONNECT_DEADLINE_MS },
239
+ });
240
+ void broker.stop().catch(() => undefined);
241
+ },
242
+ });
243
+ // The cycle itself runs through a generation guard: a run the queue abandoned at its deadline
244
+ // that resumes later builds nothing (agent-reconnect.ts), so two brokers never share a node id.
245
+ let pendingConfig = null;
246
+ const reconnectNow = (0, agent_reconnect_js_1.createReconnectRunner)({
247
+ current: () => broker,
248
+ assign: (next) => {
249
+ broker = next;
250
+ },
251
+ stop: (target) => target.stop(),
252
+ prepare: () => {
253
+ console.log('[Agent] Reconnecting with updated config...');
254
+ // Reload config from file (UI may have written new hubAddress/secret)
255
+ const fresh = (0, agent_config_js_1.loadAgentConfig)(undefined, config.dataDir);
256
+ pendingConfig = fresh;
257
+ console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'none'}, secret=${fresh.secret ? 'yes' : 'none'}, ` +
258
+ `mesh=${fresh.meshTls !== undefined ? 'tls' : 'clear'}`);
259
+ // Re-derive CAMSTACK_HUB_URL when the dashboard has just written a hubAddress. Explicit operator values
260
+ // still win via the boot-captured `hubUrlWasExplicit` flag.
261
+ // KNOWN LIMITATION (Stage-0): runners already spawned before this reconnect
262
+ // keep the env they inherited; picking up a changed hub address in live
263
+ // children would need a runner respawn. Boot-time — the actual trap — is
264
+ // fully covered.
265
+ const freshHubUrl = (0, derive_hub_url_js_1.deriveHubUrlForExport)(hubUrlWasExplicit, fresh.hubAddress);
266
+ if (freshHubUrl !== undefined && process.env['CAMSTACK_HUB_URL'] !== freshHubUrl) {
267
+ process.env['CAMSTACK_HUB_URL'] = freshHubUrl;
268
+ console.log(`[Agent] Derived CAMSTACK_HUB_URL=${freshHubUrl} from hub address "${fresh.hubAddress}"`);
269
+ }
270
+ currentSecret = fresh.secret ?? null;
271
+ currentMeshTls = fresh.meshTls ?? null;
272
+ },
273
+ build: () => {
274
+ const fresh = pendingConfig;
275
+ if (fresh === null)
276
+ throw new Error('reconnect: no configuration was prepared');
277
+ const next = (0, system_1.createBroker)({
278
+ nodeID: config.nodeId,
279
+ mode: 'agent',
280
+ hubAddress: fresh.hubAddress,
281
+ logLevel: fresh.logLevel,
282
+ middlewares: meshJoin.middlewares,
283
+ ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
284
+ });
285
+ meshJoin.attach(next);
286
+ return next;
287
+ },
257
288
  // The new broker's hub connection runs a join, and its settle re-sends
258
289
  // the manifest (`registration.onJoinSettled`).
259
- await broker.start();
260
- console.log('[Agent] Reconnected successfully');
261
- };
290
+ start: async (next) => {
291
+ await next.start();
292
+ console.log('[Agent] Reconnected successfully');
293
+ },
294
+ onSuperseded: (stage) => consoleLogger.warn('A superseded reconnect stopped without building a second broker', {
295
+ meta: { stage },
296
+ }),
297
+ });
262
298
  const loadedAddons = new Map();
263
299
  // D3 subtree registry: accumulates manifests from every group-runner child
264
300
  // that calls `$agent.registerNode`. The agent re-registers the UNION of its
@@ -444,6 +480,9 @@ async function startAgent(configPath) {
444
480
  types_1.audioAnalyzerCapability,
445
481
  types_1.platformProbeCapability,
446
482
  types_1.serverManagementCapability,
483
+ // Certificates of THIS node, called by the hub (D856): mesh leaf renewal and the
484
+ // hub-issued HTTPS leaf. Provided by the runtime, like `server-management`.
485
+ types_1.nodeCertificatesCapability,
447
486
  ];
448
487
  for (const cap of agentCapabilities) {
449
488
  capabilityRegistry.declareCapability(cap);
@@ -460,6 +499,22 @@ async function startAgent(configPath) {
460
499
  dataDir: config.dataDir,
461
500
  });
462
501
  capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
502
+ // The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
503
+ // hub-issued HTTPS leaf hot-swapped into the sniffing listener. The listener is
504
+ // created later in boot, so it is read through a getter.
505
+ capabilityRegistry.registerProvider('node-certificates', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, agent_node_certificates_js_1.createAgentNodeCertificates)({
506
+ nodeId: config.nodeId,
507
+ dataDir: config.dataDir,
508
+ configPath: config.configPath,
509
+ reconnect,
510
+ getHttpsListener: () => agentHttpListener,
511
+ hostFacts: (0, system_2.systemHostFacts)(),
512
+ log: {
513
+ info: (message, extras) => consoleLogger.info(message, extras),
514
+ warn: (message, extras) => consoleLogger.warn(message, extras),
515
+ error: (message, extras) => consoleLogger.error(message, extras),
516
+ },
517
+ }));
463
518
  // Boot-health confirmation: on the agent's FIRST acked `$hub.registerNode`,
464
519
  // run the single-copy GC sweep once. Under the single-copy model there is NO
465
520
  // probation boot, NO N-1 promotion, and NO auto-rollback — the starter
@@ -838,6 +893,11 @@ async function startAgent(configPath) {
838
893
  },
839
894
  };
840
895
  void (0, agent_http_js_1.startAgentHttpServer)(getBrokerFn, {
896
+ log: {
897
+ info: (message, extras) => consoleLogger.info(message, extras),
898
+ warn: (message, extras) => consoleLogger.warn(message, extras),
899
+ error: (message, extras) => consoleLogger.error(message, extras),
900
+ },
841
901
  port: config.statusPort ?? 4444,
842
902
  nodeId: config.nodeId,
843
903
  dataDir: config.dataDir,
@@ -846,6 +906,8 @@ async function startAgent(configPath) {
846
906
  getHubConnectionState,
847
907
  getMeshTransport: () => (currentMeshTls !== null ? 'tls' : 'clear'),
848
908
  updateControls,
909
+ }).then((listener) => {
910
+ agentHttpListener = listener;
849
911
  });
850
912
  // ── Phase 1: Load core infrastructure addons (in-process) ──
851
913
  // storage + settings + metrics + hub-forwarder (log destination)
@@ -922,6 +984,8 @@ async function startAgent(configPath) {
922
984
  }
923
985
  }
924
986
  }
987
+ // Stop accepting new HTTP/HTTPS connections before the broker goes away.
988
+ await agentHttpListener?.close().catch(() => undefined);
925
989
  await broker.stop();
926
990
  process.exit(0);
927
991
  };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SerialQueueTimeoutError = void 0;
4
+ exports.createSerialQueue = createSerialQueue;
5
+ class SerialQueueTimeoutError extends Error {
6
+ deadlineMs;
7
+ constructor(deadlineMs) {
8
+ super(`the job did not finish within ${deadlineMs} ms and was abandoned`);
9
+ this.deadlineMs = deadlineMs;
10
+ this.name = 'SerialQueueTimeoutError';
11
+ }
12
+ }
13
+ exports.SerialQueueTimeoutError = SerialQueueTimeoutError;
14
+ function defaultSetTimer(fn, ms) {
15
+ const timer = setTimeout(fn, ms);
16
+ timer.unref?.();
17
+ return { cancel: () => clearTimeout(timer) };
18
+ }
19
+ function createSerialQueue(job, options = {}) {
20
+ let tail = Promise.resolve();
21
+ const setTimer = options.setTimer ?? defaultSetTimer;
22
+ const guarded = () => {
23
+ const deadlineMs = options.deadlineMs;
24
+ if (deadlineMs === undefined)
25
+ return job();
26
+ return new Promise((resolve, reject) => {
27
+ const timer = setTimer(() => {
28
+ try {
29
+ options.onTimeout?.();
30
+ }
31
+ finally {
32
+ reject(new SerialQueueTimeoutError(deadlineMs));
33
+ }
34
+ }, deadlineMs);
35
+ job().then((value) => {
36
+ timer.cancel();
37
+ resolve(value);
38
+ }, (error) => {
39
+ timer.cancel();
40
+ reject(error);
41
+ });
42
+ });
43
+ };
44
+ return () => {
45
+ const run = tail.then(guarded, guarded);
46
+ tail = run.catch(() => undefined);
47
+ return run;
48
+ };
49
+ }