@camstack/agent 1.0.7 → 1.1.0

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.
@@ -69,7 +69,11 @@ import * as os2 from "os";
69
69
  import * as fs3 from "fs";
70
70
  import * as path3 from "path";
71
71
  import { Errors } from "moleculer";
72
- import { clusterSecretMatches, CLUSTER_SECRET_MISMATCH_TYPE } from "@camstack/system";
72
+ import {
73
+ clusterSecretMatches,
74
+ CLUSTER_SECRET_MISMATCH_TYPE,
75
+ isAddonDeploySource
76
+ } from "@camstack/system";
73
77
 
74
78
  // src/agent-deploy-swap.ts
75
79
  import * as fs2 from "fs";
@@ -138,6 +142,29 @@ async function applyDeployedBundle(input) {
138
142
  return { addonDir: liveDir };
139
143
  }
140
144
 
145
+ // src/fetch-bundle-from-hub.ts
146
+ import { createHash } from "crypto";
147
+ import { Agent, fetch as undicicFetch } from "undici";
148
+ async function fetchBundleFromHub(opts) {
149
+ const authHeader = { Authorization: `Bearer ${opts.token}` };
150
+ const res = opts.fetchImpl ? await opts.fetchImpl(opts.url, { headers: authHeader }) : await undicicFetch(opts.url, {
151
+ headers: authHeader,
152
+ dispatcher: new Agent({ connect: { rejectUnauthorized: false } })
153
+ });
154
+ if (!res.ok) {
155
+ throw new Error(`bundle fetch failed: HTTP ${res.status}`);
156
+ }
157
+ const buffer = Buffer.from(await res.arrayBuffer());
158
+ if (buffer.length !== opts.bytes) {
159
+ throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
160
+ }
161
+ const sha = createHash("sha256").update(buffer).digest("hex");
162
+ if (sha !== opts.sha256) {
163
+ throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
164
+ }
165
+ return buffer;
166
+ }
167
+
141
168
  // src/agent-service.ts
142
169
  var deploySwapLogger = {
143
170
  info: (msg) => console.log(`[Agent] ${msg}`),
@@ -171,6 +198,28 @@ async function withTimeout(p, ms, fallback) {
171
198
  }
172
199
  }
173
200
  var METRICS_SNAPSHOT_TIMEOUT_MS = 2e3;
201
+ function resolveDeployAction(seam) {
202
+ return async (params) => {
203
+ const { addonId, source, bundle } = params;
204
+ if (source && isAddonDeploySource(source)) {
205
+ if (source.kind === "npm") {
206
+ await seam.installFromNpm(addonId, source.version);
207
+ return { success: true, addonId };
208
+ }
209
+ const buf2 = await seam.fetchBundle(source);
210
+ const { addonDir: addonDir2 } = await seam.applyBundle(buf2);
211
+ seam.onApplied(addonDir2);
212
+ return { success: true, addonId, path: addonDir2 };
213
+ }
214
+ if (bundle === void 0) {
215
+ throw new Error("$agent.deploy: no source descriptor and no legacy bundle provided");
216
+ }
217
+ const buf = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
218
+ const { addonDir } = await seam.applyBundle(buf);
219
+ seam.onApplied(addonDir);
220
+ return { success: true, addonId, path: addonDir };
221
+ };
222
+ }
174
223
  function readHubAddressFromConfig(configPath) {
175
224
  if (!configPath) return null;
176
225
  try {
@@ -343,8 +392,7 @@ function createAgentService(deps) {
343
392
  deploy: {
344
393
  handler: async (ctx) => {
345
394
  const { params } = ctx;
346
- const { addonId, bundle } = params;
347
- const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
395
+ const { addonId, source, bundle } = params;
348
396
  const { execFile } = await import("child_process");
349
397
  const { promisify } = await import("util");
350
398
  const execFileAsync = promisify(execFile);
@@ -362,17 +410,37 @@ function createAgentService(deps) {
362
410
  }
363
411
  }
364
412
  };
365
- const { addonDir } = await applyDeployedBundle({
366
- addonsDir: deps.addonsDir,
367
- addonId,
368
- bundle: bufferData,
369
- extract,
370
- logger: deploySwapLogger
371
- });
372
- for (const declId of readDeployedAddonIds(addonDir)) {
373
- deps.loadedAddons.delete(declId);
374
- }
375
- return { success: true, addonId, path: addonDir };
413
+ const seam = {
414
+ installFromNpm: (pkg, v) => {
415
+ if (!deps.installFromNpm) {
416
+ throw new Error("npm deploy source unsupported on this agent");
417
+ }
418
+ return deps.installFromNpm(pkg, v);
419
+ },
420
+ applyBundle: (buf) => applyDeployedBundle({
421
+ addonsDir: deps.addonsDir,
422
+ addonId,
423
+ bundle: buf,
424
+ extract,
425
+ logger: deploySwapLogger
426
+ }),
427
+ fetchBundle: (s) => fetchBundleFromHub(s),
428
+ // Evict every addon DECLARATION id this package contributes
429
+ // from `loadedAddons` so the follow-up `$agent.reload` actually
430
+ // re-instantiates it. `loadDeployedAddons` skips any addon
431
+ // still present in `loadedAddons`; without this eviction a
432
+ // redeploy would leave the agent pinned to the pre-update
433
+ // version. The `deploy` param `addonId` is the PACKAGE name
434
+ // (used only as the on-disk dir), whereas `loadedAddons` is
435
+ // keyed by the addon DECLARATION id — they differ for scoped
436
+ // packages, so we read the extracted manifest to bridge them.
437
+ onApplied: (addonDir) => {
438
+ for (const declId of readDeployedAddonIds(addonDir)) {
439
+ deps.loadedAddons.delete(declId);
440
+ }
441
+ }
442
+ };
443
+ return resolveDeployAction(seam)({ addonId, source, bundle });
376
444
  }
377
445
  },
378
446
  /**
@@ -512,6 +580,12 @@ function getRegistryNodes(broker) {
512
580
  return [];
513
581
  }
514
582
  }
583
+ function deriveHubConnectionState(nodes, discoveryMode) {
584
+ const hubInRegistry = nodes.some((n) => n.id === "hub");
585
+ if (hubInRegistry) return "connected";
586
+ if (discoveryMode) return "searching";
587
+ return "disconnected";
588
+ }
515
589
  function getEffectiveConfig(configPath, nodeId) {
516
590
  const raw = readConfigFile(configPath);
517
591
  return {
@@ -543,6 +617,7 @@ async function createAgentHttpServer(getBroker, config) {
543
617
  const nodes = getRegistryNodes(broker);
544
618
  const hubConnected = nodes.some((n) => n.id === "hub");
545
619
  const discoveryMode = !eff.hubAddress;
620
+ const hubConnectionState = config.getHubConnectionState ? config.getHubConnectionState() : deriveHubConnectionState(nodes, discoveryMode);
546
621
  try {
547
622
  const status = await broker.call("$agent.status");
548
623
  return {
@@ -550,6 +625,7 @@ async function createAgentHttpServer(getBroker, config) {
550
625
  name: eff.name,
551
626
  hubAddress: eff.hubAddress,
552
627
  hubConnected,
628
+ hubConnectionState,
553
629
  discoveryMode,
554
630
  hasSecret: eff.hasSecret,
555
631
  discoveredNodes: nodes.filter((n) => n.id !== broker.nodeID).map((n) => n.id)
@@ -560,6 +636,7 @@ async function createAgentHttpServer(getBroker, config) {
560
636
  name: eff.name,
561
637
  hubAddress: eff.hubAddress,
562
638
  hubConnected,
639
+ hubConnectionState,
563
640
  discoveryMode,
564
641
  hasSecret: eff.hasSecret,
565
642
  discoveredNodes: [],
@@ -819,7 +896,7 @@ function narrowParams(raw) {
819
896
  deviceId: typeof deviceId === "number" ? deviceId : void 0
820
897
  };
821
898
  }
822
- function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
899
+ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, inProcessLookup, logger) {
823
900
  return {
824
901
  name: AGENT_CAP_FWD_SERVICE,
825
902
  actions: {
@@ -829,11 +906,15 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
829
906
  const { capName, method, args, deviceId } = params;
830
907
  const childId = params.childId !== void 0 ? params.childId : agentUdsRegistry.resolveChildId(capName, deviceId);
831
908
  if (childId == null) {
909
+ const ref = inProcessLookup?.(capName) ?? null;
910
+ if (ref !== null) {
911
+ return ref.invoke(method, args);
912
+ }
832
913
  logger?.info(
833
- `agent ${agentNodeId}: no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
914
+ `agent ${agentNodeId}: no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
834
915
  );
835
916
  throw new Error(
836
- `agent ${agentNodeId} has no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
917
+ `agent ${agentNodeId} has no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
837
918
  );
838
919
  }
839
920
  const input = {
@@ -850,9 +931,11 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
850
931
  }
851
932
 
852
933
  // src/register-agent-cap-dispatch.ts
853
- function registerAgentCapDispatch(registrar, agentUdsRegistry, logger) {
934
+ function registerAgentCapDispatch(registrar, agentUdsRegistry, inProcessLookup, logger) {
854
935
  if (agentUdsRegistry === void 0) return;
855
- registrar.createService(createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, logger));
936
+ registrar.createService(
937
+ createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, inProcessLookup, logger)
938
+ );
856
939
  }
857
940
 
858
941
  // src/agent-bootstrap.ts
@@ -918,6 +1001,27 @@ async function startAgent(configPath) {
918
1001
  };
919
1002
  const loadedAddons = /* @__PURE__ */ new Map();
920
1003
  const subtree = new HubNodeRegistry();
1004
+ let hubConnectionStateOverride = null;
1005
+ function getHubConnectionState() {
1006
+ if (hubConnectionStateOverride !== null) return hubConnectionStateOverride;
1007
+ try {
1008
+ const registry = broker.registry;
1009
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
1010
+ if (nodes.some((n) => n.id === "hub")) return "connected";
1011
+ } catch {
1012
+ }
1013
+ const configNow = readConfigFile2(config.configPath);
1014
+ const discoveryMode = typeof configNow.hubAddress !== "string" || configNow.hubAddress.length === 0;
1015
+ return discoveryMode ? "searching" : "disconnected";
1016
+ }
1017
+ function readConfigFile2(p) {
1018
+ if (!fs6.existsSync(p)) return {};
1019
+ try {
1020
+ return JSON.parse(fs6.readFileSync(p, "utf-8"));
1021
+ } catch {
1022
+ return {};
1023
+ }
1024
+ }
921
1025
  const registerAbortController = new AbortController();
922
1026
  function buildAgentOwnManifest() {
923
1027
  const addonCapMap = /* @__PURE__ */ new Map();
@@ -980,6 +1084,7 @@ async function startAgent(configPath) {
980
1084
  consoleLogger.error(
981
1085
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
982
1086
  );
1087
+ hubConnectionStateOverride = "secret-mismatch";
983
1088
  return;
984
1089
  }
985
1090
  });
@@ -1050,7 +1155,10 @@ async function startAgent(configPath) {
1050
1155
  subtree.registerNode(params);
1051
1156
  triggerUpwardRegistration();
1052
1157
  },
1053
- expectedClusterSecretHash: config.secret ? hashClusterSecret(config.secret) : void 0
1158
+ expectedClusterSecretHash: config.secret ? hashClusterSecret(config.secret) : void 0,
1159
+ installFromNpm: async (pkg, version) => {
1160
+ await installer.install(pkg, version);
1161
+ }
1054
1162
  });
1055
1163
  broker.createService(agentServiceSchema);
1056
1164
  const agentTcpPort = deriveAgentListenPort(broker.nodeID);
@@ -1109,7 +1217,21 @@ async function startAgent(configPath) {
1109
1217
  agentParentUdsPath
1110
1218
  );
1111
1219
  broker.createService(processServiceSchema);
1112
- registerAgentCapDispatch(broker, agentUdsRegistry, consoleLogger);
1220
+ const agentInProcessLookup = (capName) => {
1221
+ const provider = capabilityRegistry.getSingleton(capName);
1222
+ if (provider === null || provider === void 0) return null;
1223
+ return {
1224
+ invoke: (method, args) => {
1225
+ const fn = provider[method];
1226
+ if (typeof fn !== "function") {
1227
+ return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`));
1228
+ }
1229
+ const result = fn.call(provider, args);
1230
+ return Promise.resolve(result);
1231
+ }
1232
+ };
1233
+ };
1234
+ registerAgentCapDispatch(broker, agentUdsRegistry, agentInProcessLookup, consoleLogger);
1113
1235
  registerEventBusService(broker);
1114
1236
  broker.createService(createHwAccelService(createKernelHwAccel()));
1115
1237
  await broker.start();
@@ -1136,7 +1258,8 @@ async function startAgent(configPath) {
1136
1258
  nodeId: config.nodeId,
1137
1259
  dataDir: config.dataDir,
1138
1260
  configPath: config.configPath,
1139
- onReconnect: reconnect
1261
+ onReconnect: reconnect,
1262
+ getHubConnectionState
1140
1263
  });
1141
1264
  await bootCoreAddons(broker, config, capabilityRegistry, loadedAddons, loggerFactory);
1142
1265
  for (const dest of capabilityRegistry.getCollection("log-destination")) {
@@ -1158,6 +1281,9 @@ async function startAgent(configPath) {
1158
1281
  broker.localBus.on("$node.connected", (data) => {
1159
1282
  const node = data.node;
1160
1283
  if (node?.id !== "hub") return;
1284
+ if (hubConnectionStateOverride === "secret-mismatch") {
1285
+ hubConnectionStateOverride = null;
1286
+ }
1161
1287
  triggerUpwardRegistration();
1162
1288
  if (hubReachableFired) return;
1163
1289
  hubReachableFired = true;
@@ -1509,4 +1635,4 @@ export {
1509
1635
  startAgentHttpServer,
1510
1636
  startAgent
1511
1637
  };
1512
- //# sourceMappingURL=chunk-GM2IV7NO.mjs.map
1638
+ //# sourceMappingURL=chunk-PFQZTXGW.mjs.map