@camstack/server 1.2.588 → 1.2.590
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/agent/agent-update-service.js +3 -0
- package/dist/agent/main.js +50 -30
- package/dist/api/core/cap-providers.js +6 -1
- package/dist/api/health/health.routes.js +14 -0
- package/dist/api/trpc/generated-cap-routers.js +26 -0
- package/dist/core/addon/addon-process-view.js +28 -0
- package/dist/core/addon/addon-registry.service.js +52 -45
- package/dist/core/runner-health/runner-boot-check.js +175 -0
- package/dist/core/runner-health/update-preflight.js +62 -0
- package/dist/core/server-update/server-update.service.js +6 -0
- package/dist/main.js +6 -0
- package/dist/manual-boot.js +48 -0
- package/dist/server-root/index.js +48 -1
- package/package.json +8 -8
|
@@ -258,6 +258,9 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
258
258
|
applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
|
|
259
259
|
env: options.env,
|
|
260
260
|
now: options.now,
|
|
261
|
+
...(options.runnerBootCheck !== undefined
|
|
262
|
+
? { runnerBootCheck: options.runnerBootCheck }
|
|
263
|
+
: {}),
|
|
261
264
|
});
|
|
262
265
|
this.nodeId = nodeId;
|
|
263
266
|
this.restartAnnouncer = restartAnnouncer;
|
package/dist/agent/main.js
CHANGED
|
@@ -35,10 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.startAgent = startAgent;
|
|
37
37
|
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const runner_boot_check_js_1 = require("../core/runner-health/runner-boot-check.js");
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
39
40
|
const system_1 = require("@camstack/system");
|
|
40
|
-
// Capability definitions — must be declared before addon registerProvider() calls
|
|
41
41
|
const types_1 = require("@camstack/types");
|
|
42
|
+
// Capability definitions — must be declared before addon registerProvider() calls
|
|
43
|
+
const types_2 = require("@camstack/types");
|
|
42
44
|
const server_management_provider_js_1 = require("../api/core/server-management.provider.js");
|
|
43
45
|
const package_dir_utils_js_1 = require("../core/addon/package-dir-utils.js");
|
|
44
46
|
const single_copy_cleanup_runner_js_1 = require("../single-copy-cleanup-runner.js");
|
|
@@ -83,7 +85,7 @@ const agentLogManager = new system_2.LogManager(5000);
|
|
|
83
85
|
* the hub singleton (which reads `nodeId` as data) instead of being pinned to
|
|
84
86
|
* a node with no provider (dead broker fallback → ServiceNotFoundError).
|
|
85
87
|
*/
|
|
86
|
-
const DATA_NODEID_CAPS = new Set(
|
|
88
|
+
const DATA_NODEID_CAPS = new Set(types_2.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.nodeIdMode === 'data').map((c) => c.name));
|
|
87
89
|
// ---------------------------------------------------------------------------
|
|
88
90
|
// Main
|
|
89
91
|
// ---------------------------------------------------------------------------
|
|
@@ -417,33 +419,33 @@ async function startAgent(configPath) {
|
|
|
417
419
|
const capabilityRegistry = new system_1.CapabilityRegistry(consoleLogger);
|
|
418
420
|
// Declare all capabilities the agent uses — required before registerProvider()
|
|
419
421
|
const agentCapabilities = [
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
types_2.storageCapability,
|
|
423
|
+
types_2.storageProviderCapability,
|
|
422
424
|
// The path browser for "add a storage location on this node" — the agent
|
|
423
425
|
// registers the provider since day one, but without DECLARING the cap
|
|
424
426
|
// here the hub's manifest never learns of it and node-routed browse
|
|
425
427
|
// answers "not available on node X" (2026-07-30 fix).
|
|
426
|
-
|
|
428
|
+
types_2.filesystemBrowseCapability,
|
|
427
429
|
// The agent runs the same two-addon data topology as the hub (D44):
|
|
428
430
|
// `settings-store` is the door served by storage-orchestrator,
|
|
429
431
|
// `data-store-provider` is the engine sqlite-settings registers. BOTH
|
|
430
432
|
// must be declared here — this array is hand-curated and nothing else
|
|
431
433
|
// teaches an agent a capability, so omitting the engine leaves the
|
|
432
434
|
// agent's door with nothing behind it.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
435
|
+
types_2.settingsStoreCapability,
|
|
436
|
+
types_2.dataStoreProviderCapability,
|
|
437
|
+
types_2.logDestinationCapability,
|
|
438
|
+
types_2.metricsProviderCapability,
|
|
439
|
+
types_2.decoderCapability,
|
|
440
|
+
types_2.motionDetectionCapability,
|
|
441
|
+
types_2.pipelineExecutorCapability,
|
|
442
|
+
types_2.pipelineRunnerCapability,
|
|
443
|
+
types_2.audioAnalyzerCapability,
|
|
444
|
+
types_2.platformProbeCapability,
|
|
445
|
+
types_2.serverManagementCapability,
|
|
444
446
|
// Certificates of THIS node, called by the hub (D856): mesh leaf renewal and the
|
|
445
447
|
// hub-issued HTTPS leaf. Provided by the runtime, like `server-management`.
|
|
446
|
-
|
|
448
|
+
types_2.nodeCertificatesCapability,
|
|
447
449
|
];
|
|
448
450
|
for (const cap of agentCapabilities) {
|
|
449
451
|
capabilityRegistry.declareCapability(cap);
|
|
@@ -455,9 +457,14 @@ async function startAgent(configPath) {
|
|
|
455
457
|
// `agent-runtime` addonId (no addon owns it); `buildAgentOwnManifest`
|
|
456
458
|
// appends the matching manifest entry so the hub can route
|
|
457
459
|
// nodeId-pinned `server-management` calls here.
|
|
460
|
+
// D877: the agent's runner boot check. Only the hub plans which addons an
|
|
461
|
+
// agent runs, so the agent compares what its process service holds (a runner
|
|
462
|
+
// that failed or refused is named; one that never spawned is not knowable here).
|
|
463
|
+
let agentRunnerBootCheck = null;
|
|
458
464
|
const agentUpdateService = new agent_update_service_js_1.AgentUpdateService({
|
|
459
465
|
logger: consoleLogger,
|
|
460
466
|
dataDir: config.dataDir,
|
|
467
|
+
runnerBootCheck: () => agentRunnerBootCheck?.current(),
|
|
461
468
|
});
|
|
462
469
|
capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
|
|
463
470
|
// The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
|
|
@@ -781,6 +788,19 @@ async function startAgent(configPath) {
|
|
|
781
788
|
// discovery resolve (or time out cleanly via the read-blob fallback)
|
|
782
789
|
// so agent boot completes instead of hanging on the first infra addon.
|
|
783
790
|
await broker.start();
|
|
791
|
+
agentRunnerBootCheck = new runner_boot_check_js_1.RunnerBootCheckService({
|
|
792
|
+
logger: consoleLogger,
|
|
793
|
+
nodeId: config.nodeId,
|
|
794
|
+
planned: () => null,
|
|
795
|
+
listProcesses: async () => {
|
|
796
|
+
// Untyped Moleculer boundary; the process service owns the row shape.
|
|
797
|
+
const rows = await broker.call('$process.list', {});
|
|
798
|
+
return rows;
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
agentRunnerBootCheck.scheduleAfterBoot();
|
|
802
|
+
// Re-evaluate a verdict that still had runners on their way, on each runner state change.
|
|
803
|
+
(0, system_1.getBrokerEventBus)(broker).subscribe({ category: types_1.EventCategory.ProcessStateChanged }, () => agentRunnerBootCheck?.noteRunnerChange());
|
|
784
804
|
// C2: wire the UDS ↔ Moleculer event bridge so events emitted by UDS
|
|
785
805
|
// children fan to siblings and reach the cluster, and cluster / parent-
|
|
786
806
|
// local events propagate to every UDS child. Inert when agentUdsRegistry
|
|
@@ -900,7 +920,7 @@ async function startAgent(configPath) {
|
|
|
900
920
|
(0, agent_pairing_store_js_1.forgetPairing)(config.configPath, config.dataDir);
|
|
901
921
|
pairing.noteUnpaired();
|
|
902
922
|
hubConnectionStateOverride = null;
|
|
903
|
-
await reconnect().catch((err) => consoleLogger.error(`reconnect after unpairing failed: ${(0,
|
|
923
|
+
await reconnect().catch((err) => consoleLogger.error(`reconnect after unpairing failed: ${(0, types_2.errMsg)(err)}`));
|
|
904
924
|
advertise();
|
|
905
925
|
};
|
|
906
926
|
advertise();
|
|
@@ -1041,7 +1061,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
|
|
|
1041
1061
|
await loader.loadFromAddonDir(dir);
|
|
1042
1062
|
}
|
|
1043
1063
|
catch (err) {
|
|
1044
|
-
console.warn(`[Agent] Failed to scan ${dir}: ${(0,
|
|
1064
|
+
console.warn(`[Agent] Failed to scan ${dir}: ${(0, types_2.errMsg)(err)}`);
|
|
1045
1065
|
}
|
|
1046
1066
|
}
|
|
1047
1067
|
// Resolution is per-CAPABILITY (the guard names every missing required
|
|
@@ -1094,7 +1114,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
|
|
|
1094
1114
|
// configured for type addons-data" → fatal crash-loop.
|
|
1095
1115
|
listStorageLocationDeclarations: () => loader.listStorageLocationDeclarations(),
|
|
1096
1116
|
});
|
|
1097
|
-
const initResult = (0,
|
|
1117
|
+
const initResult = (0, types_2.normalizeAddonInitResult)(await instance.initialize(context));
|
|
1098
1118
|
for (const reg of initResult?.providers ?? []) {
|
|
1099
1119
|
const capName = reg.capability.name;
|
|
1100
1120
|
registry.registerProvider(capName, addonId, reg.provider);
|
|
@@ -1112,7 +1132,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
|
|
|
1112
1132
|
console.log(`[Agent] Core addon "${addonId}" initialized`);
|
|
1113
1133
|
}
|
|
1114
1134
|
catch (err) {
|
|
1115
|
-
const msg = (0,
|
|
1135
|
+
const msg = (0, types_2.errMsg)(err);
|
|
1116
1136
|
console.error(`[Agent] Failed to initialize core addon "${addonId}": ${msg}`);
|
|
1117
1137
|
if (step.required) {
|
|
1118
1138
|
throw new Error(`Required infrastructure addon "${addonId}" failed: ${msg}`, { cause: err });
|
|
@@ -1155,7 +1175,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
|
|
|
1155
1175
|
await loader.loadFromAddonDir(dir);
|
|
1156
1176
|
}
|
|
1157
1177
|
catch (err) {
|
|
1158
|
-
console.warn(`[Agent] Skipping ${dir}: ${(0,
|
|
1178
|
+
console.warn(`[Agent] Skipping ${dir}: ${(0, types_2.errMsg)(err)}`);
|
|
1159
1179
|
continue;
|
|
1160
1180
|
}
|
|
1161
1181
|
dirToLoader.set(dir, loader);
|
|
@@ -1164,11 +1184,11 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
|
|
|
1164
1184
|
continue;
|
|
1165
1185
|
if (registered.declaration.execution === undefined)
|
|
1166
1186
|
continue;
|
|
1167
|
-
const placement = (0,
|
|
1187
|
+
const placement = (0, types_2.resolveAddonPlacement)(registered.declaration);
|
|
1168
1188
|
if (placement === 'hub-only')
|
|
1169
1189
|
continue;
|
|
1170
1190
|
allGroupCandidates.push({
|
|
1171
|
-
groupId: (0,
|
|
1191
|
+
groupId: (0, types_2.resolveRunnerId)(registered.declaration, registered.declaration.id),
|
|
1172
1192
|
addonId: registered.declaration.id,
|
|
1173
1193
|
addonDir: dir,
|
|
1174
1194
|
version: registered.declaration.version ?? '0.0.0',
|
|
@@ -1216,7 +1236,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
|
|
|
1216
1236
|
const addonId = registered.declaration.id;
|
|
1217
1237
|
if (loadedAddons.has(addonId))
|
|
1218
1238
|
continue;
|
|
1219
|
-
if (!(0,
|
|
1239
|
+
if (!(0, types_2.isDeployableToAgent)(registered.declaration))
|
|
1220
1240
|
continue;
|
|
1221
1241
|
console.warn(`[Agent] Addon "${addonId}" is deployable but missing from any spawned group — verify package.json execution field`);
|
|
1222
1242
|
}
|
|
@@ -1250,11 +1270,11 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
|
|
|
1250
1270
|
const addonId = decl.id;
|
|
1251
1271
|
if (loadedAddons.has(addonId))
|
|
1252
1272
|
continue;
|
|
1253
|
-
if (!(0,
|
|
1273
|
+
if (!(0, types_2.isDeployableToAgent)(decl))
|
|
1254
1274
|
continue;
|
|
1255
1275
|
if ((0, agent_group_runner_js_1.isGroupRunnerAddon)(decl)) {
|
|
1256
1276
|
groupCandidates.push({
|
|
1257
|
-
groupId: (0,
|
|
1277
|
+
groupId: (0, types_2.resolveRunnerId)(decl, addonId),
|
|
1258
1278
|
addonId,
|
|
1259
1279
|
addonDir: dir,
|
|
1260
1280
|
version: decl.version ?? '0.0.0',
|
|
@@ -1316,14 +1336,14 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
|
|
|
1316
1336
|
await loader.loadFromAddonDir(dir);
|
|
1317
1337
|
}
|
|
1318
1338
|
catch (err) {
|
|
1319
|
-
console.warn(`[Agent] Skipping ${dir}: ${(0,
|
|
1339
|
+
console.warn(`[Agent] Skipping ${dir}: ${(0, types_2.errMsg)(err)}`);
|
|
1320
1340
|
continue;
|
|
1321
1341
|
}
|
|
1322
1342
|
for (const registered of loader.listAddons()) {
|
|
1323
1343
|
const addonId = registered.declaration.id;
|
|
1324
1344
|
if (loadedAddons.has(addonId))
|
|
1325
1345
|
continue;
|
|
1326
|
-
if (!(0,
|
|
1346
|
+
if (!(0, types_2.isDeployableToAgent)(registered.declaration))
|
|
1327
1347
|
continue;
|
|
1328
1348
|
// Group-runner addons are dispatched to subprocesses by the caller.
|
|
1329
1349
|
if ((0, agent_group_runner_js_1.isGroupRunnerAddon)(registered.declaration))
|
|
@@ -1345,7 +1365,7 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
|
|
|
1345
1365
|
console.log(`[Agent] Deployed addon "${addonId}" loaded in-process`);
|
|
1346
1366
|
}
|
|
1347
1367
|
catch (err) {
|
|
1348
|
-
const msg = (0,
|
|
1368
|
+
const msg = (0, types_2.errMsg)(err);
|
|
1349
1369
|
console.error(`[Agent] Failed to load deployed addon "${addonId}": ${msg}`);
|
|
1350
1370
|
loadedAddons.set(addonId, { id: addonId, status: 'error' });
|
|
1351
1371
|
}
|
|
@@ -586,10 +586,11 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
|
|
|
586
586
|
// tab can render the same shape regardless of whether the target is
|
|
587
587
|
// hub or agent.
|
|
588
588
|
if (input.nodeId === 'hub') {
|
|
589
|
+
await addonRegistry?.refreshRunnerStates();
|
|
589
590
|
const rows = addonRegistry?.listAddons() ?? [];
|
|
590
591
|
return rows.map((r) => ({
|
|
591
592
|
id: r.manifest.id,
|
|
592
|
-
status: r.process?.state ?? '
|
|
593
|
+
status: r.process?.state ?? 'unknown',
|
|
593
594
|
version: r.manifest.packageVersion,
|
|
594
595
|
packageName: r.manifest.packageName,
|
|
595
596
|
}));
|
|
@@ -1477,6 +1478,8 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentReg
|
|
|
1477
1478
|
for (const h of ar.getAddonHealthSnapshot()) {
|
|
1478
1479
|
healthByPackage.set(h.packageName, h);
|
|
1479
1480
|
}
|
|
1481
|
+
// Real runner states (D877) - never the literal `running` the list used to carry.
|
|
1482
|
+
await ar.refreshRunnerStates();
|
|
1480
1483
|
const hubList = ar.listAllAddons().map((item) => ({
|
|
1481
1484
|
...item,
|
|
1482
1485
|
hasBackup: rollbackable.has(item.manifest.packageName),
|
|
@@ -1506,6 +1509,8 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentReg
|
|
|
1506
1509
|
},
|
|
1507
1510
|
hasBackup: false,
|
|
1508
1511
|
health: null,
|
|
1512
|
+
// The hub's runner state is not this agent's: report none rather than a borrowed one.
|
|
1513
|
+
process: undefined,
|
|
1509
1514
|
},
|
|
1510
1515
|
];
|
|
1511
1516
|
});
|
|
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
exports.buildHubHealth = buildHubHealth;
|
|
24
24
|
exports.isAuthorizedHealthRequest = isAuthorizedHealthRequest;
|
|
25
25
|
exports.registerHealthRoutes = registerHealthRoutes;
|
|
26
|
+
const runner_boot_check_js_1 = require("../../core/runner-health/runner-boot-check.js");
|
|
26
27
|
const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
27
28
|
const AGENT_HEALTH_TIMEOUT_MS = 3_000;
|
|
28
29
|
function nowIso() {
|
|
@@ -35,6 +36,18 @@ async function buildHubHealth(deps, proc = process) {
|
|
|
35
36
|
const offlineNodes = remote.filter((n) => !n.isOnline);
|
|
36
37
|
const total = remote.length;
|
|
37
38
|
const online = total - offlineNodes.length;
|
|
39
|
+
const check = deps.runnerBootCheck?.();
|
|
40
|
+
const runners = check === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: {
|
|
43
|
+
degraded: (0, runner_boot_check_js_1.isRunnerBootCheckDegraded)(check),
|
|
44
|
+
expected: check.expected,
|
|
45
|
+
running: check.running,
|
|
46
|
+
failed: check.failed.length,
|
|
47
|
+
refused: check.refused.length,
|
|
48
|
+
missing: check.missing.length,
|
|
49
|
+
checkedAtMs: check.checkedAtMs,
|
|
50
|
+
};
|
|
38
51
|
const memUsage = proc.memoryUsage();
|
|
39
52
|
const totalMem = memUsage.heapTotal + memUsage.external + memUsage.arrayBuffers;
|
|
40
53
|
const memoryPercent = totalMem > 0 ? Math.round((memUsage.heapUsed / totalMem) * 100) : 0;
|
|
@@ -55,6 +68,7 @@ async function buildHubHealth(deps, proc = process) {
|
|
|
55
68
|
cpuPercent: 0,
|
|
56
69
|
memoryPercent,
|
|
57
70
|
checkedAt: nowIso(),
|
|
71
|
+
...(runners !== undefined ? { runners } : {}),
|
|
58
72
|
};
|
|
59
73
|
}
|
|
60
74
|
async function fetchAgentHealth(deps, nodeId) {
|
|
@@ -2158,6 +2158,15 @@ function createCapRouter_coreBlocks(getProvider, createRemoteProxy) {
|
|
|
2158
2158
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2159
2159
|
return p.updateBlockSettings(methodInput);
|
|
2160
2160
|
}),
|
|
2161
|
+
migrateRawSecrets: trpc_middleware_js_1.adminProcedure
|
|
2162
|
+
.input(types_36.coreBlocksCapability.methods.migrateRawSecrets.input.loose())
|
|
2163
|
+
.output(types_36.coreBlocksCapability.methods.migrateRawSecrets.output)
|
|
2164
|
+
.mutation(async ({ input, ctx }) => {
|
|
2165
|
+
const { nodeId, ...methodInput } = input;
|
|
2166
|
+
const p = resolveProvider('core-blocks', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2168
|
+
return p.migrateRawSecrets(methodInput);
|
|
2169
|
+
}),
|
|
2161
2170
|
getTypeDefs: trpc_middleware_js_1.adminProcedure
|
|
2162
2171
|
.input(types_36.coreBlocksCapability.methods.getTypeDefs.input.loose())
|
|
2163
2172
|
.output(types_36.coreBlocksCapability.methods.getTypeDefs.output)
|
|
@@ -4995,6 +5004,14 @@ function createCapRouter_llm(getProvider, _createRemoteProxy) {
|
|
|
4995
5004
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4996
5005
|
return p.upsertProfile(input);
|
|
4997
5006
|
}),
|
|
5007
|
+
migrateRawSecrets: trpc_middleware_js_1.adminProcedure
|
|
5008
|
+
.input(types_67.llmCapability.methods.migrateRawSecrets.input.loose())
|
|
5009
|
+
.output(types_67.llmCapability.methods.migrateRawSecrets.output)
|
|
5010
|
+
.mutation(async ({ input, ctx }) => {
|
|
5011
|
+
const p = requireCapProvider('llm', () => getProvider(ctx));
|
|
5012
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5013
|
+
return p.migrateRawSecrets(input);
|
|
5014
|
+
}),
|
|
4998
5015
|
deleteProfile: trpc_middleware_js_1.adminProcedure
|
|
4999
5016
|
.input(types_67.llmCapability.methods.deleteProfile.input.loose())
|
|
5000
5017
|
.output(types_67.llmCapability.methods.deleteProfile.output)
|
|
@@ -11971,6 +11988,15 @@ function createCapRouter_userManagement(getProvider, createRemoteProxy) {
|
|
|
11971
11988
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
11972
11989
|
return p.getTotpStatus(methodInput);
|
|
11973
11990
|
}),
|
|
11991
|
+
migrateRawSecrets: trpc_middleware_js_1.adminProcedure
|
|
11992
|
+
.input(types_138.userManagementCapability.methods.migrateRawSecrets.input.loose())
|
|
11993
|
+
.output(types_138.userManagementCapability.methods.migrateRawSecrets.output)
|
|
11994
|
+
.mutation(async ({ input, ctx }) => {
|
|
11995
|
+
const { nodeId, ...methodInput } = input;
|
|
11996
|
+
const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
11997
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
11998
|
+
return p.migrateRawSecrets(methodInput);
|
|
11999
|
+
}),
|
|
11974
12000
|
verifyTotp: trpc_middleware_js_1.protectedProcedure
|
|
11975
12001
|
.input(types_138.userManagementCapability.methods.verifyTotp.input.loose())
|
|
11976
12002
|
.output(types_138.userManagementCapability.methods.verifyTotp.output)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NEVER_LOADED_PROCESS = void 0;
|
|
4
|
+
exports.describeAddonProcess = describeAddonProcess;
|
|
5
|
+
function describeAddonProcess(input) {
|
|
6
|
+
if (!input.forked) {
|
|
7
|
+
// Hosted by hub-main itself: its state is hub-main's, which is running if this code is.
|
|
8
|
+
return input.initialized
|
|
9
|
+
? { pid: input.hubPid, mode: 'in-process', state: 'running' }
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
if (input.runners === null)
|
|
13
|
+
return { mode: 'forked', state: 'unknown' };
|
|
14
|
+
const row = input.runners.get(input.runnerId);
|
|
15
|
+
if (row === undefined)
|
|
16
|
+
return { mode: 'forked', state: 'missing' };
|
|
17
|
+
return {
|
|
18
|
+
pid: row.pid,
|
|
19
|
+
mode: 'forked',
|
|
20
|
+
state: row.state,
|
|
21
|
+
...(row.refusal !== undefined ? { reason: row.refusal.reason } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A package on disk that never loaded: there is no process, and the card must
|
|
26
|
+
* show the diagnostic state distinctly (`scanFailedToLoadPackages`).
|
|
27
|
+
*/
|
|
28
|
+
exports.NEVER_LOADED_PROCESS = { mode: 'in-process', state: 'failed' };
|
|
@@ -39,6 +39,7 @@ exports.shouldEvictMissingOnDisk = shouldEvictMissingOnDisk;
|
|
|
39
39
|
exports.addonUpdatePayload = addonUpdatePayload;
|
|
40
40
|
exports.parseDataPlaneEndpoints = parseDataPlaneEndpoints;
|
|
41
41
|
exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
|
|
42
|
+
const addon_process_view_js_1 = require("./addon-process-view.js");
|
|
42
43
|
const node_crypto_1 = require("node:crypto");
|
|
43
44
|
const fs = __importStar(require("node:fs"));
|
|
44
45
|
const os = __importStar(require("node:os"));
|
|
@@ -2045,17 +2046,16 @@ class AddonRegistryService {
|
|
|
2045
2046
|
return this.isPackageProtected(entry.packageName);
|
|
2046
2047
|
}
|
|
2047
2048
|
listAddons() {
|
|
2048
|
-
//
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
}
|
|
2049
|
+
// The real process-service state of each addon's runner (D877) - the last
|
|
2050
|
+
// `refreshRunnerStates()` answer; `unknown` before the first one.
|
|
2051
|
+
const runnerStates = this.runnerStates;
|
|
2052
|
+
const processFor = (id, entry) => (0, addon_process_view_js_1.describeAddonProcess)({
|
|
2053
|
+
forked: this.runsInOwnRunner(entry),
|
|
2054
|
+
initialized: entry.initialized === true,
|
|
2055
|
+
runnerId: entry.declaration ? (0, types_1.resolveRunnerId)(entry.declaration, id) : id,
|
|
2056
|
+
runners: runnerStates,
|
|
2057
|
+
hubPid: process.pid,
|
|
2058
|
+
});
|
|
2059
2059
|
const live = Array.from(this.addonEntries.values())
|
|
2060
2060
|
.filter((entry) => entry.addon?.manifest?.id)
|
|
2061
2061
|
.map((entry) => {
|
|
@@ -2098,7 +2098,7 @@ class AddonRegistryService {
|
|
|
2098
2098
|
declaration: entry.declaration,
|
|
2099
2099
|
source: entry.source,
|
|
2100
2100
|
installSource,
|
|
2101
|
-
process:
|
|
2101
|
+
process: processFor(entry.addon.manifest.id, entry),
|
|
2102
2102
|
};
|
|
2103
2103
|
});
|
|
2104
2104
|
// Surface packages that exist on disk but failed to load — typical
|
|
@@ -2194,7 +2194,7 @@ class AddonRegistryService {
|
|
|
2194
2194
|
declaration: decl,
|
|
2195
2195
|
source: 'installed',
|
|
2196
2196
|
...(installSource ? { installSource } : {}),
|
|
2197
|
-
process:
|
|
2197
|
+
process: addon_process_view_js_1.NEVER_LOADED_PROCESS,
|
|
2198
2198
|
});
|
|
2199
2199
|
}
|
|
2200
2200
|
};
|
|
@@ -2228,38 +2228,6 @@ class AddonRegistryService {
|
|
|
2228
2228
|
listAllAddons() {
|
|
2229
2229
|
return this.listAddons();
|
|
2230
2230
|
}
|
|
2231
|
-
/**
|
|
2232
|
-
* List all addon processes (in-process addons)
|
|
2233
|
-
* as ManagedProcessStatus-compatible entries for the process tree.
|
|
2234
|
-
*/
|
|
2235
|
-
async listAddonProcesses() {
|
|
2236
|
-
const result = [];
|
|
2237
|
-
// Collect hub PID and fetch stats
|
|
2238
|
-
const { getPidStats } = await Promise.resolve().then(() => __importStar(require('@camstack/system')));
|
|
2239
|
-
const pidStats = await getPidStats([process.pid]);
|
|
2240
|
-
// In-process addons
|
|
2241
|
-
const hubStats = pidStats.get(process.pid);
|
|
2242
|
-
for (const [id, entry] of this.addonEntries) {
|
|
2243
|
-
if (entry.initialized) {
|
|
2244
|
-
result.push({
|
|
2245
|
-
id: `addon:${id}`,
|
|
2246
|
-
label: `Addon: ${id} (in-process)`,
|
|
2247
|
-
state: 'running',
|
|
2248
|
-
pid: process.pid,
|
|
2249
|
-
stats: {
|
|
2250
|
-
pid: process.pid,
|
|
2251
|
-
cpu: hubStats?.cpu ?? 0,
|
|
2252
|
-
memory: hubStats?.memory ?? 0,
|
|
2253
|
-
uptime: Math.round(process.uptime()),
|
|
2254
|
-
restartCount: 0,
|
|
2255
|
-
},
|
|
2256
|
-
restartCount: 0,
|
|
2257
|
-
mode: 'in-process',
|
|
2258
|
-
});
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
return result;
|
|
2262
|
-
}
|
|
2263
2231
|
/**
|
|
2264
2232
|
* Phase 11 (settings redesign): `getAddonConfigSchema`,
|
|
2265
2233
|
* `getAddonConfig`, and `updateAddonConfig` — the legacy adapter
|
|
@@ -3243,6 +3211,45 @@ class AddonRegistryService {
|
|
|
3243
3211
|
meta: { runnerId, addonCount: addons.length, addonIds: addons.map((a) => a.addonId) },
|
|
3244
3212
|
});
|
|
3245
3213
|
}
|
|
3214
|
+
/**
|
|
3215
|
+
* The last answer of `$process.list`, keyed by runner id (D877); `null` before
|
|
3216
|
+
* the first one or when the process service could not answer. Read by the
|
|
3217
|
+
* synchronous `listAddons()`; refreshed by `refreshRunnerStates()`.
|
|
3218
|
+
*/
|
|
3219
|
+
runnerStates = null;
|
|
3220
|
+
/** Every runner row the process service holds right now, or `null` when it cannot answer. */
|
|
3221
|
+
async listRunnerRows() {
|
|
3222
|
+
try {
|
|
3223
|
+
// Untyped Moleculer boundary: the process-service owns `ProcessInfo`.
|
|
3224
|
+
const procs = (await this.broker.call('$process.list'));
|
|
3225
|
+
return procs.map((p) => ({
|
|
3226
|
+
name: p.name,
|
|
3227
|
+
pid: p.pid,
|
|
3228
|
+
addonIds: p.addonIds,
|
|
3229
|
+
state: p.state,
|
|
3230
|
+
...(p.refusal !== undefined ? { refusal: { reason: p.refusal.reason } } : {}),
|
|
3231
|
+
}));
|
|
3232
|
+
}
|
|
3233
|
+
catch (err) {
|
|
3234
|
+
this.logger.warn('could not list the runner processes - addon process state is unknown', {
|
|
3235
|
+
meta: { error: (0, types_1.errMsg)(err) },
|
|
3236
|
+
});
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
/** Re-read the process service so the next `listAddons()` reports real runner states. */
|
|
3241
|
+
async refreshRunnerStates() {
|
|
3242
|
+
const rows = await this.listRunnerRows();
|
|
3243
|
+
this.runnerStates = rows === null ? null : new Map(rows.map((r) => [r.name, r]));
|
|
3244
|
+
}
|
|
3245
|
+
/** The runner ids this node PLANS to run: `resolveRunnerId` over every forked addon. */
|
|
3246
|
+
plannedRunnerIds() {
|
|
3247
|
+
return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).keys()];
|
|
3248
|
+
}
|
|
3249
|
+
/** The forked addon ids (the ones whose Init the D870 guard gates). */
|
|
3250
|
+
forkedAddonIds() {
|
|
3251
|
+
return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).values()].flatMap((roster) => roster.map((r) => r.addonId));
|
|
3252
|
+
}
|
|
3246
3253
|
/**
|
|
3247
3254
|
* Snapshot of the live runner subprocesses (`$process.list`), reduced to the
|
|
3248
3255
|
* `{ runnerId, addonIds }` shape the convergence planner needs. Returns `[]`
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunnerBootCheckService = exports.RUNNER_BOOT_CHECK_DELAY_MS = void 0;
|
|
4
|
+
exports.evaluateRunnerBootCheck = evaluateRunnerBootCheck;
|
|
5
|
+
exports.isRunnerBootCheckSettled = isRunnerBootCheckSettled;
|
|
6
|
+
exports.isRunnerBootCheckDegraded = isRunnerBootCheckDegraded;
|
|
7
|
+
exports.describeRunnerBootCheck = describeRunnerBootCheck;
|
|
8
|
+
/**
|
|
9
|
+
* Pure verdict. A planned runner is `running`, `refused` (typed Init refusal),
|
|
10
|
+
* `missing` (the process service has no entry), or `failed` - which here means
|
|
11
|
+
* "has an entry and is none of the above": the D6 breaker's terminal state, a
|
|
12
|
+
* crash waiting on its backoff, a stop, or a start that never reached READY.
|
|
13
|
+
*/
|
|
14
|
+
function evaluateRunnerBootCheck(input) {
|
|
15
|
+
const byName = new Map(input.processes.map((p) => [p.name, p]));
|
|
16
|
+
const expectedIds = input.planned ?? input.processes.map((p) => p.name);
|
|
17
|
+
const failed = [];
|
|
18
|
+
const refused = [];
|
|
19
|
+
const missing = [];
|
|
20
|
+
const pending = [];
|
|
21
|
+
let running = 0;
|
|
22
|
+
for (const runnerId of expectedIds) {
|
|
23
|
+
const row = byName.get(runnerId);
|
|
24
|
+
if (row === undefined) {
|
|
25
|
+
missing.push(runnerId);
|
|
26
|
+
}
|
|
27
|
+
else if (row.state === 'running') {
|
|
28
|
+
running += 1;
|
|
29
|
+
}
|
|
30
|
+
else if (row.state === 'starting' || row.state === 'crashed') {
|
|
31
|
+
// On its way: a slow boot or a crash waiting on its backoff respawn is not
|
|
32
|
+
// a failure yet - the breaker (`failed`) is what says "gave up".
|
|
33
|
+
pending.push(runnerId);
|
|
34
|
+
}
|
|
35
|
+
else if (row.state === 'refused') {
|
|
36
|
+
refused.push({
|
|
37
|
+
runnerId,
|
|
38
|
+
addonIds: [...row.addonIds],
|
|
39
|
+
reason: row.refusal?.reason ?? 'refused to start (reason not reported)',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
failed.push(runnerId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
checkedAtMs: input.now,
|
|
48
|
+
expected: expectedIds.length,
|
|
49
|
+
running,
|
|
50
|
+
failed,
|
|
51
|
+
refused,
|
|
52
|
+
missing,
|
|
53
|
+
pending,
|
|
54
|
+
planKnown: input.planned !== null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** True when no runner is still on its way - only a settled verdict is pushed. */
|
|
58
|
+
function isRunnerBootCheckSettled(check) {
|
|
59
|
+
return (check.pending ?? []).length === 0;
|
|
60
|
+
}
|
|
61
|
+
/** True when the check found anything an operator has to act on. */
|
|
62
|
+
function isRunnerBootCheckDegraded(check) {
|
|
63
|
+
return (check !== undefined &&
|
|
64
|
+
(check.failed.length > 0 || check.refused.length > 0 || check.missing.length > 0));
|
|
65
|
+
}
|
|
66
|
+
/** One sentence naming the findings, for a log line or an alert. */
|
|
67
|
+
function describeRunnerBootCheck(check) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (check.failed.length > 0)
|
|
70
|
+
parts.push(`failed: ${check.failed.join(', ')}`);
|
|
71
|
+
if (check.refused.length > 0) {
|
|
72
|
+
parts.push(`refused: ${check.refused.map((r) => `${r.runnerId} (${r.reason})`).join('; ')}`);
|
|
73
|
+
}
|
|
74
|
+
if (check.missing.length > 0)
|
|
75
|
+
parts.push(`missing: ${check.missing.join(', ')}`);
|
|
76
|
+
if ((check.pending ?? []).length > 0)
|
|
77
|
+
parts.push(`still starting: ${(check.pending ?? []).join(', ')}`);
|
|
78
|
+
return `${check.running}/${check.expected} runners running${parts.length > 0 ? ` - ${parts.join(' - ')}` : ''}`;
|
|
79
|
+
}
|
|
80
|
+
exports.RUNNER_BOOT_CHECK_DELAY_MS = 60_000;
|
|
81
|
+
class RunnerBootCheckService {
|
|
82
|
+
options;
|
|
83
|
+
latest;
|
|
84
|
+
timer = null;
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.options = options;
|
|
87
|
+
}
|
|
88
|
+
/** The stored verdict; `undefined` until the check has run. */
|
|
89
|
+
current() {
|
|
90
|
+
return this.latest;
|
|
91
|
+
}
|
|
92
|
+
/** Arm the one-shot check. A second call while armed is a no-op. */
|
|
93
|
+
scheduleAfterBoot(delayMs = exports.RUNNER_BOOT_CHECK_DELAY_MS) {
|
|
94
|
+
if (this.timer !== null)
|
|
95
|
+
return;
|
|
96
|
+
this.timer = setTimeout(() => {
|
|
97
|
+
this.timer = null;
|
|
98
|
+
void this.run();
|
|
99
|
+
}, delayMs);
|
|
100
|
+
this.timer.unref();
|
|
101
|
+
}
|
|
102
|
+
reevaluating = null;
|
|
103
|
+
/**
|
|
104
|
+
* A runner settled on a state. While the stored verdict still has pending
|
|
105
|
+
* runners, evaluate again - once at a time, no timer: the process service
|
|
106
|
+
* tells us when something changed (D3).
|
|
107
|
+
*/
|
|
108
|
+
noteRunnerChange() {
|
|
109
|
+
if (this.latest === undefined || isRunnerBootCheckSettled(this.latest))
|
|
110
|
+
return;
|
|
111
|
+
if (this.reevaluating !== null) {
|
|
112
|
+
// A change landed while a re-evaluation was reading: read once more after it.
|
|
113
|
+
this.changedDuringRun = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.reevaluating = this.run().finally(() => {
|
|
117
|
+
this.reevaluating = null;
|
|
118
|
+
if (this.changedDuringRun) {
|
|
119
|
+
this.changedDuringRun = false;
|
|
120
|
+
this.noteRunnerChange();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
changedDuringRun = false;
|
|
125
|
+
cancel() {
|
|
126
|
+
if (this.timer !== null)
|
|
127
|
+
clearTimeout(this.timer);
|
|
128
|
+
this.timer = null;
|
|
129
|
+
}
|
|
130
|
+
/** Run the check now. A process service that cannot answer leaves the previous verdict. */
|
|
131
|
+
async run() {
|
|
132
|
+
const { logger, nodeId } = this.options;
|
|
133
|
+
let processes;
|
|
134
|
+
try {
|
|
135
|
+
processes = await this.options.listProcesses();
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
logger.warn('runner boot check could not list the runners - no verdict recorded', {
|
|
139
|
+
tags: { nodeId },
|
|
140
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
141
|
+
});
|
|
142
|
+
return this.latest;
|
|
143
|
+
}
|
|
144
|
+
const check = evaluateRunnerBootCheck({
|
|
145
|
+
planned: this.options.planned(),
|
|
146
|
+
processes,
|
|
147
|
+
now: (this.options.now ?? Date.now)(),
|
|
148
|
+
});
|
|
149
|
+
this.latest = check;
|
|
150
|
+
const summary = describeRunnerBootCheck(check);
|
|
151
|
+
if (!isRunnerBootCheckSettled(check)) {
|
|
152
|
+
// Not final: nothing is announced until every runner has settled; the next
|
|
153
|
+
// runner state change re-evaluates (`noteRunnerChange`).
|
|
154
|
+
logger.info(`runner boot check not settled: ${summary}`, { tags: { nodeId } });
|
|
155
|
+
return check;
|
|
156
|
+
}
|
|
157
|
+
if (isRunnerBootCheckDegraded(check)) {
|
|
158
|
+
logger.error(`runner boot check DEGRADED: ${summary}`, { tags: { nodeId } });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
logger.info(`runner boot check: ${summary}`, { tags: { nodeId } });
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
this.options.onResult?.(check);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
logger.warn('runner boot check result handler threw', {
|
|
168
|
+
tags: { nodeId },
|
|
169
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return check;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.RunnerBootCheckService = RunnerBootCheckService;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LEGACY_BLOB_GUARD_SINCE = void 0;
|
|
4
|
+
exports.buildUpdatePreflight = buildUpdatePreflight;
|
|
5
|
+
exports.legacyGuardClientOf = legacyGuardClientOf;
|
|
6
|
+
/**
|
|
7
|
+
* The `applyServerUpdate` pre-flight (D877).
|
|
8
|
+
*
|
|
9
|
+
* Question: would the TARGET version refuse to start any addon on THIS node's
|
|
10
|
+
* live data? Today there is one deterministic Init refusal the framework can
|
|
11
|
+
* pre-check: the D870 legacy-blob guard (`assertLegacyBlobsMigrated`) - an addon
|
|
12
|
+
* whose settings still sit in its per-addon blob tables while the standard store
|
|
13
|
+
* holds none.
|
|
14
|
+
*
|
|
15
|
+
* The store-shape check is deliberately version-independent: the live store is
|
|
16
|
+
* what it is, so the only version question is "does the target contain the
|
|
17
|
+
* guard?" - answered by {@link LEGACY_BLOB_GUARD_SINCE}. The honest limit: it
|
|
18
|
+
* runs inside the version that ships it, so it protects updates FROM this
|
|
19
|
+
* version onwards; an update from an older hub never ran it. And it knows only
|
|
20
|
+
* the refusals listed here - a new deterministic Init refusal has to be added
|
|
21
|
+
* to this module to be pre-checked.
|
|
22
|
+
*/
|
|
23
|
+
const system_1 = require("@camstack/system");
|
|
24
|
+
const index_js_1 = require("../../server-root/index.js");
|
|
25
|
+
/** The first server version whose addons run `assertLegacyBlobsMigrated` at Init. */
|
|
26
|
+
exports.LEGACY_BLOB_GUARD_SINCE = '1.2.585';
|
|
27
|
+
function buildUpdatePreflight(deps) {
|
|
28
|
+
return async (targetVersion) => {
|
|
29
|
+
if ((0, index_js_1.compareSemver)(targetVersion, exports.LEGACY_BLOB_GUARD_SINCE) < 0)
|
|
30
|
+
return [];
|
|
31
|
+
const store = deps.store();
|
|
32
|
+
if (store === null) {
|
|
33
|
+
throw new Error('the settings store is not available to pre-check the update');
|
|
34
|
+
}
|
|
35
|
+
const blockers = [];
|
|
36
|
+
for (const addonId of deps.forkedAddonIds()) {
|
|
37
|
+
try {
|
|
38
|
+
await (0, system_1.assertLegacyBlobsMigrated)(store, addonId, deps.logger);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
// A refusal is the finding; any other error is "could not ask" and the
|
|
42
|
+
// caller treats a throw as exactly that (it never blocks on it).
|
|
43
|
+
if (!(err instanceof system_1.UnmigratedSettingsError))
|
|
44
|
+
throw err;
|
|
45
|
+
blockers.push({
|
|
46
|
+
addonId,
|
|
47
|
+
reason: err.message,
|
|
48
|
+
remedy: system_1.UNMIGRATED_SETTINGS_REMEDY,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return blockers;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The three store calls the guard makes, over the hub's `settings-store` provider. */
|
|
56
|
+
function legacyGuardClientOf(store) {
|
|
57
|
+
return {
|
|
58
|
+
query: { query: (input) => store.query(input) },
|
|
59
|
+
get: { query: (input) => store.get(input) },
|
|
60
|
+
count: { query: (input) => store.count(input) },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -103,6 +103,12 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
|
103
103
|
applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
|
|
104
104
|
env: options.env,
|
|
105
105
|
now: options.now,
|
|
106
|
+
...(options.updatePreflight !== undefined
|
|
107
|
+
? { updatePreflight: options.updatePreflight }
|
|
108
|
+
: {}),
|
|
109
|
+
...(options.runnerBootCheck !== undefined
|
|
110
|
+
? { runnerBootCheck: options.runnerBootCheck }
|
|
111
|
+
: {}),
|
|
106
112
|
});
|
|
107
113
|
this.updateAvailability =
|
|
108
114
|
options.eventBus !== undefined
|
package/dist/main.js
CHANGED
|
@@ -67,6 +67,7 @@ const request_census_settings_1 = require("./api/core/request-census-settings");
|
|
|
67
67
|
const register_cors_policy_1 = require("./api/cors/register-cors-policy");
|
|
68
68
|
const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
|
|
69
69
|
const health_routes_1 = require("./api/health/health.routes");
|
|
70
|
+
const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
|
|
70
71
|
const hub_cluster_url_1 = require("./api/hub-cluster-url");
|
|
71
72
|
const model_distributor_js_1 = require("./api/model-distributor.js");
|
|
72
73
|
const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
|
|
@@ -729,6 +730,7 @@ async function bootstrap() {
|
|
|
729
730
|
// router finishes registering (see `trpcRegistered = true` below).
|
|
730
731
|
isReady: () => trpcRegistered,
|
|
731
732
|
verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
|
|
733
|
+
runnerBootCheck: () => app.get(runner_boot_check_1.RunnerBootCheckService).current(),
|
|
732
734
|
});
|
|
733
735
|
console.log(`[bootstrap] Health routes registered (hub v${hubVersion})`);
|
|
734
736
|
(0, adaptive_probe_routes_1.registerAdaptiveProbeRoute)(fastify, {
|
|
@@ -1693,6 +1695,10 @@ async function bootstrap() {
|
|
|
1693
1695
|
// Post-boot: fork workers, register device streams, emit system.boot
|
|
1694
1696
|
const postBoot = app.get(post_boot_service_1.PostBootService);
|
|
1695
1697
|
await postBoot.run({ port, host, dataPath, trpcRegistered, tlsCert });
|
|
1698
|
+
// D877: ONE check, ~60 s after the boot sequence, of the runners this node
|
|
1699
|
+
// planned against what its process service holds. Stored in memory and served
|
|
1700
|
+
// on `getServerPackageStatus` / `/health`; not a timer loop.
|
|
1701
|
+
app.get(runner_boot_check_1.RunnerBootCheckService).scheduleAfterBoot();
|
|
1696
1702
|
// The hub reached ready — when this boot is the PROBATION boot of a freshly
|
|
1697
1703
|
// staged @camstack/server root, promote it (currentVersion ← pending, N-1
|
|
1698
1704
|
// retained for rollback) and disarm the starter's crash-loop auto-rollback.
|
package/dist/manual-boot.js
CHANGED
|
@@ -75,6 +75,8 @@ const notification_wrapper_service_1 = require("./core/notification/notification
|
|
|
75
75
|
const toast_wrapper_service_1 = require("./core/notification/toast-wrapper.service");
|
|
76
76
|
const repl_engine_service_1 = require("./core/repl/repl-engine.service");
|
|
77
77
|
const server_update_service_1 = require("./core/server-update/server-update.service");
|
|
78
|
+
const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
|
|
79
|
+
const update_preflight_1 = require("./core/runner-health/update-preflight");
|
|
78
80
|
const storage_service_1 = require("./core/storage/storage.service");
|
|
79
81
|
const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
|
|
80
82
|
const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
|
|
@@ -251,7 +253,49 @@ async function bootManual(opts) {
|
|
|
251
253
|
// Runtime-updatable root package (phase 1: hub only). The service owns the
|
|
252
254
|
// data-dir server-root staging/rollback lifecycle; restarts route through
|
|
253
255
|
// AddonPackageService.restartServer for the marker + reconnect toast.
|
|
256
|
+
// D877: the runner boot check (one-shot after boot, scheduled by main.ts) and
|
|
257
|
+
// the update pre-flight (would the target refuse addons on the live store?).
|
|
258
|
+
const hubRunnerBootCheck = new runner_boot_check_1.RunnerBootCheckService({
|
|
259
|
+
logger: loggingService.createLogger('RunnerBootCheck'),
|
|
260
|
+
nodeId: 'hub',
|
|
261
|
+
planned: () => addonRegistryService.plannedRunnerIds(),
|
|
262
|
+
listProcesses: async () => {
|
|
263
|
+
const rows = await addonRegistryService.listRunnerRows();
|
|
264
|
+
if (rows === null)
|
|
265
|
+
throw new Error('the process service did not answer');
|
|
266
|
+
return rows;
|
|
267
|
+
},
|
|
268
|
+
// The verdict reaches the alert centre (one alert + the direct critical push).
|
|
269
|
+
onResult: (check) => {
|
|
270
|
+
eventBusService.emit({
|
|
271
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
272
|
+
timestamp: new Date(),
|
|
273
|
+
source: { type: 'core', id: 'runner-boot-check' },
|
|
274
|
+
category: types_1.EventCategory.SystemRunnerCheck,
|
|
275
|
+
data: {
|
|
276
|
+
nodeId: 'hub',
|
|
277
|
+
degraded: (0, runner_boot_check_1.isRunnerBootCheckDegraded)(check),
|
|
278
|
+
summary: (0, runner_boot_check_1.describeRunnerBootCheck)(check),
|
|
279
|
+
expected: check.expected,
|
|
280
|
+
running: check.running,
|
|
281
|
+
failed: [...check.failed],
|
|
282
|
+
refused: check.refused.map((r) => ({ runnerId: r.runnerId, reason: r.reason })),
|
|
283
|
+
missing: [...check.missing],
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
const settingsStoreOrNull = () => capabilityService.getRegistry()?.getSingleton('settings-store') ?? null;
|
|
254
289
|
const serverUpdateService = new server_update_service_1.ServerUpdateService({
|
|
290
|
+
updatePreflight: (0, update_preflight_1.buildUpdatePreflight)({
|
|
291
|
+
logger: loggingService.createLogger('UpdatePreflight'),
|
|
292
|
+
forkedAddonIds: () => addonRegistryService.forkedAddonIds(),
|
|
293
|
+
store: () => {
|
|
294
|
+
const store = settingsStoreOrNull();
|
|
295
|
+
return store === null ? null : (0, update_preflight_1.legacyGuardClientOf)(store);
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
runnerBootCheck: () => hubRunnerBootCheck.current(),
|
|
255
299
|
logger: loggingService.createLogger('ServerUpdate'),
|
|
256
300
|
restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
|
|
257
301
|
eventBus: eventBusService,
|
|
@@ -337,6 +381,10 @@ async function bootManual(opts) {
|
|
|
337
381
|
container.register(addon_package_service_1.AddonPackageService, addonPackageService);
|
|
338
382
|
container.register(repl_engine_service_1.ReplEngineService, replEngineService);
|
|
339
383
|
container.register(server_update_service_1.ServerUpdateService, serverUpdateService);
|
|
384
|
+
container.register(runner_boot_check_1.RunnerBootCheckService, hubRunnerBootCheck);
|
|
385
|
+
// A runner settling (READY / crashed / failed / refused) re-evaluates a verdict
|
|
386
|
+
// that still had runners on their way (D877).
|
|
387
|
+
eventBusService.subscribe({ category: types_1.EventCategory.ProcessStateChanged }, () => hubRunnerBootCheck.noteRunnerChange());
|
|
340
388
|
container.register(topology_emitter_service_1.TopologyEmitterService, topologyEmitterService);
|
|
341
389
|
container.register(post_boot_service_1.PostBootService, postBootService);
|
|
342
390
|
container.register(update_check_scheduler_js_1.UpdateCheckScheduler, updateCheckScheduler);
|
|
@@ -1141,6 +1141,8 @@ var require_dist = __commonJS({
|
|
|
1141
1141
|
logTags;
|
|
1142
1142
|
restartServerFn;
|
|
1143
1143
|
dataDir;
|
|
1144
|
+
updatePreflightFn;
|
|
1145
|
+
runnerBootCheckFn;
|
|
1144
1146
|
execNpm;
|
|
1145
1147
|
ensureNativePrebuildsFn;
|
|
1146
1148
|
applyClosurePatchesFn;
|
|
@@ -1172,6 +1174,8 @@ var require_dist = __commonJS({
|
|
|
1172
1174
|
};
|
|
1173
1175
|
this.restartServerFn = options.restartServer;
|
|
1174
1176
|
this.dataDir = path6.resolve(options.dataDir);
|
|
1177
|
+
this.updatePreflightFn = options.updatePreflight ?? null;
|
|
1178
|
+
this.runnerBootCheckFn = options.runnerBootCheck ?? (() => void 0);
|
|
1175
1179
|
this.execNpm = options.execNpm ?? (async (args, opts) => {
|
|
1176
1180
|
const { stdout } = await execFileAsync("npm", [
|
|
1177
1181
|
...args
|
|
@@ -1320,7 +1324,12 @@ var require_dist = __commonJS({
|
|
|
1320
1324
|
// The other half of the same question. `imageContract` says the shell is
|
|
1321
1325
|
// BEHIND; this says WHAT the shell is, which is the only thing that can
|
|
1322
1326
|
// turn that verdict into an instruction the operator can act on.
|
|
1323
|
-
wrapper: this.wrapperIdentity()
|
|
1327
|
+
wrapper: this.wrapperIdentity(),
|
|
1328
|
+
// Planned runners against the process service's states (D877); absent
|
|
1329
|
+
// until the one-shot boot check has run.
|
|
1330
|
+
...this.runnerBootCheckFn() !== void 0 ? {
|
|
1331
|
+
runners: this.runnerBootCheckFn()
|
|
1332
|
+
} : {}
|
|
1324
1333
|
};
|
|
1325
1334
|
}
|
|
1326
1335
|
// ── Check ─────────────────────────────────────────────────────────────
|
|
@@ -1440,6 +1449,21 @@ var require_dist = __commonJS({
|
|
|
1440
1449
|
error: null
|
|
1441
1450
|
};
|
|
1442
1451
|
}
|
|
1452
|
+
async runPreflight(target) {
|
|
1453
|
+
if (this.updatePreflightFn === null) return [];
|
|
1454
|
+
try {
|
|
1455
|
+
return await this.updatePreflightFn(target);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
this.logger.warn("update pre-flight could not run - proceeding without it", {
|
|
1458
|
+
tags: this.logTags,
|
|
1459
|
+
meta: {
|
|
1460
|
+
targetVersion: target,
|
|
1461
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1443
1467
|
async applyServerUpdate(input) {
|
|
1444
1468
|
this.logger.info("root package update requested", {
|
|
1445
1469
|
tags: this.logTags,
|
|
@@ -1477,6 +1501,29 @@ var require_dist = __commonJS({
|
|
|
1477
1501
|
return this.refuseUpdate("dev-uploads-missing", target, `Refused: dev-channel version ${target} has no uploaded tarballs on this node (${uploadsDir} is missing). On the hub: \`camstack deploy-server\`. On an agent: \`camstack deploy-server -n <nodeId>\` (or \`--all\`) so the hub can POST /api/server-upload/propagate first.`);
|
|
1478
1502
|
}
|
|
1479
1503
|
}
|
|
1504
|
+
const blockers = await this.runPreflight(target);
|
|
1505
|
+
if (blockers.length > 0 && input.acknowledgeRefusals !== true) {
|
|
1506
|
+
const names = blockers.map((b) => b.addonId).join(", ");
|
|
1507
|
+
const remedies = [
|
|
1508
|
+
...new Set(blockers.map((b) => b.remedy))
|
|
1509
|
+
].join(" ");
|
|
1510
|
+
const refused = this.refuseUpdate("addon-would-refuse", target, `Refused: ${this.spec.packageName}@${target} would refuse to start ${blockers.length} addon(s) on this node's data: ${names}. ${remedies} Then retry, or pass acknowledgeRefusals to apply anyway (those addons will stay down).`);
|
|
1511
|
+
return {
|
|
1512
|
+
...refused,
|
|
1513
|
+
blockers: [
|
|
1514
|
+
...blockers
|
|
1515
|
+
]
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if (blockers.length > 0) {
|
|
1519
|
+
this.logger.warn("root package update applied over acknowledged addon refusals", {
|
|
1520
|
+
tags: this.logTags,
|
|
1521
|
+
meta: {
|
|
1522
|
+
targetVersion: target,
|
|
1523
|
+
addonIds: blockers.map((b) => b.addonId)
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1480
1527
|
this.inFlight = "staging";
|
|
1481
1528
|
let stagingPath;
|
|
1482
1529
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.590",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
41
|
+
"@camstack/addon-admin-ui": "1.2.434",
|
|
42
|
+
"@camstack/addon-agent-ui": "1.2.241",
|
|
43
43
|
"@camstack/addon-auth": "1.2.148",
|
|
44
44
|
"@camstack/addon-decoder-nodeav": "1.2.123",
|
|
45
45
|
"@camstack/addon-notifiers": "1.2.135",
|
|
46
46
|
"@camstack/addon-pipeline": "1.2.374",
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
48
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
47
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.330",
|
|
48
|
+
"@camstack/addon-post-analysis": "1.2.373",
|
|
49
49
|
"@camstack/develop": "0.1.11",
|
|
50
50
|
"@camstack/sdk": "1.2.132",
|
|
51
|
-
"@camstack/system": "1.2.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
51
|
+
"@camstack/system": "1.2.443",
|
|
52
|
+
"@camstack/types": "1.2.379",
|
|
53
|
+
"@camstack/ui-library": "1.2.297",
|
|
54
54
|
"@fastify/compress": "^9.0.0",
|
|
55
55
|
"@fastify/cookie": "^11.0.2",
|
|
56
56
|
"@fastify/cors": "^11.2.0",
|