@camstack/server 1.2.477 → 1.2.479
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 +22 -2
- 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
|
@@ -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)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
|
|
4
4
|
exports.enrichInputWithRelayClass = enrichInputWithRelayClass;
|
|
5
|
+
exports.stripTalkbackFromRequest = stripTalkbackFromRequest;
|
|
5
6
|
exports.enrichHintsWithClientClass = enrichHintsWithClientClass;
|
|
6
7
|
exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
|
|
7
8
|
exports.buildAppRouter = buildAppRouter;
|
|
@@ -95,6 +96,25 @@ function enrichInputWithRelayClass(input, clientClass) {
|
|
|
95
96
|
const relayOnly = FORCE_RELAY_REMOTE ? (0, client_ip_js_1.deriveRelayOnly)(clientClass) : false;
|
|
96
97
|
return { ...input, relayOnly };
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Strip `talkback` from a REQUEST-bound `handleOffer` (D771).
|
|
101
|
+
*
|
|
102
|
+
* Talk-back sends the peer's microphone to the camera's speaker. Only a
|
|
103
|
+
* trusted, mesh-originated caller may ask for it — today the Alexa export,
|
|
104
|
+
* which has already checked the linked user may control the camera. A
|
|
105
|
+
* browser's talk permission lives on the intercom cap's own surface (share
|
|
106
|
+
* tokens gate it there); letting a request-bound `handleOffer` set this flag
|
|
107
|
+
* would be a second door to the speaker. Same rule as `relayOnly`: a
|
|
108
|
+
* mesh-originated call (`clientClass === null`) passes through unchanged.
|
|
109
|
+
*
|
|
110
|
+
* Immutable — builds a NEW input object, never mutates the caller's.
|
|
111
|
+
*/
|
|
112
|
+
function stripTalkbackFromRequest(input, clientClass) {
|
|
113
|
+
if (clientClass === null || input.talkback === undefined)
|
|
114
|
+
return input;
|
|
115
|
+
const { talkback: _dropped, ...rest } = input;
|
|
116
|
+
return rest;
|
|
117
|
+
}
|
|
98
118
|
/**
|
|
99
119
|
* Bias used to live here (`prefersTier: 'low'` for `remote`). That pinned
|
|
100
120
|
* every phone-sized session — including LAN IPv6 GUA misclassified as remote
|
|
@@ -145,7 +165,7 @@ function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
|
|
|
145
165
|
return {
|
|
146
166
|
...provider,
|
|
147
167
|
createSession: (input) => provider.createSession(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
|
|
148
|
-
handleOffer: (input) => provider.handleOffer(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
|
|
168
|
+
handleOffer: (input) => provider.handleOffer(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(stripTalkbackFromRequest(input, clientClass), userAgent), clientClass), clientClass)),
|
|
149
169
|
};
|
|
150
170
|
}
|
|
151
171
|
/**
|
|
@@ -170,7 +190,7 @@ function buildServerProviders(services) {
|
|
|
170
190
|
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
191
|
// server-management — runtime-updatable root package (phase 1: hub only).
|
|
172
192
|
'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)),
|
|
193
|
+
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.agentRegistry)),
|
|
174
194
|
// webrtc-session — the resolved broker singleton, wrapped per request to
|
|
175
195
|
// inject the server-read User-Agent into signaling attribution (the broker
|
|
176
196
|
// 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
|