@camstack/server 1.2.477 → 1.2.478
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-service.js +27 -71
- package/dist/agent/agent-undeploy.js +255 -0
- package/dist/agent/main.js +13 -0
- package/dist/api/core/cap-providers.js +91 -3
- package/dist/api/trpc/generated-cap-routers.js +8 -133
- package/dist/api/trpc/trpc.router.js +1 -1
- package/dist/core/addon/addon-uninstall.js +259 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +5 -5
- package/dist/core/agent/agent-addon-backfill.js +20 -18
- package/dist/core/agent/agent-registry.service.js +35 -7
- package/dist/server-root/index.js +0 -1
- package/dist/single-copy-cleanup.js +0 -1
- package/package.json +13 -13
- package/dist/api/bridge-addons.router.js +0 -109
|
@@ -42,6 +42,7 @@ const path = __importStar(require("node:path"));
|
|
|
42
42
|
const system_1 = require("@camstack/system");
|
|
43
43
|
const moleculer_1 = require("moleculer");
|
|
44
44
|
const agent_deploy_swap_js_1 = require("./agent-deploy-swap.js");
|
|
45
|
+
const agent_undeploy_js_1 = require("./agent-undeploy.js");
|
|
45
46
|
const apply_model_distribution_js_1 = require("./apply-model-distribution.js");
|
|
46
47
|
const fetch_bundle_from_hub_js_1 = require("./fetch-bundle-from-hub.js");
|
|
47
48
|
/**
|
|
@@ -57,6 +58,14 @@ const deploySwapLogger = {
|
|
|
57
58
|
child: () => deploySwapLogger,
|
|
58
59
|
withTags: () => deploySwapLogger,
|
|
59
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Packages an agent bootstraps itself when the bootstrap does not say
|
|
63
|
+
* otherwise. Mirrors the hub reconcile's `AGENT_BOOTSTRAP_PACKAGES`.
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_AGENT_BOOTSTRAP_PACKAGES = new Set([
|
|
66
|
+
'@camstack/system',
|
|
67
|
+
'@camstack/addon-agent-ui',
|
|
68
|
+
]);
|
|
60
69
|
/** Timeout for the agent-local `$process.restart` delegation. */
|
|
61
70
|
const AGENT_PROCESS_RESTART_TIMEOUT_MS = 30_000;
|
|
62
71
|
function getLocalIps() {
|
|
@@ -455,81 +464,28 @@ function createAgentService(deps) {
|
|
|
455
464
|
},
|
|
456
465
|
},
|
|
457
466
|
/**
|
|
458
|
-
* Genuinely drop a deployed addon from this agent.
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* the addon
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* To truly undeploy we must, in order:
|
|
465
|
-
* 1. `shutdown()` the running instance (releases timers, sockets,
|
|
466
|
-
* native handles) — same hook the agent's SIGTERM path uses.
|
|
467
|
-
* 2. `broker.destroyService(addonId)` — the deployed-addon Moleculer
|
|
468
|
-
* service is named after the addon DECLARATION id (see
|
|
469
|
-
* `createAddonService` → `name: declaration.id`). Destroying it
|
|
470
|
-
* unregisters every capability action so the cluster stops
|
|
471
|
-
* routing to / advertising this agent's provider.
|
|
472
|
-
* 3. Drop the `loadedAddons` entry and delete the on-disk folder.
|
|
473
|
-
*
|
|
474
|
-
* `addonId` here is the addon DECLARATION id — the same key
|
|
475
|
-
* `loadedAddons` uses and the same value `$agent.status` reports, so
|
|
476
|
-
* the hub reconciler can match it directly.
|
|
467
|
+
* Genuinely drop a deployed addon — or a whole package — from this agent.
|
|
468
|
+
* `{ packageName }` is the operator's uninstall (`addons.uninstallPackage
|
|
469
|
+
* ({ nodeId })`); `{ addonId }` is the stale-addon reconcile. Both stop the
|
|
470
|
+
* runner(s) hosting the addon, which the old body never did: on an agent
|
|
471
|
+
* nearly every addon lives in a group runner, so deleting its folder left
|
|
472
|
+
* the process running and advertising its caps. See `agent-undeploy.ts`.
|
|
477
473
|
*/
|
|
478
474
|
undeploy: {
|
|
479
475
|
handler: async (ctx) => {
|
|
480
476
|
const { params, broker } = ctx;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
// 2. Destroy the Moleculer service so the cluster stops seeing
|
|
496
|
-
// this agent's capability providers for the addon.
|
|
497
|
-
try {
|
|
498
|
-
await broker.destroyService(addonId);
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
// Service may not exist (group-spawned addons have no per-addon
|
|
502
|
-
// service on the agent broker, or it was never created).
|
|
503
|
-
}
|
|
504
|
-
// 3. Forget the entry and remove the deployed folder. The deploy
|
|
505
|
-
// handler keys the on-disk dir by the PACKAGE name, but $agent
|
|
506
|
-
// redeploys for a single addon use addonId === packageName for
|
|
507
|
-
// unscoped packages; remove whichever folder matches.
|
|
508
|
-
deps.loadedAddons.delete(addonId);
|
|
509
|
-
const addonDir = path.join(deps.addonsDir, addonId);
|
|
510
|
-
if (fs.existsSync(addonDir)) {
|
|
511
|
-
fs.rmSync(addonDir, { recursive: true, force: true });
|
|
512
|
-
}
|
|
513
|
-
// Defense-in-depth: one package dir can host MANY addons (e.g.
|
|
514
|
-
// `@camstack/addon-pipeline` ships decoder-ffmpeg, recorder,
|
|
515
|
-
// motion-wasm, …). Removing the whole bundle because ONE member was
|
|
516
|
-
// undeployed is destructive — every sibling runner would crash-loop
|
|
517
|
-
// on the missing package.json. Only remove the package dir when no
|
|
518
|
-
// OTHER loaded addon still lives in it (this addon's own entry was
|
|
519
|
-
// already deleted from `loadedAddons` above, so the remaining
|
|
520
|
-
// values are exactly the siblings).
|
|
521
|
-
const pkgName = entry?.packageName;
|
|
522
|
-
if (pkgName && pkgName !== addonId) {
|
|
523
|
-
const pkgShared = [...deps.loadedAddons.values()].some((e) => e.packageName === pkgName);
|
|
524
|
-
if (!pkgShared) {
|
|
525
|
-
const pkgDir = path.join(deps.addonsDir, pkgName);
|
|
526
|
-
if (fs.existsSync(pkgDir)) {
|
|
527
|
-
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
broker.logger.info(`$agent.undeploy: ${addonId} disposed (instance + service + folder)`);
|
|
532
|
-
return { success: true, addonId };
|
|
477
|
+
return (0, agent_undeploy_js_1.undeployFromAgent)({
|
|
478
|
+
addonsDir: deps.addonsDir,
|
|
479
|
+
loadedAddons: deps.loadedAddons,
|
|
480
|
+
bootstrapPackages: deps.bootstrapPackages ?? DEFAULT_AGENT_BOOTSTRAP_PACKAGES,
|
|
481
|
+
listRunnersByAddon: (addonId) => broker.call('$process.listByAddon', { addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
|
|
482
|
+
stopRunner: (name) => broker.call('$process.stop', { name }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
|
|
483
|
+
destroyService: (addonId) => broker.destroyService(addonId),
|
|
484
|
+
unregisterProvider: (capName, addonId) => deps.unregisterProvider?.(capName, addonId),
|
|
485
|
+
respawnSurvivors: deps.reloadDeployedAddons,
|
|
486
|
+
reconcileNodeRegistration: deps.reconcileNodeRegistration,
|
|
487
|
+
logger: deploySwapLogger,
|
|
488
|
+
}, { addonId: params.addonId, packageName: params.packageName });
|
|
533
489
|
},
|
|
534
490
|
},
|
|
535
491
|
/**
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.undeployFromAgent = undeployFromAgent;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const agent_group_runner_js_1 = require("./agent-group-runner.js");
|
|
40
|
+
function capName(cap) {
|
|
41
|
+
return typeof cap === 'string' ? cap : cap.name;
|
|
42
|
+
}
|
|
43
|
+
function packageDirFor(addonsDir, packageName) {
|
|
44
|
+
const resolved = path.resolve(addonsDir, packageName);
|
|
45
|
+
const root = path.resolve(addonsDir);
|
|
46
|
+
// A package name is data from the network. It must name a dir INSIDE the
|
|
47
|
+
// addon root and never the root itself.
|
|
48
|
+
if (resolved === root || !resolved.startsWith(root + path.sep))
|
|
49
|
+
return null;
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
function resolveTarget(seam, request) {
|
|
53
|
+
const packageName = request.packageName ??
|
|
54
|
+
(request.addonId !== undefined
|
|
55
|
+
? seam.loadedAddons.get(request.addonId)?.packageName
|
|
56
|
+
: undefined);
|
|
57
|
+
const packageDir = packageName !== undefined ? packageDirFor(seam.addonsDir, packageName) : null;
|
|
58
|
+
const manifest = packageDir !== null ? (0, agent_group_runner_js_1.readPackageManifestAddons)(packageDir) : null;
|
|
59
|
+
const capsByAddon = new Map();
|
|
60
|
+
let protectedBy = null;
|
|
61
|
+
for (const decl of manifest?.declarations ?? []) {
|
|
62
|
+
capsByAddon.set(decl.id, (decl.capabilities ?? []).map(capName));
|
|
63
|
+
if (decl.protected === true)
|
|
64
|
+
protectedBy = decl.id;
|
|
65
|
+
}
|
|
66
|
+
if (request.packageName === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
packageName,
|
|
69
|
+
addonIds: request.addonId !== undefined ? [request.addonId] : [],
|
|
70
|
+
capsByAddon,
|
|
71
|
+
packageDir,
|
|
72
|
+
protectedBy,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const ids = new Set(capsByAddon.keys());
|
|
76
|
+
for (const entry of seam.loadedAddons.values()) {
|
|
77
|
+
if (entry.packageName === request.packageName)
|
|
78
|
+
ids.add(entry.id);
|
|
79
|
+
}
|
|
80
|
+
return { packageName, addonIds: [...ids], capsByAddon, packageDir, protectedBy };
|
|
81
|
+
}
|
|
82
|
+
function refusalReason(seam, target) {
|
|
83
|
+
if (target.packageName !== undefined && seam.bootstrapPackages.has(target.packageName)) {
|
|
84
|
+
return `${target.packageName} is this node's bootstrap infrastructure and cannot be uninstalled`;
|
|
85
|
+
}
|
|
86
|
+
if (target.protectedBy !== null) {
|
|
87
|
+
return `${target.packageName ?? 'package'} is protected (addon "${target.protectedBy}" declares protected: true)`;
|
|
88
|
+
}
|
|
89
|
+
if (target.packageName === undefined && target.addonIds.length === 0) {
|
|
90
|
+
return 'neither addonId nor packageName was given';
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async function shutdownInProcess(seam, addonId) {
|
|
95
|
+
const entry = seam.loadedAddons.get(addonId);
|
|
96
|
+
if (entry?.addon?.shutdown) {
|
|
97
|
+
try {
|
|
98
|
+
await entry.addon.shutdown();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
seam.logger.warn(`undeploy: ${addonId} shutdown() threw`, {
|
|
102
|
+
tags: { addonId },
|
|
103
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await seam.destroyService(addonId);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Group-runner addons have no per-addon service on the agent broker.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function stopHostingRunners(seam, addonIds) {
|
|
115
|
+
const removing = new Set(addonIds);
|
|
116
|
+
const runners = new Map();
|
|
117
|
+
const warnings = [];
|
|
118
|
+
for (const id of addonIds) {
|
|
119
|
+
try {
|
|
120
|
+
for (const row of await seam.listRunnersByAddon(id))
|
|
121
|
+
runners.set(row.name, row);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
warnings.push(`could not list runners for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const stopped = [];
|
|
128
|
+
const survivors = [];
|
|
129
|
+
for (const runner of runners.values()) {
|
|
130
|
+
const result = await seam.stopRunner(runner.name).catch((err) => ({
|
|
131
|
+
success: false,
|
|
132
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
133
|
+
}));
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
warnings.push(`runner "${runner.name}" did not stop: ${result.reason ?? 'unknown'}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
stopped.push(runner.name);
|
|
139
|
+
for (const id of runner.addonIds) {
|
|
140
|
+
if (!removing.has(id))
|
|
141
|
+
survivors.push(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { stopped, survivors, warnings };
|
|
145
|
+
}
|
|
146
|
+
function removeDirs(seam, request, target) {
|
|
147
|
+
const removed = [];
|
|
148
|
+
const rm = (dir) => {
|
|
149
|
+
if (dir === null || !fs.existsSync(dir))
|
|
150
|
+
return;
|
|
151
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
removed.push(dir);
|
|
153
|
+
};
|
|
154
|
+
if (request.packageName !== undefined) {
|
|
155
|
+
rm(target.packageDir);
|
|
156
|
+
return removed;
|
|
157
|
+
}
|
|
158
|
+
// Addon mode — the reconcile's per-id removal. One package dir can host MANY
|
|
159
|
+
// addons; only remove it when no OTHER loaded addon still lives in it (this
|
|
160
|
+
// addon's own row is already gone from `loadedAddons`).
|
|
161
|
+
const addonId = request.addonId;
|
|
162
|
+
if (addonId !== undefined)
|
|
163
|
+
rm(packageDirFor(seam.addonsDir, addonId));
|
|
164
|
+
const pkg = target.packageName;
|
|
165
|
+
if (pkg !== undefined && pkg !== addonId) {
|
|
166
|
+
const shared = [...seam.loadedAddons.values()].some((e) => e.packageName === pkg);
|
|
167
|
+
if (!shared)
|
|
168
|
+
rm(target.packageDir);
|
|
169
|
+
}
|
|
170
|
+
return removed;
|
|
171
|
+
}
|
|
172
|
+
/** Run one `$agent.undeploy`. Never throws for a refusal — it answers `refused`. */
|
|
173
|
+
async function undeployFromAgent(seam, request) {
|
|
174
|
+
const target = resolveTarget(seam, request);
|
|
175
|
+
const scope = request.packageName !== undefined ? 'package' : 'addon';
|
|
176
|
+
const base = {
|
|
177
|
+
scope,
|
|
178
|
+
addonId: request.addonId,
|
|
179
|
+
packageName: target.packageName,
|
|
180
|
+
addonIds: target.addonIds,
|
|
181
|
+
};
|
|
182
|
+
const refused = refusalReason(seam, target);
|
|
183
|
+
if (refused !== null) {
|
|
184
|
+
seam.logger.warn(`undeploy refused: ${refused}`, { meta: { request } });
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
success: false,
|
|
188
|
+
outcome: 'refused',
|
|
189
|
+
reason: refused,
|
|
190
|
+
runnersStopped: [],
|
|
191
|
+
runnersRespawned: [],
|
|
192
|
+
removedDirs: [],
|
|
193
|
+
warnings: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const present = target.addonIds.some((id) => seam.loadedAddons.has(id)) ||
|
|
197
|
+
(target.packageDir !== null && fs.existsSync(target.packageDir));
|
|
198
|
+
if (scope === 'package' && !present) {
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
success: true,
|
|
202
|
+
outcome: 'not-installed',
|
|
203
|
+
runnersStopped: [],
|
|
204
|
+
runnersRespawned: [],
|
|
205
|
+
removedDirs: [],
|
|
206
|
+
warnings: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
for (const id of target.addonIds)
|
|
210
|
+
await shutdownInProcess(seam, id);
|
|
211
|
+
const runners = await stopHostingRunners(seam, target.addonIds);
|
|
212
|
+
for (const id of target.addonIds) {
|
|
213
|
+
for (const cap of target.capsByAddon.get(id) ?? [])
|
|
214
|
+
seam.unregisterProvider(cap, id);
|
|
215
|
+
seam.loadedAddons.delete(id);
|
|
216
|
+
}
|
|
217
|
+
// A survivor's row is dropped too, so the discovery pass below sees it as
|
|
218
|
+
// not-loaded and respawns its runner — without the removed addons, whose
|
|
219
|
+
// code is gone by then.
|
|
220
|
+
for (const id of runners.survivors) {
|
|
221
|
+
for (const cap of target.capsByAddon.get(id) ?? [])
|
|
222
|
+
seam.unregisterProvider(cap, id);
|
|
223
|
+
seam.loadedAddons.delete(id);
|
|
224
|
+
}
|
|
225
|
+
const removedDirs = removeDirs(seam, request, target);
|
|
226
|
+
const warnings = [...runners.warnings];
|
|
227
|
+
let runnersRespawned = [];
|
|
228
|
+
if (runners.survivors.length > 0) {
|
|
229
|
+
if (seam.respawnSurvivors) {
|
|
230
|
+
try {
|
|
231
|
+
await seam.respawnSurvivors();
|
|
232
|
+
runnersRespawned = runners.stopped;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
warnings.push(`survivors ${runners.survivors.join(', ')} were not respawned: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
warnings.push(`survivors ${runners.survivors.join(', ')} were stopped and not respawned`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
seam.reconcileNodeRegistration?.();
|
|
243
|
+
seam.logger.info(`undeploy: ${target.packageName ?? request.addonId ?? '?'} removed ` +
|
|
244
|
+
`(addons=${target.addonIds.join(',') || '-'} runnersStopped=${runners.stopped.join(',') || '-'} ` +
|
|
245
|
+
`dirs=${removedDirs.length})`, { meta: { warnings } });
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
success: true,
|
|
249
|
+
outcome: 'uninstalled',
|
|
250
|
+
runnersStopped: runners.stopped,
|
|
251
|
+
runnersRespawned,
|
|
252
|
+
removedDirs,
|
|
253
|
+
warnings,
|
|
254
|
+
};
|
|
255
|
+
}
|
package/dist/agent/main.js
CHANGED
|
@@ -470,6 +470,19 @@ async function startAgent(configPath) {
|
|
|
470
470
|
// (manifest strip + runtime deps + native deps + atomic swap) instead of
|
|
471
471
|
// the bare tar swap that shipped addons without their node_modules.
|
|
472
472
|
installBundleTgz: (tgzPath) => installer.installFromTgz(tgzPath),
|
|
473
|
+
// `$agent.undeploy` refuses what this node bootstraps itself, and drops the
|
|
474
|
+
// group-runner cap proxies it removes (registered by
|
|
475
|
+
// `registerGroupRunnerProviders`, ownerless — so a plain unregister owns them).
|
|
476
|
+
bootstrapPackages: new Set([
|
|
477
|
+
'@camstack/system',
|
|
478
|
+
'@camstack/addon-agent-ui',
|
|
479
|
+
...system_1.AddonInstaller.AGENT_PACKAGES,
|
|
480
|
+
]),
|
|
481
|
+
unregisterProvider: (capName, addonId) => {
|
|
482
|
+
if (capabilityRegistry.hasProvider(capName, addonId)) {
|
|
483
|
+
capabilityRegistry.unregisterProvider(capName, addonId);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
473
486
|
});
|
|
474
487
|
broker.createService(agentServiceSchema);
|
|
475
488
|
// $process service — manages forked child processes (same as hub).
|
|
@@ -77,6 +77,7 @@ const types_1 = require("@camstack/types");
|
|
|
77
77
|
const server_1 = require("@trpc/server");
|
|
78
78
|
const integration_id_backfill_1 = require("../../boot/integration-id-backfill");
|
|
79
79
|
const addon_package_service_js_1 = require("../../core/addon/addon-package.service.js");
|
|
80
|
+
const addon_uninstall_js_1 = require("../../core/addon/addon-uninstall.js");
|
|
80
81
|
const lifecycle_runner_singleton_js_1 = require("../../core/lifecycle/lifecycle-runner.singleton.js");
|
|
81
82
|
const agent_installed_packages_js_1 = require("../../core/updates/agent-installed-packages.js");
|
|
82
83
|
const http_request_census_singleton_js_1 = require("../trpc/http-request-census.singleton.js");
|
|
@@ -1318,8 +1319,82 @@ function isHubNode(nodeId) {
|
|
|
1318
1319
|
* filter it out to make the list look clean — the number reaching zero is the
|
|
1319
1320
|
* point of the number.
|
|
1320
1321
|
*/
|
|
1321
|
-
|
|
1322
|
+
/**
|
|
1323
|
+
* How long the hub waits for an agent's `$agent.undeploy`. It stops runners
|
|
1324
|
+
* (each up to a 5 s SIGTERM grace plus the group reap) and may respawn a shared
|
|
1325
|
+
* runner's survivors, so it is far longer than a status read.
|
|
1326
|
+
*/
|
|
1327
|
+
const AGENT_UNDEPLOY_TIMEOUT_MS = 120_000;
|
|
1328
|
+
function parseAgentStatusAddons(reply) {
|
|
1329
|
+
if (typeof reply !== 'object' || reply === null || !('addons' in reply))
|
|
1330
|
+
return [];
|
|
1331
|
+
const addons = reply.addons;
|
|
1332
|
+
if (!Array.isArray(addons))
|
|
1333
|
+
return [];
|
|
1334
|
+
const rows = [];
|
|
1335
|
+
for (const raw of addons) {
|
|
1336
|
+
if (typeof raw !== 'object' || raw === null || !('id' in raw) || typeof raw.id !== 'string') {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const packageName = 'packageName' in raw && typeof raw.packageName === 'string' ? raw.packageName : undefined;
|
|
1340
|
+
const version = 'version' in raw && typeof raw.version === 'string' ? raw.version : undefined;
|
|
1341
|
+
rows.push({ id: raw.id, packageName, version });
|
|
1342
|
+
}
|
|
1343
|
+
return rows;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* The real seam behind `addons.uninstallPackage` / `listInstallations`. The
|
|
1347
|
+
* agent leg is the same hub-side `broker.call({ nodeID })` to the agent's
|
|
1348
|
+
* `$agent` service that `updatePackage({ nodeId })` uses.
|
|
1349
|
+
*/
|
|
1350
|
+
function buildUninstallSeam(ar, ps, broker, agentRegistry) {
|
|
1351
|
+
return {
|
|
1352
|
+
isProtected: (name) => ps.isProtected(name),
|
|
1353
|
+
listHubPackages: () => {
|
|
1354
|
+
let installed;
|
|
1355
|
+
try {
|
|
1356
|
+
installed = ps.listInstalled();
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
return [];
|
|
1360
|
+
}
|
|
1361
|
+
const idsByPackage = new Map();
|
|
1362
|
+
for (const row of ar.listAddons()) {
|
|
1363
|
+
const ids = idsByPackage.get(row.manifest.packageName) ?? [];
|
|
1364
|
+
ids.push(row.manifest.id);
|
|
1365
|
+
idsByPackage.set(row.manifest.packageName, ids);
|
|
1366
|
+
}
|
|
1367
|
+
return installed.map((p) => ({
|
|
1368
|
+
name: p.name,
|
|
1369
|
+
version: p.version,
|
|
1370
|
+
addonIds: idsByPackage.get(p.name) ?? [],
|
|
1371
|
+
}));
|
|
1372
|
+
},
|
|
1373
|
+
uninstallOnHub: async (name) => {
|
|
1374
|
+
await ps.uninstallAndReload(name);
|
|
1375
|
+
},
|
|
1376
|
+
listAgents: async () => {
|
|
1377
|
+
if (!agentRegistry)
|
|
1378
|
+
return [];
|
|
1379
|
+
const directory = await agentRegistry.listNodeDirectory();
|
|
1380
|
+
return directory.nodes
|
|
1381
|
+
.filter((n) => !n.isHub && !n.id.includes('/'))
|
|
1382
|
+
.map((n) => ({ id: n.id, name: n.name, isOnline: n.isOnline }));
|
|
1383
|
+
},
|
|
1384
|
+
agentAddons: async (nodeId) => parseAgentStatusAddons(await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: agent_installed_packages_js_1.AGENT_STATUS_TIMEOUT_MS })),
|
|
1385
|
+
agentUndeploy: (nodeId, request) => broker.call('$agent.undeploy', request, {
|
|
1386
|
+
nodeID: nodeId,
|
|
1387
|
+
timeout: AGENT_UNDEPLOY_TIMEOUT_MS,
|
|
1388
|
+
}),
|
|
1389
|
+
recordedPackages: async (nodeId) => agentRegistry ? agentRegistry.getRecordedPackages(nodeId) : [],
|
|
1390
|
+
notePackageUndeployed: async (nodeId, packageName, addonIds) => {
|
|
1391
|
+
await agentRegistry?.notePackageUndeployed(nodeId, packageName, addonIds);
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentRegistry) {
|
|
1322
1396
|
const broker = moleculer.broker;
|
|
1397
|
+
const uninstallSeam = buildUninstallSeam(ar, ps, broker, agentRegistry);
|
|
1323
1398
|
const frameworkAllowSet = new Set([addon_package_service_js_1.SYSTEM_PACKAGE]);
|
|
1324
1399
|
// Process-wide runner built once at boot (manual-boot wires its deps from the
|
|
1325
1400
|
// addon-package service + event bus). `createdBy` travels per `startJob` call.
|
|
@@ -1349,7 +1424,18 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1349
1424
|
installFromWorkspace: async (input) => ps.installFromWorkspaceAndLoad(input.packageName),
|
|
1350
1425
|
isWorkspaceAvailable: async () => ps.isWorkspaceAvailable(),
|
|
1351
1426
|
listWorkspacePackages: async () => ps.listWorkspacePackages(),
|
|
1352
|
-
uninstallPackage: async (input) =>
|
|
1427
|
+
uninstallPackage: async (input) => {
|
|
1428
|
+
try {
|
|
1429
|
+
return await (0, addon_uninstall_js_1.uninstallPackage)(uninstallSeam, input);
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
if (err instanceof addon_uninstall_js_1.PackageProtectedError) {
|
|
1433
|
+
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: err.message });
|
|
1434
|
+
}
|
|
1435
|
+
throw err;
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
listInstallations: async (input) => (0, addon_uninstall_js_1.listInstallations)(uninstallSeam, input.packageName),
|
|
1353
1439
|
reloadPackages: async () => ps.reloadPackages(),
|
|
1354
1440
|
searchAvailable: async (input) => {
|
|
1355
1441
|
const results = await ps.searchNpm(input?.query);
|
|
@@ -1486,7 +1572,9 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1486
1572
|
if (offNode.length > 0) {
|
|
1487
1573
|
throw new server_1.TRPCError({
|
|
1488
1574
|
code: 'BAD_REQUEST',
|
|
1489
|
-
message:
|
|
1575
|
+
message: input.kind === 'uninstall'
|
|
1576
|
+
? `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Uninstall from an agent via uninstallPackage({ packageName, nodeId }), or from every node via uninstallPackage({ packageName, everywhere: true }).`
|
|
1577
|
+
: `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Update an agent's packages via updatePackage({ nodeId }).`,
|
|
1490
1578
|
});
|
|
1491
1579
|
}
|
|
1492
1580
|
return lifecycleRunner.startJob({
|
|
@@ -569,6 +569,14 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
|
|
|
569
569
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
570
570
|
return p.uninstallPackage(input);
|
|
571
571
|
}),
|
|
572
|
+
listInstallations: trpc_middleware_js_1.adminProcedure
|
|
573
|
+
.input(types_10.addonsCapability.methods.listInstallations.input.loose())
|
|
574
|
+
.output(types_10.addonsCapability.methods.listInstallations.output)
|
|
575
|
+
.query(async ({ input, ctx }) => {
|
|
576
|
+
const p = requireCapProvider('addons', () => getProvider(ctx));
|
|
577
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
578
|
+
return p.listInstallations(input);
|
|
579
|
+
}),
|
|
572
580
|
reloadPackages: trpc_middleware_js_1.adminProcedure
|
|
573
581
|
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
574
582
|
.output(types_10.addonsCapability.methods.reloadPackages.output)
|
|
@@ -2434,15 +2442,6 @@ function createCapRouter_dayNight(getProvider, createRemoteProxy) {
|
|
|
2434
2442
|
}
|
|
2435
2443
|
function createCapRouter_decoder(getProvider, createRemoteProxy) {
|
|
2436
2444
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
2437
|
-
supportsCodec: trpc_middleware_js_1.protectedProcedure
|
|
2438
|
-
.input(types_42.decoderCapability.methods.supportsCodec.input.loose())
|
|
2439
|
-
.output(types_42.decoderCapability.methods.supportsCodec.output)
|
|
2440
|
-
.query(async ({ input, ctx }) => {
|
|
2441
|
-
const { nodeId, ...methodInput } = input;
|
|
2442
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2443
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2444
|
-
return p.supportsCodec(methodInput);
|
|
2445
|
-
}),
|
|
2446
2445
|
getInfo: trpc_middleware_js_1.protectedProcedure
|
|
2447
2446
|
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
2448
2447
|
.output(types_42.decoderCapability.methods.getInfo.output)
|
|
@@ -2450,103 +2449,6 @@ function createCapRouter_decoder(getProvider, createRemoteProxy) {
|
|
|
2450
2449
|
const p = resolveProvider('decoder', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2451
2450
|
return p.getInfo();
|
|
2452
2451
|
}),
|
|
2453
|
-
createSession: trpc_middleware_js_1.protectedProcedure
|
|
2454
|
-
.input(types_42.decoderCapability.methods.createSession.input.loose())
|
|
2455
|
-
.output(types_42.decoderCapability.methods.createSession.output)
|
|
2456
|
-
.query(async ({ input, ctx }) => {
|
|
2457
|
-
const { nodeId, ...methodInput } = input;
|
|
2458
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2459
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2460
|
-
return p.createSession(methodInput);
|
|
2461
|
-
}),
|
|
2462
|
-
destroySession: trpc_middleware_js_1.protectedProcedure
|
|
2463
|
-
.input(types_42.decoderCapability.methods.destroySession.input.loose())
|
|
2464
|
-
.output(types_42.decoderCapability.methods.destroySession.output)
|
|
2465
|
-
.query(async ({ input, ctx }) => {
|
|
2466
|
-
const { nodeId, ...methodInput } = input;
|
|
2467
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2468
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2469
|
-
return p.destroySession(methodInput);
|
|
2470
|
-
}),
|
|
2471
|
-
pushPacket: trpc_middleware_js_1.protectedProcedure
|
|
2472
|
-
.input(types_42.decoderCapability.methods.pushPacket.input.loose())
|
|
2473
|
-
.output(types_42.decoderCapability.methods.pushPacket.output)
|
|
2474
|
-
.query(async ({ input, ctx }) => {
|
|
2475
|
-
const { nodeId, ...methodInput } = input;
|
|
2476
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2477
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2478
|
-
return p.pushPacket(methodInput);
|
|
2479
|
-
}),
|
|
2480
|
-
openStream: trpc_middleware_js_1.protectedProcedure
|
|
2481
|
-
.input(types_42.decoderCapability.methods.openStream.input.loose())
|
|
2482
|
-
.output(types_42.decoderCapability.methods.openStream.output)
|
|
2483
|
-
.query(async ({ input, ctx }) => {
|
|
2484
|
-
const { nodeId, ...methodInput } = input;
|
|
2485
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2486
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2487
|
-
return p.openStream(methodInput);
|
|
2488
|
-
}),
|
|
2489
|
-
pullFrames: trpc_middleware_js_1.protectedProcedure
|
|
2490
|
-
.input(types_42.decoderCapability.methods.pullFrames.input.loose())
|
|
2491
|
-
.output(types_42.decoderCapability.methods.pullFrames.output)
|
|
2492
|
-
.query(async ({ input, ctx }) => {
|
|
2493
|
-
const { nodeId, ...methodInput } = input;
|
|
2494
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2495
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2496
|
-
return p.pullFrames(methodInput);
|
|
2497
|
-
}),
|
|
2498
|
-
pullHandles: trpc_middleware_js_1.protectedProcedure
|
|
2499
|
-
.input(types_42.decoderCapability.methods.pullHandles.input.loose())
|
|
2500
|
-
.output(types_42.decoderCapability.methods.pullHandles.output)
|
|
2501
|
-
.query(async ({ input, ctx }) => {
|
|
2502
|
-
const { nodeId, ...methodInput } = input;
|
|
2503
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2504
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2505
|
-
return p.pullHandles(methodInput);
|
|
2506
|
-
}),
|
|
2507
|
-
getFrame: trpc_middleware_js_1.protectedProcedure
|
|
2508
|
-
.input(types_42.decoderCapability.methods.getFrame.input.loose())
|
|
2509
|
-
.output(types_42.decoderCapability.methods.getFrame.output)
|
|
2510
|
-
.query(async ({ input, ctx }) => {
|
|
2511
|
-
const { nodeId, ...methodInput } = input;
|
|
2512
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2513
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2514
|
-
return p.getFrame(methodInput);
|
|
2515
|
-
}),
|
|
2516
|
-
getShmStats: trpc_middleware_js_1.protectedProcedure
|
|
2517
|
-
.input(types_42.decoderCapability.methods.getShmStats.input.loose())
|
|
2518
|
-
.output(types_42.decoderCapability.methods.getShmStats.output)
|
|
2519
|
-
.query(async ({ input, ctx }) => {
|
|
2520
|
-
const { nodeId, ...methodInput } = input;
|
|
2521
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2522
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2523
|
-
return p.getShmStats(methodInput);
|
|
2524
|
-
}),
|
|
2525
|
-
updateConfig: trpc_middleware_js_1.protectedProcedure
|
|
2526
|
-
.input(types_42.decoderCapability.methods.updateConfig.input.loose())
|
|
2527
|
-
.output(types_42.decoderCapability.methods.updateConfig.output)
|
|
2528
|
-
.query(async ({ input, ctx }) => {
|
|
2529
|
-
const { nodeId, ...methodInput } = input;
|
|
2530
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2531
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2532
|
-
return p.updateConfig(methodInput);
|
|
2533
|
-
}),
|
|
2534
|
-
getStats: trpc_middleware_js_1.protectedProcedure
|
|
2535
|
-
.input(types_42.decoderCapability.methods.getStats.input.loose())
|
|
2536
|
-
.output(types_42.decoderCapability.methods.getStats.output)
|
|
2537
|
-
.query(async ({ input, ctx }) => {
|
|
2538
|
-
const { nodeId, ...methodInput } = input;
|
|
2539
|
-
const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2540
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2541
|
-
return p.getStats(methodInput);
|
|
2542
|
-
}),
|
|
2543
|
-
listActiveSessions: trpc_middleware_js_1.protectedProcedure
|
|
2544
|
-
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
2545
|
-
.output(types_42.decoderCapability.methods.listActiveSessions.output)
|
|
2546
|
-
.query(async ({ input, ctx }) => {
|
|
2547
|
-
const p = resolveProvider('decoder', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2548
|
-
return p.listActiveSessions();
|
|
2549
|
-
}),
|
|
2550
2452
|
reprobeHwaccel: trpc_middleware_js_1.adminProcedure
|
|
2551
2453
|
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
2552
2454
|
.output(types_42.decoderCapability.methods.reprobeHwaccel.output)
|
|
@@ -10255,33 +10157,6 @@ function createCapRouter_streamBroker(getProvider, createRemoteProxy) {
|
|
|
10255
10157
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10256
10158
|
return p.unsubscribeAudioChunks(methodInput);
|
|
10257
10159
|
}),
|
|
10258
|
-
subscribeFrames: trpc_middleware_js_1.protectedProcedure
|
|
10259
|
-
.input(types_118.streamBrokerCapability.methods.subscribeFrames.input.loose())
|
|
10260
|
-
.output(types_118.streamBrokerCapability.methods.subscribeFrames.output)
|
|
10261
|
-
.mutation(async ({ input, ctx }) => {
|
|
10262
|
-
const { nodeId, ...methodInput } = input;
|
|
10263
|
-
const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
10264
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10265
|
-
return p.subscribeFrames(methodInput);
|
|
10266
|
-
}),
|
|
10267
|
-
pullFrameHandles: trpc_middleware_js_1.protectedProcedure
|
|
10268
|
-
.input(types_118.streamBrokerCapability.methods.pullFrameHandles.input.loose())
|
|
10269
|
-
.output(types_118.streamBrokerCapability.methods.pullFrameHandles.output)
|
|
10270
|
-
.query(async ({ input, ctx }) => {
|
|
10271
|
-
const { nodeId, ...methodInput } = input;
|
|
10272
|
-
const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
10273
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10274
|
-
return p.pullFrameHandles(methodInput);
|
|
10275
|
-
}),
|
|
10276
|
-
unsubscribeFrames: trpc_middleware_js_1.protectedProcedure
|
|
10277
|
-
.input(types_118.streamBrokerCapability.methods.unsubscribeFrames.input.loose())
|
|
10278
|
-
.output(types_118.streamBrokerCapability.methods.unsubscribeFrames.output)
|
|
10279
|
-
.mutation(async ({ input, ctx }) => {
|
|
10280
|
-
const { nodeId, ...methodInput } = input;
|
|
10281
|
-
const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
10282
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10283
|
-
return p.unsubscribeFrames(methodInput);
|
|
10284
|
-
}),
|
|
10285
10160
|
setPreBufferDuration: trpc_middleware_js_1.adminProcedure
|
|
10286
10161
|
.input(types_118.streamBrokerCapability.methods.setPreBufferDuration.input.loose())
|
|
10287
10162
|
.output(types_118.streamBrokerCapability.methods.setPreBufferDuration.output)
|
|
@@ -170,7 +170,7 @@ function buildServerProviders(services) {
|
|
|
170
170
|
nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry, (0, cap_providers_js_1.createNodeRootPackageLookup)(services.moleculer, services.serverUpdateService), services.loggingService.createLogger('nodes'))),
|
|
171
171
|
// server-management — runtime-updatable root package (phase 1: hub only).
|
|
172
172
|
'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService)),
|
|
173
|
-
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx)),
|
|
173
|
+
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.agentRegistry)),
|
|
174
174
|
// webrtc-session — the resolved broker singleton, wrapped per request to
|
|
175
175
|
// inject the server-read User-Agent into signaling attribution (the broker
|
|
176
176
|
// is a forked addon that can't see the HTTP request). Cross-node (agent-
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PackageProtectedError = void 0;
|
|
4
|
+
exports.isHubNodeId = isHubNodeId;
|
|
5
|
+
exports.uninstallPackage = uninstallPackage;
|
|
6
|
+
exports.listInstallations = listInstallations;
|
|
7
|
+
/** The hub refuses before touching any node. Mapped to FORBIDDEN by the router. */
|
|
8
|
+
class PackageProtectedError extends Error {
|
|
9
|
+
constructor(packageName) {
|
|
10
|
+
super(`Package ${packageName} is required and cannot be uninstalled`);
|
|
11
|
+
this.name = 'PackageProtectedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.PackageProtectedError = PackageProtectedError;
|
|
15
|
+
/** A node id that refers to the hub itself (top-level or a hub group runner). */
|
|
16
|
+
function isHubNodeId(nodeId) {
|
|
17
|
+
return nodeId === 'hub' || nodeId.startsWith('hub/');
|
|
18
|
+
}
|
|
19
|
+
const OK_OUTCOMES = new Set([
|
|
20
|
+
'uninstalled',
|
|
21
|
+
'not-installed',
|
|
22
|
+
'pending-reconnect',
|
|
23
|
+
]);
|
|
24
|
+
function errMessage(err) {
|
|
25
|
+
return err instanceof Error ? err.message : String(err);
|
|
26
|
+
}
|
|
27
|
+
function nodeResult(nodeId, outcome, extra = {}) {
|
|
28
|
+
return {
|
|
29
|
+
nodeId,
|
|
30
|
+
outcome,
|
|
31
|
+
addonIds: extra.addonIds ?? [],
|
|
32
|
+
runnersStopped: extra.runnersStopped ?? [],
|
|
33
|
+
warnings: extra.warnings ?? [],
|
|
34
|
+
...(extra.error !== undefined ? { error: extra.error } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function stringArray(value) {
|
|
38
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
39
|
+
}
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null;
|
|
42
|
+
}
|
|
43
|
+
function parseUndeployReply(reply) {
|
|
44
|
+
if (!isRecord(reply)) {
|
|
45
|
+
return {
|
|
46
|
+
packageScoped: false,
|
|
47
|
+
outcome: null,
|
|
48
|
+
reason: undefined,
|
|
49
|
+
addonIds: [],
|
|
50
|
+
runnersStopped: null,
|
|
51
|
+
warnings: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const outcome = reply['outcome'];
|
|
55
|
+
return {
|
|
56
|
+
packageScoped: reply['scope'] === 'package',
|
|
57
|
+
outcome: outcome === 'uninstalled' || outcome === 'not-installed' || outcome === 'refused'
|
|
58
|
+
? outcome
|
|
59
|
+
: null,
|
|
60
|
+
reason: typeof reply['reason'] === 'string' ? reply['reason'] : undefined,
|
|
61
|
+
addonIds: stringArray(reply['addonIds']),
|
|
62
|
+
runnersStopped: Array.isArray(reply['runnersStopped'])
|
|
63
|
+
? stringArray(reply['runnersStopped'])
|
|
64
|
+
: null,
|
|
65
|
+
warnings: stringArray(reply['warnings']),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* An agent built before package mode answers `{ success, addonId }` and removes
|
|
70
|
+
* ONE addon — its folder, not its runner. Its packages are still removed (one
|
|
71
|
+
* call per addon id), and the result says the runner outlives it.
|
|
72
|
+
*/
|
|
73
|
+
const LEGACY_AGENT_WARNING = 'this agent predates runner teardown: the addon is removed from disk but its runner keeps running until the agent restarts — update the agent';
|
|
74
|
+
async function uninstallFromOnlineAgent(seam, nodeId, packageName) {
|
|
75
|
+
let listed;
|
|
76
|
+
try {
|
|
77
|
+
listed = (await seam.agentAddons(nodeId))
|
|
78
|
+
.filter((a) => a.packageName === packageName)
|
|
79
|
+
.map((a) => a.id);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return nodeResult(nodeId, 'failed', { error: `status read failed: ${errMessage(err)}` });
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
// `addonId` is for an agent that predates package mode: it reads only that
|
|
86
|
+
// field. With no listed addon, the package name doubles as the dir to drop.
|
|
87
|
+
const first = parseUndeployReply(await seam.agentUndeploy(nodeId, { packageName, addonId: listed[0] ?? packageName }));
|
|
88
|
+
if (first.packageScoped && first.outcome !== null) {
|
|
89
|
+
if (first.outcome === 'refused') {
|
|
90
|
+
return nodeResult(nodeId, 'refused', { error: first.reason, addonIds: first.addonIds });
|
|
91
|
+
}
|
|
92
|
+
await seam.notePackageUndeployed(nodeId, packageName, first.addonIds);
|
|
93
|
+
return nodeResult(nodeId, first.outcome, {
|
|
94
|
+
addonIds: first.addonIds,
|
|
95
|
+
runnersStopped: first.runnersStopped ?? [],
|
|
96
|
+
warnings: first.warnings,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const addonId of listed.slice(1)) {
|
|
100
|
+
await seam.agentUndeploy(nodeId, { packageName, addonId });
|
|
101
|
+
}
|
|
102
|
+
await seam.notePackageUndeployed(nodeId, packageName, listed);
|
|
103
|
+
return nodeResult(nodeId, listed.length > 0 ? 'uninstalled' : 'not-installed', {
|
|
104
|
+
addonIds: listed,
|
|
105
|
+
warnings: listed.length > 0 ? [LEGACY_AGENT_WARNING] : [],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return nodeResult(nodeId, 'failed', { addonIds: listed, error: errMessage(err) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function uninstallFromAgent(seam, node, packageName) {
|
|
113
|
+
if (node.isOnline)
|
|
114
|
+
return uninstallFromOnlineAgent(seam, node.id, packageName);
|
|
115
|
+
// Offline: nothing can be called. Pruning the record is what keeps the
|
|
116
|
+
// back-fill from returning it; the copy itself is the stale sweep's on rejoin.
|
|
117
|
+
try {
|
|
118
|
+
await seam.notePackageUndeployed(node.id, packageName, []);
|
|
119
|
+
return nodeResult(node.id, 'pending-reconnect', {
|
|
120
|
+
warnings: [
|
|
121
|
+
'node is offline — its copy is removed when it reconnects, provided the hub no longer installs the package',
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return nodeResult(node.id, 'failed', { error: errMessage(err) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function uninstallFromHub(seam, packageName) {
|
|
130
|
+
const row = seam.listHubPackages().find((p) => p.name === packageName);
|
|
131
|
+
if (!row)
|
|
132
|
+
return nodeResult('hub', 'not-installed');
|
|
133
|
+
try {
|
|
134
|
+
await seam.uninstallOnHub(packageName);
|
|
135
|
+
return nodeResult('hub', 'uninstalled', { addonIds: row.addonIds });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
return nodeResult('hub', 'failed', { addonIds: row.addonIds, error: errMessage(err) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function summarize(packageName, results) {
|
|
142
|
+
return { success: results.every((r) => OK_OUTCOMES.has(r.outcome)), packageName, results };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Agents that have (or are recorded to have) the package. An online agent
|
|
146
|
+
* whose status read fails is still targeted — the undeploy leg reports the
|
|
147
|
+
* failure for that node rather than silently skipping it.
|
|
148
|
+
*/
|
|
149
|
+
async function agentsHolding(seam, packageName) {
|
|
150
|
+
const agents = await seam.listAgents();
|
|
151
|
+
const holding = await Promise.all(agents.map(async (node) => {
|
|
152
|
+
if (node.isOnline) {
|
|
153
|
+
try {
|
|
154
|
+
const addons = await seam.agentAddons(node.id);
|
|
155
|
+
if (addons.some((a) => a.packageName === packageName))
|
|
156
|
+
return node;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return node;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const recorded = await seam.recordedPackages(node.id).catch(() => []);
|
|
163
|
+
return recorded.some((p) => p.name === packageName) ? node : null;
|
|
164
|
+
}));
|
|
165
|
+
return holding.filter((n) => n !== null);
|
|
166
|
+
}
|
|
167
|
+
async function uninstallPackage(seam, request) {
|
|
168
|
+
const { packageName } = request;
|
|
169
|
+
if (seam.isProtected(packageName))
|
|
170
|
+
throw new PackageProtectedError(packageName);
|
|
171
|
+
if (request.everywhere === true) {
|
|
172
|
+
const agents = await agentsHolding(seam, packageName);
|
|
173
|
+
// Agents first, hub last — the hub routes every call to the agents, and a
|
|
174
|
+
// hub uninstall that fails must not leave the agents' records already
|
|
175
|
+
// pruned for nothing: each leg records only its own success.
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const node of agents)
|
|
178
|
+
results.push(await uninstallFromAgent(seam, node, packageName));
|
|
179
|
+
results.push(await uninstallFromHub(seam, packageName));
|
|
180
|
+
return summarize(packageName, results);
|
|
181
|
+
}
|
|
182
|
+
const nodeId = request.nodeId;
|
|
183
|
+
if (nodeId === undefined || isHubNodeId(nodeId)) {
|
|
184
|
+
// The original contract: hub-only, and a failure THROWS.
|
|
185
|
+
await seam.uninstallOnHub(packageName);
|
|
186
|
+
const row = seam.listHubPackages().find((p) => p.name === packageName);
|
|
187
|
+
return summarize(packageName, [
|
|
188
|
+
nodeResult('hub', 'uninstalled', { addonIds: row?.addonIds ?? [] }),
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
const node = (await seam.listAgents()).find((n) => n.id === nodeId);
|
|
192
|
+
if (!node) {
|
|
193
|
+
return summarize(packageName, [
|
|
194
|
+
nodeResult(nodeId, 'failed', { error: `unknown node ${nodeId}` }),
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
return summarize(packageName, [await uninstallFromAgent(seam, node, packageName)]);
|
|
198
|
+
}
|
|
199
|
+
async function listInstallations(seam, packageName) {
|
|
200
|
+
const wanted = (name) => packageName === undefined || name === packageName;
|
|
201
|
+
const hubPackages = seam.listHubPackages();
|
|
202
|
+
const onHub = new Set(hubPackages.map((p) => p.name));
|
|
203
|
+
const installations = hubPackages
|
|
204
|
+
.filter((p) => wanted(p.name))
|
|
205
|
+
.map((p) => ({
|
|
206
|
+
packageName: p.name,
|
|
207
|
+
nodeId: 'hub',
|
|
208
|
+
nodeName: 'Hub',
|
|
209
|
+
isHub: true,
|
|
210
|
+
isOnline: true,
|
|
211
|
+
version: p.version,
|
|
212
|
+
addonIds: p.addonIds,
|
|
213
|
+
source: 'live',
|
|
214
|
+
onHub: true,
|
|
215
|
+
}));
|
|
216
|
+
const unreachable = [];
|
|
217
|
+
const perAgent = await Promise.all((await seam.listAgents()).map(async (node) => {
|
|
218
|
+
const base = { nodeId: node.id, nodeName: node.name, isHub: false, isOnline: node.isOnline };
|
|
219
|
+
if (node.isOnline) {
|
|
220
|
+
try {
|
|
221
|
+
const byPackage = new Map();
|
|
222
|
+
for (const addon of await seam.agentAddons(node.id)) {
|
|
223
|
+
if (addon.packageName === undefined || !wanted(addon.packageName))
|
|
224
|
+
continue;
|
|
225
|
+
const row = byPackage.get(addon.packageName) ?? {
|
|
226
|
+
version: addon.version ?? null,
|
|
227
|
+
addonIds: [],
|
|
228
|
+
};
|
|
229
|
+
row.addonIds.push(addon.id);
|
|
230
|
+
byPackage.set(addon.packageName, row);
|
|
231
|
+
}
|
|
232
|
+
return [...byPackage].map(([name, row]) => ({
|
|
233
|
+
...base,
|
|
234
|
+
packageName: name,
|
|
235
|
+
version: row.version,
|
|
236
|
+
addonIds: row.addonIds,
|
|
237
|
+
source: 'live',
|
|
238
|
+
onHub: onHub.has(name),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
unreachable.push({ nodeId: node.id, error: errMessage(err) });
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const recorded = await seam.recordedPackages(node.id).catch(() => []);
|
|
247
|
+
return recorded
|
|
248
|
+
.filter((p) => wanted(p.name))
|
|
249
|
+
.map((p) => ({
|
|
250
|
+
...base,
|
|
251
|
+
packageName: p.name,
|
|
252
|
+
version: p.version,
|
|
253
|
+
addonIds: [],
|
|
254
|
+
source: 'recorded',
|
|
255
|
+
onHub: onHub.has(p.name),
|
|
256
|
+
}));
|
|
257
|
+
}));
|
|
258
|
+
return { installations: [...installations, ...perAgent.flat()], unreachable };
|
|
259
|
+
}
|
|
@@ -50,11 +50,11 @@ const types_1 = require("@camstack/types");
|
|
|
50
50
|
*
|
|
51
51
|
* What's left is ONLY the addon package management surface: the installer
|
|
52
52
|
* + the loader + `reloadPackages` + `listAvailableAddons`. These are still
|
|
53
|
-
* used by
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* used by `server/backend/src/api/addon-upload.ts` (multipart upload →
|
|
54
|
+
* install → loader reload). The `bridge-addons.router.ts` that also used it
|
|
55
|
+
* was never mounted and was deleted on 2026-10-08 — its hard-coded REQUIRED
|
|
56
|
+
* set read like the uninstall guard and was not; uninstall is the `addons`
|
|
57
|
+
* cap's (`core/addon/addon-uninstall.ts`).
|
|
58
58
|
*/
|
|
59
59
|
class AddonBridgeService {
|
|
60
60
|
loggingService;
|
|
@@ -24,13 +24,20 @@
|
|
|
24
24
|
* both mutations are attributable actions rather than heuristics.
|
|
25
25
|
*
|
|
26
26
|
* THE INTENDED VERSION
|
|
27
|
-
* The hub's own installed version
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
27
|
+
* The hub's own installed version, and only that. Never `latest` — resolving
|
|
28
|
+
* `latest` per node is exactly the silent per-node drift the bootstrap
|
|
29
|
+
* installer already suffers from (`addon-installer.ts` installs unpinned and
|
|
30
|
+
* `ensureRequiredPackages` never re-installs, so whatever `latest` meant at a
|
|
31
|
+
* node's first boot is what that node runs forever).
|
|
32
|
+
*
|
|
33
|
+
* A package the hub does NOT install is never delivered, whatever the node last
|
|
34
|
+
* reported. It used to fall back to the node's last-reported version, which
|
|
35
|
+
* made the back-fill the one path that could put an UNINSTALLED addon back: an
|
|
36
|
+
* operator removes `@camstack/addon-decoder-ffmpeg` from the hub and moves the
|
|
37
|
+
* agent's copy aside, and the next reconnect reinstalled it from npm at the
|
|
38
|
+
* version the node last ran — while the same pass's stale sweep exists to
|
|
39
|
+
* remove exactly such an addon. The two directions disagreed; now the hub's
|
|
40
|
+
* install set bounds both. Such packages are reported as `notOnHub`.
|
|
34
41
|
*
|
|
35
42
|
* BOUNDING
|
|
36
43
|
* Delivery failures are counted per `(node, package, version)` and the pair is
|
|
@@ -66,7 +73,7 @@ function computeBackfillPlan(input) {
|
|
|
66
73
|
const liveNames = new Set(input.live.map((p) => p.name));
|
|
67
74
|
const targets = [];
|
|
68
75
|
const retired = [];
|
|
69
|
-
const
|
|
76
|
+
const notOnHub = [];
|
|
70
77
|
for (const entry of input.recorded) {
|
|
71
78
|
if (liveNames.has(entry.name))
|
|
72
79
|
continue;
|
|
@@ -79,18 +86,13 @@ function computeBackfillPlan(input) {
|
|
|
79
86
|
continue;
|
|
80
87
|
}
|
|
81
88
|
const hubVersion = input.hubVersions.get(entry.name);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
unresolvable.push(entry.name);
|
|
89
|
+
if (hubVersion === undefined) {
|
|
90
|
+
notOnHub.push(entry.name);
|
|
85
91
|
continue;
|
|
86
92
|
}
|
|
87
|
-
targets.push({
|
|
88
|
-
name: entry.name,
|
|
89
|
-
version,
|
|
90
|
-
versionSource: hubVersion === undefined ? 'node-last-reported' : 'hub-installed',
|
|
91
|
-
});
|
|
93
|
+
targets.push({ name: entry.name, version: hubVersion, versionSource: 'hub-installed' });
|
|
92
94
|
}
|
|
93
|
-
return { targets, retired,
|
|
95
|
+
return { targets, retired, notOnHub, liveCount: liveNames.size };
|
|
94
96
|
}
|
|
95
97
|
function confirmBackfillPlan(plan, secondRead) {
|
|
96
98
|
const present = new Set(secondRead.map((p) => p.name));
|
|
@@ -187,5 +189,5 @@ async function executeBackfill(seam, nodeId, plan, tracker) {
|
|
|
187
189
|
if (delivered.length > 0) {
|
|
188
190
|
await seam.reload(nodeId);
|
|
189
191
|
}
|
|
190
|
-
return { delivered, failed, skipped: plan.retired,
|
|
192
|
+
return { delivered, failed, skipped: plan.retired, notOnHub: plan.notOnHub };
|
|
191
193
|
}
|
|
@@ -281,6 +281,32 @@ class AgentRegistryService {
|
|
|
281
281
|
return;
|
|
282
282
|
await this.historyStore?.prunePackages(nodeId, [packageName]);
|
|
283
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The operator removed a whole PACKAGE from a node (`addons.uninstallPackage`
|
|
286
|
+
* with a `nodeId`, or `everywhere`). Same intent as
|
|
287
|
+
* {@link noteAddonUndeployed}, keyed by package — which is what an uninstall
|
|
288
|
+
* names, and the only key that still resolves once the hub itself no longer
|
|
289
|
+
* has the package. Emits one `AddonUninstalled` per addon removed, like the
|
|
290
|
+
* stale-addon reconcile does.
|
|
291
|
+
*/
|
|
292
|
+
async notePackageUndeployed(nodeId, packageName, addonIds) {
|
|
293
|
+
if (AGENT_BOOTSTRAP_PACKAGES.has(packageName))
|
|
294
|
+
return;
|
|
295
|
+
await this.historyStore?.prunePackages(nodeId, [packageName]);
|
|
296
|
+
for (const addonId of addonIds) {
|
|
297
|
+
this.eventBus.emit({
|
|
298
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
299
|
+
timestamp: new Date(),
|
|
300
|
+
source: { type: 'core', id: 'agent-registry' },
|
|
301
|
+
category: types_1.EventCategory.AddonUninstalled,
|
|
302
|
+
data: { addonId, agentId: nodeId, packageName, reason: 'operator uninstall' },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** The packages a node last declared (its intended set). `[]` with no store. */
|
|
307
|
+
async getRecordedPackages(nodeId) {
|
|
308
|
+
return this.historyStore ? this.historyStore.getPackages(nodeId) : [];
|
|
309
|
+
}
|
|
284
310
|
/** Map an addon declaration id to the package that ships it, via the hub registry. */
|
|
285
311
|
resolvePackageForAddon(addonId) {
|
|
286
312
|
if (!this.addonRegistry)
|
|
@@ -744,9 +770,14 @@ class AgentRegistryService {
|
|
|
744
770
|
retired: this.backfillTracker.retiredFor(agentId),
|
|
745
771
|
hubVersions,
|
|
746
772
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
773
|
+
// Never a delivery, and said so: an addon the hub no longer installs stays
|
|
774
|
+
// off the node even though the node once declared it (the stale sweep above
|
|
775
|
+
// removes a copy that is still there).
|
|
776
|
+
if (planned.notOnHub.length > 0) {
|
|
777
|
+
console.log(`[agent-registry] Back-fill ${agentId}: NOT delivering ${planned.notOnHub.join(', ')} — ` +
|
|
778
|
+
`in the node's declared roster but not installed on the hub`);
|
|
779
|
+
}
|
|
780
|
+
if (planned.targets.length === 0 && planned.retired.length === 0) {
|
|
750
781
|
return;
|
|
751
782
|
}
|
|
752
783
|
// GATE 1 — the trigger allowlist. A pass that must not install says so and
|
|
@@ -773,7 +804,7 @@ class AgentRegistryService {
|
|
|
773
804
|
const plan = await this.confirmBackfillTargets(agentId, planned);
|
|
774
805
|
if (plan === null)
|
|
775
806
|
return;
|
|
776
|
-
if (plan.targets.length === 0 && plan.retired.length === 0
|
|
807
|
+
if (plan.targets.length === 0 && plan.retired.length === 0) {
|
|
777
808
|
return;
|
|
778
809
|
}
|
|
779
810
|
// Hold the delivery window open for the whole pass, so the reload this
|
|
@@ -813,9 +844,6 @@ class AgentRegistryService {
|
|
|
813
844
|
for (const name of outcome.skipped) {
|
|
814
845
|
console.error(`[agent-registry] Back-fill ${agentId}: ${name} is still missing and RETIRED — the node is degraded until an operator intervenes`);
|
|
815
846
|
}
|
|
816
|
-
for (const name of outcome.unresolvable) {
|
|
817
|
-
console.error(`[agent-registry] Back-fill ${agentId}: ${name} is missing but no version could be resolved (not installed on the hub, no version recorded) — not guessing "latest"`);
|
|
818
|
-
}
|
|
819
847
|
}
|
|
820
848
|
/**
|
|
821
849
|
* Re-read the node's roster and drop every target the second read reports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.478",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.build.json && tsup && node scripts/copy-closure-patches.mjs",
|
|
20
20
|
"postinstall": "node scripts/closure-postinstall.mjs",
|
|
21
|
-
"dev": "npx concurrently -n server,ui -c blue,magenta \"CAMSTACK_RESTART_TOUCH_FILE=src/launcher.ts tsx watch --env-file=.env --ignore=./camstack-data --ignore=../../packages/core/dist --ignore=../../packages/sdk/dist --ignore=../../packages/ui-library/dist
|
|
22
|
-
"serve": "CAMSTACK_RESTART_TOUCH_FILE=src/launcher.ts tsx watch --env-file=.env --ignore=./camstack-data --ignore=../../packages/core/dist --ignore=../../packages/sdk/dist --ignore=../../packages/ui-library/dist
|
|
21
|
+
"dev": "npx concurrently -n server,ui -c blue,magenta \"CAMSTACK_RESTART_TOUCH_FILE=src/launcher.ts tsx watch --env-file=.env --ignore=./camstack-data --ignore=../../packages/core/dist --ignore=../../packages/sdk/dist --ignore=../../packages/ui-library/dist src/launcher.ts\" \"cd ../../packages/addon-admin-ui && npx vite --port 3001\"",
|
|
22
|
+
"serve": "CAMSTACK_RESTART_TOUCH_FILE=src/launcher.ts tsx watch --env-file=.env --ignore=./camstack-data --ignore=../../packages/core/dist --ignore=../../packages/sdk/dist --ignore=../../packages/ui-library/dist src/launcher.ts",
|
|
23
23
|
"serve:once": "tsx --env-file=.env src/launcher.ts",
|
|
24
24
|
"start": "node --env-file=.env dist/launcher.js",
|
|
25
25
|
"typecheck": "tsc --noEmit -p tsconfig.spec.json",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
41
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
40
|
+
"@camstack/addon-admin-ui": "1.2.351",
|
|
41
|
+
"@camstack/addon-agent-ui": "1.2.190",
|
|
42
42
|
"@camstack/addon-auth": "1.2.130",
|
|
43
|
-
"@camstack/addon-decoder-nodeav": "1.2.
|
|
43
|
+
"@camstack/addon-decoder-nodeav": "1.2.121",
|
|
44
44
|
"@camstack/addon-notifiers": "1.2.128",
|
|
45
|
-
"@camstack/addon-pipeline": "1.2.
|
|
46
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
47
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
48
|
-
"@camstack/sdk": "1.2.
|
|
49
|
-
"@camstack/system": "1.2.
|
|
50
|
-
"@camstack/types": "1.2.
|
|
51
|
-
"@camstack/ui-library": "1.2.
|
|
45
|
+
"@camstack/addon-pipeline": "1.2.344",
|
|
46
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.297",
|
|
47
|
+
"@camstack/addon-post-analysis": "1.2.342",
|
|
48
|
+
"@camstack/sdk": "1.2.126",
|
|
49
|
+
"@camstack/system": "1.2.373",
|
|
50
|
+
"@camstack/types": "1.2.324",
|
|
51
|
+
"@camstack/ui-library": "1.2.248",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|
|
53
53
|
"@fastify/cookie": "^11.0.2",
|
|
54
54
|
"@fastify/cors": "^11.2.0",
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createBridgeAddonsRouter = createBridgeAddonsRouter;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const server_1 = require("@trpc/server");
|
|
6
|
-
const trpc_middleware_1 = require("./trpc/trpc.middleware");
|
|
7
|
-
function createBridgeAddonsRouter(bridge, addonSearch, addonRegistry, toastService) {
|
|
8
|
-
return (0, trpc_middleware_1.trpcRouter)({
|
|
9
|
-
/** List all addon packages installed in the addons directory */
|
|
10
|
-
listPackages: trpc_middleware_1.protectedProcedure.query(() => {
|
|
11
|
-
const installer = bridge.getInstaller();
|
|
12
|
-
if (!installer)
|
|
13
|
-
return [];
|
|
14
|
-
return installer.listInstalled();
|
|
15
|
-
}),
|
|
16
|
-
/** List all available addons across all loaded packages */
|
|
17
|
-
listAddons: trpc_middleware_1.protectedProcedure.query(() => {
|
|
18
|
-
return bridge.listAvailableAddons().map((id) => {
|
|
19
|
-
const addon = bridge.getLoader().getAddon(id);
|
|
20
|
-
return {
|
|
21
|
-
id,
|
|
22
|
-
packageName: addon?.packageName ?? 'unknown',
|
|
23
|
-
slot: addon?.declaration.slot ?? null,
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
}),
|
|
27
|
-
/** Install a community addon package from npm */
|
|
28
|
-
installPackage: trpc_middleware_1.adminProcedure
|
|
29
|
-
.input(zod_1.z.object({ packageName: zod_1.z.string(), version: zod_1.z.string().optional() }))
|
|
30
|
-
.mutation(async ({ input }) => {
|
|
31
|
-
const installer = bridge.getInstaller();
|
|
32
|
-
if (!installer) {
|
|
33
|
-
throw new server_1.TRPCError({
|
|
34
|
-
code: 'PRECONDITION_FAILED',
|
|
35
|
-
message: 'Addon installer not available — bridge may have failed to initialize',
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
await installer.install(input.packageName, input.version);
|
|
39
|
-
await bridge.reloadPackages();
|
|
40
|
-
const result = addonRegistry
|
|
41
|
-
? await addonRegistry.loadNewAddons()
|
|
42
|
-
: { loaded: [], failed: [] };
|
|
43
|
-
toastService?.broadcast({
|
|
44
|
-
title: 'Addon Installed',
|
|
45
|
-
message: `${input.packageName} installed successfully${result.loaded.length ? ` (${result.loaded.join(', ')})` : ''}`,
|
|
46
|
-
severity: result.failed.length ? 'warning' : 'info',
|
|
47
|
-
});
|
|
48
|
-
return { success: true, loaded: result.loaded, failed: result.failed };
|
|
49
|
-
}),
|
|
50
|
-
/** Uninstall a community addon package */
|
|
51
|
-
uninstallPackage: trpc_middleware_1.adminProcedure
|
|
52
|
-
.input(zod_1.z.object({ packageName: zod_1.z.string() }))
|
|
53
|
-
.mutation(async ({ input }) => {
|
|
54
|
-
// Server-side guard: prevent uninstalling required packages.
|
|
55
|
-
// After Phase D bundle merge, the pipeline-related packages
|
|
56
|
-
// (stream-broker, detection-pipeline, motion-wasm, decoders,
|
|
57
|
-
// audio) all live in @camstack/addon-pipeline.
|
|
58
|
-
const REQUIRED = new Set([
|
|
59
|
-
'@camstack/system',
|
|
60
|
-
'@camstack/addon-pipeline',
|
|
61
|
-
'@camstack/addon-pipeline-orchestrator',
|
|
62
|
-
'@camstack/addon-post-analysis',
|
|
63
|
-
'@camstack/addon-admin-ui',
|
|
64
|
-
]);
|
|
65
|
-
if (REQUIRED.has(input.packageName)) {
|
|
66
|
-
throw new server_1.TRPCError({
|
|
67
|
-
code: 'FORBIDDEN',
|
|
68
|
-
message: `Package ${input.packageName} is required and cannot be uninstalled`,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
const installer = bridge.getInstaller();
|
|
72
|
-
if (!installer) {
|
|
73
|
-
throw new server_1.TRPCError({
|
|
74
|
-
code: 'PRECONDITION_FAILED',
|
|
75
|
-
message: 'Addon installer not available — bridge may have failed to initialize',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
await installer.uninstall(input.packageName);
|
|
79
|
-
await bridge.reloadPackages();
|
|
80
|
-
if (addonRegistry)
|
|
81
|
-
await addonRegistry.loadNewAddons();
|
|
82
|
-
toastService?.broadcast({
|
|
83
|
-
title: 'Addon Uninstalled',
|
|
84
|
-
message: `${input.packageName} has been removed`,
|
|
85
|
-
severity: 'info',
|
|
86
|
-
});
|
|
87
|
-
return { success: true };
|
|
88
|
-
}),
|
|
89
|
-
/** Force reload all addon packages (re-scan directories, re-import modules) */
|
|
90
|
-
reloadPackages: trpc_middleware_1.adminProcedure.mutation(async () => {
|
|
91
|
-
await bridge.reloadPackages();
|
|
92
|
-
return { success: true, message: 'Addon packages reloaded' };
|
|
93
|
-
}),
|
|
94
|
-
/** Search npm for available CamStack addons */
|
|
95
|
-
searchAvailable: trpc_middleware_1.protectedProcedure
|
|
96
|
-
.input(zod_1.z.object({ query: zod_1.z.string().optional() }).optional())
|
|
97
|
-
.query(async ({ input }) => {
|
|
98
|
-
const results = await addonSearch.searchAddons(input?.query);
|
|
99
|
-
// Enrich with install status from locally installed packages
|
|
100
|
-
const installed = bridge.getInstaller()?.listInstalled() ?? [];
|
|
101
|
-
const installedMap = new Map(installed.map((p) => [p.name, p.version]));
|
|
102
|
-
return results.map((r) => ({
|
|
103
|
-
...r,
|
|
104
|
-
installed: installedMap.has(r.name),
|
|
105
|
-
installedVersion: installedMap.get(r.name),
|
|
106
|
-
}));
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
}
|