@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/{chunk-GM2IV7NO.mjs → chunk-PFQZTXGW.mjs} +149 -23
- package/dist/chunk-PFQZTXGW.mjs.map +1 -0
- package/dist/cli.js +143 -21
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.js +143 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -2
- package/dist/chunk-GM2IV7NO.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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;
|