@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.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __cr } from 'node:module'; const require = globalThis.require ?? __cr(import.meta.url);
3
3
  import {
4
4
  startAgent
5
- } from "./chunk-GM2IV7NO.mjs";
5
+ } from "./chunk-PFQZTXGW.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import * as os from "os";
package/dist/index.d.ts CHANGED
@@ -75,11 +75,27 @@ interface AgentServiceDeps {
75
75
  * presenting a matching `clusterSecretHash`. (Cluster-secret gate.)
76
76
  */
77
77
  readonly expectedClusterSecretHash?: string;
78
+ /** Pull-install a published addon from npm/GHCR (wired by agent-bootstrap). */
79
+ readonly installFromNpm?: (packageName: string, version: string) => Promise<void>;
78
80
  }
79
81
  declare function createAgentService(deps: AgentServiceDeps): ServiceSchema;
80
82
 
81
83
  declare function startAgent(configPath?: string): Promise<void>;
82
84
 
85
+ /**
86
+ * Discriminated hub connection state surfaced by the agent status API.
87
+ *
88
+ * - `connected` — hub node is in the Moleculer registry (available).
89
+ * - `searching` — discovery mode active, no hub found yet.
90
+ * - `disconnected` — direct address configured but hub not in registry.
91
+ * - `secret-mismatch`— `$hub.registerNode` was rejected with a cluster-secret error.
92
+ *
93
+ * `registering` (transport up but registerNode not yet acked) is not
94
+ * currently deterministic from the broker registry alone — it would require
95
+ * a tighter integration with the retry loop. Omitted until a clean signal
96
+ * exists; the brief gap is invisible at 3 s poll rate.
97
+ */
98
+ type HubConnectionState = 'connected' | 'searching' | 'disconnected' | 'secret-mismatch';
83
99
  interface AgentHttpConfig {
84
100
  readonly port: number;
85
101
  readonly nodeId: string;
@@ -87,6 +103,14 @@ interface AgentHttpConfig {
87
103
  readonly configPath: string;
88
104
  /** Called when the UI requests a reconnect (hub/secret changed). */
89
105
  readonly onReconnect?: () => Promise<void>;
106
+ /**
107
+ * Optional getter for the current hub connection state. When provided,
108
+ * the status endpoint uses this to surface a richer discriminated state
109
+ * instead of the legacy boolean `hubConnected`. The bootstrap wires this
110
+ * to expose `secret-mismatch` which is not derivable from the broker
111
+ * registry alone.
112
+ */
113
+ readonly getHubConnectionState?: () => HubConnectionState;
90
114
  }
91
115
  declare function createAgentHttpServer(getBroker: () => ServiceBroker, config: AgentHttpConfig): Promise<FastifyInstance>;
92
116
  declare function startAgentHttpServer(getBroker: () => ServiceBroker, config: AgentHttpConfig): Promise<void>;
package/dist/index.js CHANGED
@@ -176,6 +176,29 @@ async function applyDeployedBundle(input) {
176
176
  return { addonDir: liveDir };
177
177
  }
178
178
 
179
+ // src/fetch-bundle-from-hub.ts
180
+ var import_node_crypto2 = require("crypto");
181
+ var import_undici = require("undici");
182
+ async function fetchBundleFromHub(opts) {
183
+ const authHeader = { Authorization: `Bearer ${opts.token}` };
184
+ const res = opts.fetchImpl ? await opts.fetchImpl(opts.url, { headers: authHeader }) : await (0, import_undici.fetch)(opts.url, {
185
+ headers: authHeader,
186
+ dispatcher: new import_undici.Agent({ connect: { rejectUnauthorized: false } })
187
+ });
188
+ if (!res.ok) {
189
+ throw new Error(`bundle fetch failed: HTTP ${res.status}`);
190
+ }
191
+ const buffer = Buffer.from(await res.arrayBuffer());
192
+ if (buffer.length !== opts.bytes) {
193
+ throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
194
+ }
195
+ const sha = (0, import_node_crypto2.createHash)("sha256").update(buffer).digest("hex");
196
+ if (sha !== opts.sha256) {
197
+ throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
198
+ }
199
+ return buffer;
200
+ }
201
+
179
202
  // src/agent-service.ts
180
203
  var deploySwapLogger = {
181
204
  info: (msg) => console.log(`[Agent] ${msg}`),
@@ -209,6 +232,28 @@ async function withTimeout(p, ms, fallback) {
209
232
  }
210
233
  }
211
234
  var METRICS_SNAPSHOT_TIMEOUT_MS = 2e3;
235
+ function resolveDeployAction(seam) {
236
+ return async (params) => {
237
+ const { addonId, source, bundle } = params;
238
+ if (source && (0, import_system.isAddonDeploySource)(source)) {
239
+ if (source.kind === "npm") {
240
+ await seam.installFromNpm(addonId, source.version);
241
+ return { success: true, addonId };
242
+ }
243
+ const buf2 = await seam.fetchBundle(source);
244
+ const { addonDir: addonDir2 } = await seam.applyBundle(buf2);
245
+ seam.onApplied(addonDir2);
246
+ return { success: true, addonId, path: addonDir2 };
247
+ }
248
+ if (bundle === void 0) {
249
+ throw new Error("$agent.deploy: no source descriptor and no legacy bundle provided");
250
+ }
251
+ const buf = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
252
+ const { addonDir } = await seam.applyBundle(buf);
253
+ seam.onApplied(addonDir);
254
+ return { success: true, addonId, path: addonDir };
255
+ };
256
+ }
212
257
  function readHubAddressFromConfig(configPath) {
213
258
  if (!configPath) return null;
214
259
  try {
@@ -381,8 +426,7 @@ function createAgentService(deps) {
381
426
  deploy: {
382
427
  handler: async (ctx) => {
383
428
  const { params } = ctx;
384
- const { addonId, bundle } = params;
385
- const bufferData = typeof bundle === "string" ? Buffer.from(bundle, "base64") : bundle;
429
+ const { addonId, source, bundle } = params;
386
430
  const { execFile } = await import("child_process");
387
431
  const { promisify } = await import("util");
388
432
  const execFileAsync = promisify(execFile);
@@ -400,17 +444,37 @@ function createAgentService(deps) {
400
444
  }
401
445
  }
402
446
  };
403
- const { addonDir } = await applyDeployedBundle({
404
- addonsDir: deps.addonsDir,
405
- addonId,
406
- bundle: bufferData,
407
- extract,
408
- logger: deploySwapLogger
409
- });
410
- for (const declId of readDeployedAddonIds(addonDir)) {
411
- deps.loadedAddons.delete(declId);
412
- }
413
- return { success: true, addonId, path: addonDir };
447
+ const seam = {
448
+ installFromNpm: (pkg, v) => {
449
+ if (!deps.installFromNpm) {
450
+ throw new Error("npm deploy source unsupported on this agent");
451
+ }
452
+ return deps.installFromNpm(pkg, v);
453
+ },
454
+ applyBundle: (buf) => applyDeployedBundle({
455
+ addonsDir: deps.addonsDir,
456
+ addonId,
457
+ bundle: buf,
458
+ extract,
459
+ logger: deploySwapLogger
460
+ }),
461
+ fetchBundle: (s) => fetchBundleFromHub(s),
462
+ // Evict every addon DECLARATION id this package contributes
463
+ // from `loadedAddons` so the follow-up `$agent.reload` actually
464
+ // re-instantiates it. `loadDeployedAddons` skips any addon
465
+ // still present in `loadedAddons`; without this eviction a
466
+ // redeploy would leave the agent pinned to the pre-update
467
+ // version. The `deploy` param `addonId` is the PACKAGE name
468
+ // (used only as the on-disk dir), whereas `loadedAddons` is
469
+ // keyed by the addon DECLARATION id — they differ for scoped
470
+ // packages, so we read the extracted manifest to bridge them.
471
+ onApplied: (addonDir) => {
472
+ for (const declId of readDeployedAddonIds(addonDir)) {
473
+ deps.loadedAddons.delete(declId);
474
+ }
475
+ }
476
+ };
477
+ return resolveDeployAction(seam)({ addonId, source, bundle });
414
478
  }
415
479
  },
416
480
  /**
@@ -554,6 +618,12 @@ function getRegistryNodes(broker) {
554
618
  return [];
555
619
  }
556
620
  }
621
+ function deriveHubConnectionState(nodes, discoveryMode) {
622
+ const hubInRegistry = nodes.some((n) => n.id === "hub");
623
+ if (hubInRegistry) return "connected";
624
+ if (discoveryMode) return "searching";
625
+ return "disconnected";
626
+ }
557
627
  function getEffectiveConfig(configPath, nodeId) {
558
628
  const raw = readConfigFile(configPath);
559
629
  return {
@@ -585,6 +655,7 @@ async function createAgentHttpServer(getBroker, config) {
585
655
  const nodes = getRegistryNodes(broker);
586
656
  const hubConnected = nodes.some((n) => n.id === "hub");
587
657
  const discoveryMode = !eff.hubAddress;
658
+ const hubConnectionState = config.getHubConnectionState ? config.getHubConnectionState() : deriveHubConnectionState(nodes, discoveryMode);
588
659
  try {
589
660
  const status = await broker.call("$agent.status");
590
661
  return {
@@ -592,6 +663,7 @@ async function createAgentHttpServer(getBroker, config) {
592
663
  name: eff.name,
593
664
  hubAddress: eff.hubAddress,
594
665
  hubConnected,
666
+ hubConnectionState,
595
667
  discoveryMode,
596
668
  hasSecret: eff.hasSecret,
597
669
  discoveredNodes: nodes.filter((n) => n.id !== broker.nodeID).map((n) => n.id)
@@ -602,6 +674,7 @@ async function createAgentHttpServer(getBroker, config) {
602
674
  name: eff.name,
603
675
  hubAddress: eff.hubAddress,
604
676
  hubConnected,
677
+ hubConnectionState,
605
678
  discoveryMode,
606
679
  hasSecret: eff.hasSecret,
607
680
  discoveredNodes: [],
@@ -814,7 +887,7 @@ function narrowParams(raw) {
814
887
  deviceId: typeof deviceId === "number" ? deviceId : void 0
815
888
  };
816
889
  }
817
- function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
890
+ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, inProcessLookup, logger) {
818
891
  return {
819
892
  name: import_system2.AGENT_CAP_FWD_SERVICE,
820
893
  actions: {
@@ -824,11 +897,15 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
824
897
  const { capName, method, args, deviceId } = params;
825
898
  const childId = params.childId !== void 0 ? params.childId : agentUdsRegistry.resolveChildId(capName, deviceId);
826
899
  if (childId == null) {
900
+ const ref = inProcessLookup?.(capName) ?? null;
901
+ if (ref !== null) {
902
+ return ref.invoke(method, args);
903
+ }
827
904
  logger?.info(
828
- `agent ${agentNodeId}: no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
905
+ `agent ${agentNodeId}: no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
829
906
  );
830
907
  throw new Error(
831
- `agent ${agentNodeId} has no UDS child for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
908
+ `agent ${agentNodeId} has no provider for cap "${capName}"${deviceId !== void 0 ? ` (deviceId ${deviceId})` : ""}`
832
909
  );
833
910
  }
834
911
  const input = {
@@ -845,9 +922,11 @@ function createAgentCapDispatchService(agentNodeId, agentUdsRegistry, logger) {
845
922
  }
846
923
 
847
924
  // src/register-agent-cap-dispatch.ts
848
- function registerAgentCapDispatch(registrar, agentUdsRegistry, logger) {
925
+ function registerAgentCapDispatch(registrar, agentUdsRegistry, inProcessLookup, logger) {
849
926
  if (agentUdsRegistry === void 0) return;
850
- registrar.createService(createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, logger));
927
+ registrar.createService(
928
+ createAgentCapDispatchService(registrar.nodeID, agentUdsRegistry, inProcessLookup, logger)
929
+ );
851
930
  }
852
931
 
853
932
  // src/agent-bootstrap.ts
@@ -913,6 +992,27 @@ async function startAgent(configPath) {
913
992
  };
914
993
  const loadedAddons = /* @__PURE__ */ new Map();
915
994
  const subtree = new import_system3.HubNodeRegistry();
995
+ let hubConnectionStateOverride = null;
996
+ function getHubConnectionState() {
997
+ if (hubConnectionStateOverride !== null) return hubConnectionStateOverride;
998
+ try {
999
+ const registry = broker.registry;
1000
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
1001
+ if (nodes.some((n) => n.id === "hub")) return "connected";
1002
+ } catch {
1003
+ }
1004
+ const configNow = readConfigFile2(config.configPath);
1005
+ const discoveryMode = typeof configNow.hubAddress !== "string" || configNow.hubAddress.length === 0;
1006
+ return discoveryMode ? "searching" : "disconnected";
1007
+ }
1008
+ function readConfigFile2(p) {
1009
+ if (!fs6.existsSync(p)) return {};
1010
+ try {
1011
+ return JSON.parse(fs6.readFileSync(p, "utf-8"));
1012
+ } catch {
1013
+ return {};
1014
+ }
1015
+ }
916
1016
  const registerAbortController = new AbortController();
917
1017
  function buildAgentOwnManifest() {
918
1018
  const addonCapMap = /* @__PURE__ */ new Map();
@@ -975,6 +1075,7 @@ async function startAgent(configPath) {
975
1075
  consoleLogger.error(
976
1076
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
977
1077
  );
1078
+ hubConnectionStateOverride = "secret-mismatch";
978
1079
  return;
979
1080
  }
980
1081
  });
@@ -1045,7 +1146,10 @@ async function startAgent(configPath) {
1045
1146
  subtree.registerNode(params);
1046
1147
  triggerUpwardRegistration();
1047
1148
  },
1048
- expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
1149
+ expectedClusterSecretHash: config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
1150
+ installFromNpm: async (pkg, version) => {
1151
+ await installer.install(pkg, version);
1152
+ }
1049
1153
  });
1050
1154
  broker.createService(agentServiceSchema);
1051
1155
  const agentTcpPort = (0, import_system3.deriveAgentListenPort)(broker.nodeID);
@@ -1104,7 +1208,21 @@ async function startAgent(configPath) {
1104
1208
  agentParentUdsPath
1105
1209
  );
1106
1210
  broker.createService(processServiceSchema);
1107
- registerAgentCapDispatch(broker, agentUdsRegistry, consoleLogger);
1211
+ const agentInProcessLookup = (capName) => {
1212
+ const provider = capabilityRegistry.getSingleton(capName);
1213
+ if (provider === null || provider === void 0) return null;
1214
+ return {
1215
+ invoke: (method, args) => {
1216
+ const fn = provider[method];
1217
+ if (typeof fn !== "function") {
1218
+ return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`));
1219
+ }
1220
+ const result = fn.call(provider, args);
1221
+ return Promise.resolve(result);
1222
+ }
1223
+ };
1224
+ };
1225
+ registerAgentCapDispatch(broker, agentUdsRegistry, agentInProcessLookup, consoleLogger);
1108
1226
  (0, import_system3.registerEventBusService)(broker);
1109
1227
  broker.createService((0, import_system3.createHwAccelService)((0, import_system3.createKernelHwAccel)()));
1110
1228
  await broker.start();
@@ -1131,7 +1249,8 @@ async function startAgent(configPath) {
1131
1249
  nodeId: config.nodeId,
1132
1250
  dataDir: config.dataDir,
1133
1251
  configPath: config.configPath,
1134
- onReconnect: reconnect
1252
+ onReconnect: reconnect,
1253
+ getHubConnectionState
1135
1254
  });
1136
1255
  await bootCoreAddons(broker, config, capabilityRegistry, loadedAddons, loggerFactory);
1137
1256
  for (const dest of capabilityRegistry.getCollection("log-destination")) {
@@ -1153,6 +1272,9 @@ async function startAgent(configPath) {
1153
1272
  broker.localBus.on("$node.connected", (data) => {
1154
1273
  const node = data.node;
1155
1274
  if (node?.id !== "hub") return;
1275
+ if (hubConnectionStateOverride === "secret-mismatch") {
1276
+ hubConnectionStateOverride = null;
1277
+ }
1156
1278
  triggerUpwardRegistration();
1157
1279
  if (hubReachableFired) return;
1158
1280
  hubReachableFired = true;