@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.
package/dist/cli.js CHANGED
@@ -65,6 +65,12 @@ function getRegistryNodes(broker) {
65
65
  return [];
66
66
  }
67
67
  }
68
+ function deriveHubConnectionState(nodes, discoveryMode) {
69
+ const hubInRegistry = nodes.some((n) => n.id === "hub");
70
+ if (hubInRegistry) return "connected";
71
+ if (discoveryMode) return "searching";
72
+ return "disconnected";
73
+ }
68
74
  function getEffectiveConfig(configPath, nodeId) {
69
75
  const raw = readConfigFile(configPath);
70
76
  return {
@@ -96,6 +102,7 @@ async function createAgentHttpServer(getBroker, config) {
96
102
  const nodes = getRegistryNodes(broker);
97
103
  const hubConnected = nodes.some((n) => n.id === "hub");
98
104
  const discoveryMode = !eff.hubAddress;
105
+ const hubConnectionState = config.getHubConnectionState ? config.getHubConnectionState() : deriveHubConnectionState(nodes, discoveryMode);
99
106
  try {
100
107
  const status = await broker.call("$agent.status");
101
108
  return {
@@ -103,6 +110,7 @@ async function createAgentHttpServer(getBroker, config) {
103
110
  name: eff.name,
104
111
  hubAddress: eff.hubAddress,
105
112
  hubConnected,
113
+ hubConnectionState,
106
114
  discoveryMode,
107
115
  hasSecret: eff.hasSecret,
108
116
  discoveredNodes: nodes.filter((n) => n.id !== broker.nodeID).map((n) => n.id)
@@ -113,6 +121,7 @@ async function createAgentHttpServer(getBroker, config) {
113
121
  name: eff.name,
114
122
  hubAddress: eff.hubAddress,
115
123
  hubConnected,
124
+ hubConnectionState,
116
125
  discoveryMode,
117
126
  hasSecret: eff.hasSecret,
118
127
  discoveredNodes: [],
@@ -360,6 +369,29 @@ async function applyDeployedBundle(input) {
360
369
  return { addonDir: liveDir };
361
370
  }
362
371
 
372
+ // src/fetch-bundle-from-hub.ts
373
+ var import_node_crypto2 = require("crypto");
374
+ var import_undici = require("undici");
375
+ async function fetchBundleFromHub(opts) {
376
+ const authHeader = { Authorization: `Bearer ${opts.token}` };
377
+ const res = opts.fetchImpl ? await opts.fetchImpl(opts.url, { headers: authHeader }) : await (0, import_undici.fetch)(opts.url, {
378
+ headers: authHeader,
379
+ dispatcher: new import_undici.Agent({ connect: { rejectUnauthorized: false } })
380
+ });
381
+ if (!res.ok) {
382
+ throw new Error(`bundle fetch failed: HTTP ${res.status}`);
383
+ }
384
+ const buffer = Buffer.from(await res.arrayBuffer());
385
+ if (buffer.length !== opts.bytes) {
386
+ throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
387
+ }
388
+ const sha = (0, import_node_crypto2.createHash)("sha256").update(buffer).digest("hex");
389
+ if (sha !== opts.sha256) {
390
+ throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
391
+ }
392
+ return buffer;
393
+ }
394
+
363
395
  // src/agent-service.ts
364
396
  var deploySwapLogger = {
365
397
  info: (msg) => console.log(`[Agent] ${msg}`),
@@ -393,6 +425,28 @@ async function withTimeout(p, ms, fallback) {
393
425
  }
394
426
  }
395
427
  var METRICS_SNAPSHOT_TIMEOUT_MS = 2e3;
428
+ function resolveDeployAction(seam) {
429
+ return async (params) => {
430
+ const { addonId, source, bundle } = params;
431
+ if (source && (0, import_system.isAddonDeploySource)(source)) {
432
+ if (source.kind === "npm") {
433
+ await seam.installFromNpm(addonId, source.version);
434
+ return { success: true, addonId };
435
+ }
436
+ const buf2 = await seam.fetchBundle(source);
437
+ const { addonDir: addonDir2 } = await seam.applyBundle(buf2);
438
+ seam.onApplied(addonDir2);
439
+ return { success: true, addonId, path: addonDir2 };
440
+ }
441
+ if (bundle === void 0) {
442
+ throw new Error("$agent.deploy: no source descriptor and no legacy bundle provided");
443
+ }
444
+ const buf = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
445
+ const { addonDir } = await seam.applyBundle(buf);
446
+ seam.onApplied(addonDir);
447
+ return { success: true, addonId, path: addonDir };
448
+ };
449
+ }
396
450
  function readHubAddressFromConfig(configPath) {
397
451
  if (!configPath) return null;
398
452
  try {
@@ -565,8 +619,7 @@ function createAgentService(deps) {
565
619
  deploy: {
566
620
  handler: async (ctx) => {
567
621
  const { params } = ctx;
568
- const { addonId, bundle } = params;
569
- const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
622
+ const { addonId, source, bundle } = params;
570
623
  const { execFile } = await import("child_process");
571
624
  const { promisify } = await import("util");
572
625
  const execFileAsync = promisify(execFile);
@@ -584,17 +637,37 @@ function createAgentService(deps) {
584
637
  }
585
638
  }
586
639
  };
587
- const { addonDir } = await applyDeployedBundle({
588
- addonsDir: deps.addonsDir,
589
- addonId,
590
- bundle: bufferData,
591
- extract,
592
- logger: deploySwapLogger
593
- });
594
- for (const declId of readDeployedAddonIds(addonDir)) {
595
- deps.loadedAddons.delete(declId);
596
- }
597
- return { success: true, addonId, path: addonDir };
640
+ const seam = {
641
+ installFromNpm: (pkg, v) => {
642
+ if (!deps.installFromNpm) {
643
+ throw new Error("npm deploy source unsupported on this agent");
644
+ }
645
+ return deps.installFromNpm(pkg, v);
646
+ },
647
+ applyBundle: (buf) => applyDeployedBundle({
648
+ addonsDir: deps.addonsDir,
649
+ addonId,
650
+ bundle: buf,
651
+ extract,
652
+ logger: deploySwapLogger
653
+ }),
654
+ fetchBundle: (s) => fetchBundleFromHub(s),
655
+ // Evict every addon DECLARATION id this package contributes
656
+ // from `loadedAddons` so the follow-up `$agent.reload` actually
657
+ // re-instantiates it. `loadDeployedAddons` skips any addon
658
+ // still present in `loadedAddons`; without this eviction a
659
+ // redeploy would leave the agent pinned to the pre-update
660
+ // version. The `deploy` param `addonId` is the PACKAGE name
661
+ // (used only as the on-disk dir), whereas `loadedAddons` is
662
+ // keyed by the addon DECLARATION id — they differ for scoped
663
+ // packages, so we read the extracted manifest to bridge them.
664
+ onApplied: (addonDir) => {
665
+ for (const declId of readDeployedAddonIds(addonDir)) {
666
+ deps.loadedAddons.delete(declId);
667
+ }
668
+ }
669
+ };
670
+ return resolveDeployAction(seam)({ addonId, source, bundle });
598
671
  }
599
672
  },
600
673
  /**
@@ -802,7 +875,7 @@ function narrowParams(raw) {
802
875
  deviceId: typeof deviceId === "number" ? deviceId : void 0
803
876
  };
804
877
  }
805
- function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
878
+ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, inProcessLookup, logger) {
806
879
  return {
807
880
  name: import_system2.AGENT_CAP_FWD_SERVICE,
808
881
  actions: {
@@ -812,11 +885,15 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
812
885
  const { capName, method, args: args2, deviceId } = params;
813
886
  const childId = params.childId !== void 0 ? params.childId : agentUdsRegistry.resolveChildId(capName, deviceId);
814
887
  if (childId == null) {
888
+ const ref = inProcessLookup?.(capName) ?? null;
889
+ if (ref !== null) {
890
+ return ref.invoke(method, args2);
891
+ }
815
892
  logger?.info(
816
- `agent ${agentNodeId}: no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
893
+ `agent ${agentNodeId}: no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
817
894
  );
818
895
  throw new Error(
819
- `agent ${agentNodeId} has no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
896
+ `agent ${agentNodeId} has no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
820
897
  );
821
898
  }
822
899
  const input = {
@@ -833,9 +910,11 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
833
910
  }
834
911
 
835
912
  // src/register-agent-cap-dispatch.ts
836
- function registerAgentCapDispatch(registrar, agentUdsRegistry, logger) {
913
+ function registerAgentCapDispatch(registrar, agentUdsRegistry, inProcessLookup, logger) {
837
914
  if (agentUdsRegistry === void 0) return;
838
- registrar.createService(createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, logger));
915
+ registrar.createService(
916
+ createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, inProcessLookup, logger)
917
+ );
839
918
  }
840
919
 
841
920
  // src/agent-bootstrap.ts
@@ -901,6 +980,27 @@ async function startAgent(configPath) {
901
980
  };
902
981
  const loadedAddons = /* @__PURE__ */ new Map();
903
982
  const subtree = new import_system3.HubNodeRegistry();
983
+ let hubConnectionStateOverride = null;
984
+ function getHubConnectionState() {
985
+ if (hubConnectionStateOverride !== null) return hubConnectionStateOverride;
986
+ try {
987
+ const registry = broker.registry;
988
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
989
+ if (nodes.some((n) => n.id === "hub")) return "connected";
990
+ } catch {
991
+ }
992
+ const configNow = readConfigFile2(config.configPath);
993
+ const discoveryMode = typeof configNow.hubAddress !== "string" || configNow.hubAddress.length === 0;
994
+ return discoveryMode ? "searching" : "disconnected";
995
+ }
996
+ function readConfigFile2(p) {
997
+ if (!fs6.existsSync(p)) return {};
998
+ try {
999
+ return JSON.parse(fs6.readFileSync(p, "utf-8"));
1000
+ } catch {
1001
+ return {};
1002
+ }
1003
+ }
904
1004
  const registerAbortController = new AbortController();
905
1005
  function buildAgentOwnManifest() {
906
1006
  const addonCapMap = /* @__PURE__ */ new Map();
@@ -963,6 +1063,7 @@ async function startAgent(configPath) {
963
1063
  consoleLogger.error(
964
1064
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
965
1065
  );
1066
+ hubConnectionStateOverride = "secret-mismatch";
966
1067
  return;
967
1068
  }
968
1069
  });
@@ -1033,7 +1134,10 @@ async function startAgent(configPath) {
1033
1134
  subtree.registerNode(params);
1034
1135
  triggerUpwardRegistration();
1035
1136
  },
1036
- expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
1137
+ expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
1138
+ installFromNpm: async (pkg, version) => {
1139
+ await installer.install(pkg, version);
1140
+ }
1037
1141
  });
1038
1142
  broker.createService(agentServiceSchema);
1039
1143
  const agentTcpPort = (0, import_system3.deriveAgentListenPort)(broker.nodeID);
@@ -1092,7 +1196,21 @@ async function startAgent(configPath) {
1092
1196
  agentParentUdsPath
1093
1197
  );
1094
1198
  broker.createService(processServiceSchema);
1095
- registerAgentCapDispatch(broker, agentUdsRegistry, consoleLogger);
1199
+ const agentInProcessLookup = (capName) => {
1200
+ const provider = capabilityRegistry.getSingleton(capName);
1201
+ if (provider === null || provider === void 0) return null;
1202
+ return {
1203
+ invoke: (method, args2) => {
1204
+ const fn = provider[method];
1205
+ if (typeof fn !== "function") {
1206
+ return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`));
1207
+ }
1208
+ const result = fn.call(provider, args2);
1209
+ return Promise.resolve(result);
1210
+ }
1211
+ };
1212
+ };
1213
+ registerAgentCapDispatch(broker, agentUdsRegistry, agentInProcessLookup, consoleLogger);
1096
1214
  (0, import_system3.registerEventBusService)(broker);
1097
1215
  broker.createService((0, import_system3.createHwAccelService)((0, import_system3.createKernelHwAccel)()));
1098
1216
  await broker.start();
@@ -1119,7 +1237,8 @@ async function startAgent(configPath) {
1119
1237
  nodeId: config.nodeId,
1120
1238
  dataDir: config.dataDir,
1121
1239
  configPath: config.configPath,
1122
- onReconnect: reconnect
1240
+ onReconnect: reconnect,
1241
+ getHubConnectionState
1123
1242
  });
1124
1243
  await bootCoreAddons(broker, config, capabilityRegistry, loadedAddons, loggerFactory);
1125
1244
  for (const dest of capabilityRegistry.getCollection("log-destination")) {
@@ -1141,6 +1260,9 @@ async function startAgent(configPath) {
1141
1260
  broker.localBus.on("$node.connected", (data) => {
1142
1261
  const node = data.node;
1143
1262
  if (node?.id !== "hub") return;
1263
+ if (hubConnectionStateOverride === "secret-mismatch") {
1264
+ hubConnectionStateOverride = null;
1265
+ }
1144
1266
  triggerUpwardRegistration();
1145
1267
  if (hubReachableFired) return;
1146
1268
  hubReachableFired = true;