@camstack/server 1.2.94 → 1.2.95
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/addon-scan.js +131 -0
- package/dist/agent/main.js +35 -40
- package/dist/api/core/cap-providers.js +53 -9
- package/dist/core/addon/addon-package.service.js +17 -2
- package/package.json +14 -14
- package/dist/agent/builtins-seed.js +0 -89
- package/dist/api/addons-custom.router.js +0 -99
- package/dist/api/core/bulk-update-coordinator.js +0 -229
- package/dist/api/core/settings-backend.router.js +0 -121
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/core/addon/framework-live-sync.js +0 -344
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
- package/dist/server-root/boot-plan.js +0 -110
- package/dist/server-root/semver-compare.js +0 -45
- package/dist/server-root/server-root-state.js +0 -220
- package/dist/server-root/workspace-detect.js +0 -73
|
@@ -0,0 +1,131 @@
|
|
|
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.BUILTINS_PACKAGE = void 0;
|
|
37
|
+
exports.resolveAddonPackageDirs = resolveAddonPackageDirs;
|
|
38
|
+
exports.planAgentAddonScan = planAgentAddonScan;
|
|
39
|
+
/**
|
|
40
|
+
* Which directories the agent scans for addons — decided as pure data.
|
|
41
|
+
*
|
|
42
|
+
* ── The single-copy collapse, finally on the agent ─────────────────────────
|
|
43
|
+
* The hub stopped loading framework builtins from a physical
|
|
44
|
+
* `<addonsDir>/@camstack/system` on 2026-07-18: it loads them from the ACTIVE
|
|
45
|
+
* CLOSURE it is itself running (`addon-registry.service.ts`, `loadFromAddonDir`
|
|
46
|
+
* on the closure dir, then `loadFromDirectory(addonsDir, ['@camstack/system'])`
|
|
47
|
+
* so the copy is skipped). The reason is in that file's comment — *"that copy
|
|
48
|
+
* drifted because no update channel refreshed it"*.
|
|
49
|
+
*
|
|
50
|
+
* The agent never got the change, and drifted exactly as predicted. Measured
|
|
51
|
+
* 2026-08-11: little-unraid carried `@camstack/system@1.2.61` and macmini
|
|
52
|
+
* `1.2.62` in their addon roots while both ran a `1.2.78` closure. Nothing
|
|
53
|
+
* refreshed them, because `seedBuiltinsFromClosure` (now deleted) only wrote
|
|
54
|
+
* the copy when it was ABSENT — a self-heal that guaranteed a copy existed and
|
|
55
|
+
* never that it was current. It also contradicted
|
|
56
|
+
* `AddonInstaller.shouldSkipClosureProvidedSeed`, which already refused to
|
|
57
|
+
* plant that copy for this very reason.
|
|
58
|
+
*
|
|
59
|
+
* ── Why the skip is correctness, not tidiness ──────────────────────────────
|
|
60
|
+
* `AddonLoader` keys its map on `declaration.id`, so loading the closure AND a
|
|
61
|
+
* stale copy through one loader is not a duplicate — it is LAST-LOAD-WINS. The
|
|
62
|
+
* builtins a node runs would depend on directory iteration order. The copy must
|
|
63
|
+
* be skipped, not merely deprioritized.
|
|
64
|
+
*
|
|
65
|
+
* ── The fallback ──────────────────────────────────────────────────────────
|
|
66
|
+
* If the closure cannot be resolved, the copy is NOT skipped and the node boots
|
|
67
|
+
* from it. In a running agent this is near-impossible — `agent/main.ts`
|
|
68
|
+
* statically imports `@camstack/system`, so if the process exists the walk-up
|
|
69
|
+
* resolves — but a node with neither source must fail through the D87 guard
|
|
70
|
+
* (`assertRequiredInfraResolved`, which names every missing required cap),
|
|
71
|
+
* never through a silent "no addon packages found" early return.
|
|
72
|
+
*/
|
|
73
|
+
const fs = __importStar(require("node:fs"));
|
|
74
|
+
const path = __importStar(require("node:path"));
|
|
75
|
+
/** The framework builtins package. Loaded from the closure, never from a copy. */
|
|
76
|
+
exports.BUILTINS_PACKAGE = '@camstack/system';
|
|
77
|
+
function isDir(p) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(p).isDirectory();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every installed addon package under `addonsDir`, minus `skipPackages`.
|
|
87
|
+
*
|
|
88
|
+
* Matching mirrors `AddonLoader.loadFromDirectory`'s skip list exactly —
|
|
89
|
+
* `<scope>/<name>` for a scoped directory, the bare name for a flat one — so
|
|
90
|
+
* the hub and the agent exclude the same thing given the same list.
|
|
91
|
+
*/
|
|
92
|
+
function resolveAddonPackageDirs(addonsDir, skipPackages = []) {
|
|
93
|
+
const dirs = [];
|
|
94
|
+
if (!fs.existsSync(addonsDir))
|
|
95
|
+
return dirs;
|
|
96
|
+
const skip = new Set(skipPackages);
|
|
97
|
+
for (const name of fs.readdirSync(addonsDir)) {
|
|
98
|
+
const full = path.join(addonsDir, name);
|
|
99
|
+
if (name.startsWith('@') && isDir(full)) {
|
|
100
|
+
// Scoped packages: @camstack/addon-xyz
|
|
101
|
+
for (const sub of fs.readdirSync(full)) {
|
|
102
|
+
const subFull = path.join(full, sub);
|
|
103
|
+
if (skip.has(`${name}/${sub}`))
|
|
104
|
+
continue;
|
|
105
|
+
if (isDir(subFull) && fs.existsSync(path.join(subFull, 'package.json'))) {
|
|
106
|
+
dirs.push(subFull);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (isDir(full) && fs.existsSync(path.join(full, 'package.json'))) {
|
|
111
|
+
if (skip.has(name))
|
|
112
|
+
continue;
|
|
113
|
+
dirs.push(full);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return dirs;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The ordered directory list `bootCoreAddons` loads, closure first.
|
|
120
|
+
*
|
|
121
|
+
* `closureSystemDir` is `resolveHubClosurePackageDir(BUILTINS_PACKAGE)` — null
|
|
122
|
+
* only when the builtins are not resolvable from the running closure at all.
|
|
123
|
+
*/
|
|
124
|
+
function planAgentAddonScan(addonsDir, closureSystemDir) {
|
|
125
|
+
if (closureSystemDir === null) {
|
|
126
|
+
// No closure copy: fall back to whatever is on disk, including the
|
|
127
|
+
// addon-root builtins, rather than booting with no infra at all.
|
|
128
|
+
return resolveAddonPackageDirs(addonsDir);
|
|
129
|
+
}
|
|
130
|
+
return [closureSystemDir, ...resolveAddonPackageDirs(addonsDir, [exports.BUILTINS_PACKAGE])];
|
|
131
|
+
}
|
package/dist/agent/main.js
CHANGED
|
@@ -47,7 +47,8 @@ const agent_update_service_js_1 = require("./agent-update-service.js");
|
|
|
47
47
|
const server_management_provider_js_1 = require("../api/core/server-management.provider.js");
|
|
48
48
|
const infra_boot_guard_js_1 = require("./infra-boot-guard.js");
|
|
49
49
|
const infra_boot_plan_js_1 = require("./infra-boot-plan.js");
|
|
50
|
-
const
|
|
50
|
+
const package_dir_utils_js_1 = require("../core/addon/package-dir-utils.js");
|
|
51
|
+
const addon_scan_js_1 = require("./addon-scan.js");
|
|
51
52
|
const agent_service_js_1 = require("./agent-service.js");
|
|
52
53
|
const agent_group_runner_js_1 = require("./agent-group-runner.js");
|
|
53
54
|
const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.js");
|
|
@@ -769,13 +770,21 @@ async function startAgent(configPath) {
|
|
|
769
770
|
// Core infra addons to load on agent — all infra including log-destination (hub-forwarder)
|
|
770
771
|
const AGENT_INFRA = system_1.INFRA_CAPABILITIES;
|
|
771
772
|
async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFactory) {
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
const
|
|
773
|
+
// Builtins come from the CLOSURE this process is running, exactly as the hub
|
|
774
|
+
// has done since 2026-07-18 — never from a `<addonsDir>/@camstack/system`
|
|
775
|
+
// copy, which nothing refreshes and which therefore drifts (both agents were
|
|
776
|
+
// 17 versions behind on 2026-08-11). `planAgentAddonScan` owns the decision
|
|
777
|
+
// and the fallback; see `addon-scan.ts`. Non-framework infra providers
|
|
778
|
+
// (e.g. `@camstack/addon-platform-probe-native`) still come from the scan.
|
|
779
|
+
const closureSystemDir = (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(addon_scan_js_1.BUILTINS_PACKAGE);
|
|
780
|
+
if (closureSystemDir === null) {
|
|
781
|
+
// Louder than the hub's warn: on an agent the builtins ARE the infra, so
|
|
782
|
+
// this is pre-fatal. Boot continues to the D87 guard, which names every
|
|
783
|
+
// required cap it could not resolve.
|
|
784
|
+
console.error(`[Agent] ${addon_scan_js_1.BUILTINS_PACKAGE} not resolvable from the running closure — ` +
|
|
785
|
+
'falling back to the addon-root copy, if there is one');
|
|
786
|
+
}
|
|
787
|
+
const packageDirs = (0, addon_scan_js_1.planAgentAddonScan)(config.addonsDir, closureSystemDir);
|
|
779
788
|
if (packageDirs.length === 0) {
|
|
780
789
|
console.warn('[Agent] No addon packages found — running without infrastructure addons');
|
|
781
790
|
return;
|
|
@@ -876,7 +885,13 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
|
|
|
876
885
|
// Phase 1.5: Load cluster-capable addon packages
|
|
877
886
|
// ---------------------------------------------------------------------------
|
|
878
887
|
async function loadClusterCapableAddons(broker, config, capabilityRegistry, loadedAddons) {
|
|
879
|
-
|
|
888
|
+
// Builtins are in-process infra, never group-spawned, and they come from the
|
|
889
|
+
// closure — so the addon-root copy is skipped here too. Leaving it in would
|
|
890
|
+
// import a stale entry module during the scan below, which is the whole thing
|
|
891
|
+
// the single-copy collapse removes. The closure dir is deliberately NOT added:
|
|
892
|
+
// adding it would newly expose `hub-forwarder` / `platform-probe` (the only
|
|
893
|
+
// builtins carrying `execution`) as spawn candidates.
|
|
894
|
+
const addonPackageDirs = (0, addon_scan_js_1.resolveAddonPackageDirs)(config.addonsDir, systemSkipList());
|
|
880
895
|
if (addonPackageDirs.length === 0)
|
|
881
896
|
return;
|
|
882
897
|
// ── Phase 0 (cross-package): collect every group-eligible addon
|
|
@@ -974,7 +989,9 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
|
|
|
974
989
|
// the agent MAIN thread is what wedged `$agent.reload` and dropped the node
|
|
975
990
|
// from topology. A group-runner addon only needs its declaration to be
|
|
976
991
|
// (re)dispatched to a subprocess, which loads the real module itself.
|
|
977
|
-
|
|
992
|
+
// Same skip: a `$agent.deploy` of `@camstack/system` still lands bytes in the
|
|
993
|
+
// addon root, but a reload must not adopt them over the closure.
|
|
994
|
+
const packageDirs = (0, addon_scan_js_1.resolveAddonPackageDirs)(addonsDir, systemSkipList());
|
|
978
995
|
const groupCandidates = [];
|
|
979
996
|
// Dirs holding a deployable NON-group addon — and ONLY these — need the
|
|
980
997
|
// importing loader. None exist under the default `hub-only` placement
|
|
@@ -1118,36 +1135,14 @@ function readAgentVersion() {
|
|
|
1118
1135
|
}
|
|
1119
1136
|
return 'unknown';
|
|
1120
1137
|
}
|
|
1121
|
-
/**
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1130
|
-
/** Scan addonsDir for addon packages (scoped and unscoped, follows symlinks) */
|
|
1131
|
-
function resolveAddonPackageDirs(addonsDir) {
|
|
1132
|
-
const dirs = [];
|
|
1133
|
-
if (!fs.existsSync(addonsDir))
|
|
1134
|
-
return dirs;
|
|
1135
|
-
for (const name of fs.readdirSync(addonsDir)) {
|
|
1136
|
-
const full = path.join(addonsDir, name);
|
|
1137
|
-
if (name.startsWith('@') && isDir(full)) {
|
|
1138
|
-
// Scoped packages: @camstack/addon-xyz
|
|
1139
|
-
for (const sub of fs.readdirSync(full)) {
|
|
1140
|
-
const subFull = path.join(full, sub);
|
|
1141
|
-
if (isDir(subFull) && fs.existsSync(path.join(subFull, 'package.json'))) {
|
|
1142
|
-
dirs.push(subFull);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
else if (isDir(full) && fs.existsSync(path.join(full, 'package.json'))) {
|
|
1147
|
-
dirs.push(full);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return dirs;
|
|
1138
|
+
/**
|
|
1139
|
+
* Skip the addon-root builtins copy — but ONLY when the closure can supply
|
|
1140
|
+
* them. Same condition `planAgentAddonScan` applies, kept in one place so the
|
|
1141
|
+
* infra scan and the two deployable-addon scans can never disagree about which
|
|
1142
|
+
* copy is authoritative.
|
|
1143
|
+
*/
|
|
1144
|
+
function systemSkipList() {
|
|
1145
|
+
return (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(addon_scan_js_1.BUILTINS_PACKAGE) === null ? [] : [addon_scan_js_1.BUILTINS_PACKAGE];
|
|
1151
1146
|
}
|
|
1152
1147
|
// ---------------------------------------------------------------------------
|
|
1153
1148
|
// E1 helper — agent-local UDS child manifest adaptation
|
|
@@ -1176,14 +1176,24 @@ function isHubNode(nodeId) {
|
|
|
1176
1176
|
* agent reports its addon roster (id + status + version + packageName);
|
|
1177
1177
|
* we keep only entries that carry both a package name and a version so
|
|
1178
1178
|
* the hub can diff them against npm.
|
|
1179
|
+
*
|
|
1180
|
+
* `@camstack/system` is KEPT, and it is one of the most important entries here.
|
|
1181
|
+
* On an agent, `addons/@camstack/system` is the only copy of the builtin infra
|
|
1182
|
+
* ([D87](../../../../docs/decisions/adr-0087.md)), so a pending update on it is
|
|
1183
|
+
* a true, actionable signal: the node's infra is behind the closure it runs
|
|
1184
|
+
* against. Both agents sat 17 versions behind that way until 2026-08-11. Do not
|
|
1185
|
+
* filter it out to make the list look clean — the number reaching zero is the
|
|
1186
|
+
* point of the number.
|
|
1179
1187
|
*/
|
|
1180
1188
|
async function fetchAgentInstalledPackages(broker, nodeId) {
|
|
1181
1189
|
const status = await broker.call('$agent.status', {}, { nodeID: nodeId, timeout: 5_000 });
|
|
1182
1190
|
const out = [];
|
|
1183
1191
|
for (const a of status.addons ?? []) {
|
|
1184
|
-
if (typeof a.packageName
|
|
1185
|
-
|
|
1186
|
-
|
|
1192
|
+
if (typeof a.packageName !== 'string' || typeof a.version !== 'string')
|
|
1193
|
+
continue;
|
|
1194
|
+
if ((0, addon_package_service_js_1.isFrameworkPackage)(a.packageName))
|
|
1195
|
+
continue;
|
|
1196
|
+
out.push({ name: a.packageName, version: a.version });
|
|
1187
1197
|
}
|
|
1188
1198
|
return out;
|
|
1189
1199
|
}
|
|
@@ -1253,6 +1263,22 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1253
1263
|
}
|
|
1254
1264
|
return { success: false, error: task?.error ?? 'update failed', jobId };
|
|
1255
1265
|
}
|
|
1266
|
+
// `@camstack/system` DELIBERATELY travels this way to an agent, and it is
|
|
1267
|
+
// the only way it can. On the hub, `addons/@camstack/system` is a shadow
|
|
1268
|
+
// of the closure; on an AGENT it is THE ONLY COPY of the builtin infra —
|
|
1269
|
+
// `bootCoreAddons` scans the addon root and nothing else, and the image
|
|
1270
|
+
// seed excludes the framework set on purpose ([D87](../../../../docs/decisions/adr-0087.md)).
|
|
1271
|
+
// Removing it on 2026-08-07 left the node with no `filesystem-storage`,
|
|
1272
|
+
// `sqlite-settings`, `storage-orchestrator`, `hub-forwarder`,
|
|
1273
|
+
// `metrics-native` or `platform-probe`, and the outage was unobservable
|
|
1274
|
+
// because the log shipper was in the same package.
|
|
1275
|
+
//
|
|
1276
|
+
// So do NOT add a framework guard here. A 2026-08-11 attempt to do so read
|
|
1277
|
+
// the launcher's `resolved @camstack/system@… from <activeRoot>` line as
|
|
1278
|
+
// proof that nothing loads the addon-root copy. That line answers which
|
|
1279
|
+
// copy the LAUNCHER resolves for its own imports — a different consumer
|
|
1280
|
+
// from `bootCoreAddons`. Same trap D45 names: a version is a property of a
|
|
1281
|
+
// path, and a path is only evidence about the consumer that reads it.
|
|
1256
1282
|
// Agent target: the hub packs the resolved version and ships the
|
|
1257
1283
|
// tarball over `$agent.deploy` — the agent has no npm runtime.
|
|
1258
1284
|
// The reload re-instantiates the changed package's addons; for a large
|
|
@@ -1349,12 +1375,30 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
|
|
|
1349
1375
|
return { success: true };
|
|
1350
1376
|
},
|
|
1351
1377
|
// ── Lifecycle job engine ─────────────────────────────────────────
|
|
1352
|
-
startJob: async (input) =>
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1378
|
+
startJob: async (input) => {
|
|
1379
|
+
// The engine is HUB-LOCAL: `StartJobInput.nodeIds` is declared
|
|
1380
|
+
// "reserved for future cluster jobs" and nothing reads it. Passing an
|
|
1381
|
+
// agent id therefore used to apply every target to the HUB while the
|
|
1382
|
+
// job, its per-task `nodeId` and the UI all reported the agent — so
|
|
1383
|
+
// "Update all" on a selected agent silently updated the wrong node and
|
|
1384
|
+
// left the agent pending forever. That is how two agents sat at 8
|
|
1385
|
+
// pending addons across several sessions (2026-08-11). Refuse instead:
|
|
1386
|
+
// an agent converges one package at a time through `updatePackage`,
|
|
1387
|
+
// which has a real `$agent.deploy` path.
|
|
1388
|
+
const offNode = (input.nodeIds ?? []).filter((id) => !isHubNode(id));
|
|
1389
|
+
if (offNode.length > 0) {
|
|
1390
|
+
throw new server_1.TRPCError({
|
|
1391
|
+
code: 'BAD_REQUEST',
|
|
1392
|
+
message: `The lifecycle job engine is hub-local; it cannot target ${offNode.join(', ')}. Update an agent's packages via updatePackage({ nodeId }).`,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return lifecycleRunner.startJob({
|
|
1396
|
+
kind: input.kind,
|
|
1397
|
+
targets: input.targets,
|
|
1398
|
+
nodeIds: input.nodeIds,
|
|
1399
|
+
createdBy: lifecycleCreatedBy,
|
|
1400
|
+
});
|
|
1401
|
+
},
|
|
1358
1402
|
getJob: async (input) => lifecycleRunner.getJob(input.jobId),
|
|
1359
1403
|
listJobs: async (input) => lifecycleRunner.listJobs({ activeOnly: input.activeOnly }),
|
|
1360
1404
|
cancelJob: async (input) => lifecycleRunner.cancelJob(input.jobId),
|
|
@@ -1032,6 +1032,15 @@ class AddonPackageService {
|
|
|
1032
1032
|
* for use by the lifecycle job engine (TarballFetcher signature). The
|
|
1033
1033
|
* `signal` is forwarded to the underlying fetch calls so the AbortController
|
|
1034
1034
|
* timeout wired in LifecycleJobEngine fires correctly.
|
|
1035
|
+
*
|
|
1036
|
+
* `version` may be an exact semver OR a dist-tag. It looked up only
|
|
1037
|
+
* `versions[version]` until 2026-08-11, so every caller that passed the
|
|
1038
|
+
* DEFAULT — `updatePackage` resolves an absent input to `'latest'` — failed
|
|
1039
|
+
* with `no tarball url`, because `latest` is a dist-tag key and never a
|
|
1040
|
+
* `versions` key. The Addons page hid this by always sending the resolved
|
|
1041
|
+
* `latestVersion` from `listUpdates`; the API path had no such caller and
|
|
1042
|
+
* could not update anything. Mirrors `AddonInstaller.downloadNpmTarball`.
|
|
1043
|
+
* Ranges (`^1.2.0`) are still not supported — resolve those upstream.
|
|
1035
1044
|
*/
|
|
1036
1045
|
async fetchAddonTarball(name, version, signal) {
|
|
1037
1046
|
const registry = process.env['CAMSTACK_NPM_REGISTRY'];
|
|
@@ -1041,9 +1050,15 @@ class AddonPackageService {
|
|
|
1041
1050
|
if (!metaRes.ok)
|
|
1042
1051
|
throw new Error(`registry GET ${metaUrl} → ${metaRes.status}`);
|
|
1043
1052
|
const meta = (await metaRes.json());
|
|
1044
|
-
const
|
|
1053
|
+
const versions = meta.versions ?? {};
|
|
1054
|
+
const distTags = meta['dist-tags'] ?? {};
|
|
1055
|
+
const resolvedVersion = versions[version] ? version : distTags[version];
|
|
1056
|
+
if (resolvedVersion === undefined) {
|
|
1057
|
+
throw new Error(`no version resolved for ${name}@${version}`);
|
|
1058
|
+
}
|
|
1059
|
+
const tarballUrl = versions[resolvedVersion]?.dist?.tarball;
|
|
1045
1060
|
if (typeof tarballUrl !== 'string') {
|
|
1046
|
-
throw new Error(`no tarball url for ${name}@${
|
|
1061
|
+
throw new Error(`no tarball url for ${name}@${resolvedVersion}`);
|
|
1047
1062
|
}
|
|
1048
1063
|
const tarRes = await fetch(tarballUrl, { signal });
|
|
1049
1064
|
if (!tarRes.ok)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.95",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@camstack/addon-admin-ui": "
|
|
37
|
-
"@camstack/addon-agent-ui": "
|
|
38
|
-
"@camstack/addon-auth": "
|
|
39
|
-
"@camstack/addon-decoder-nodeav": "
|
|
40
|
-
"@camstack/addon-notifiers": "
|
|
41
|
-
"@camstack/addon-pipeline": "
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "
|
|
43
|
-
"@camstack/addon-post-analysis": "
|
|
44
|
-
"@camstack/sdk": "
|
|
45
|
-
"@camstack/shm-ring": "
|
|
46
|
-
"@camstack/system": "
|
|
47
|
-
"@camstack/types": "
|
|
48
|
-
"@camstack/ui-library": "
|
|
36
|
+
"@camstack/addon-admin-ui": "1.2.48",
|
|
37
|
+
"@camstack/addon-agent-ui": "1.2.13",
|
|
38
|
+
"@camstack/addon-auth": "1.2.14",
|
|
39
|
+
"@camstack/addon-decoder-nodeav": "1.2.12",
|
|
40
|
+
"@camstack/addon-notifiers": "1.2.17",
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.64",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.45",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.64",
|
|
44
|
+
"@camstack/sdk": "1.2.14",
|
|
45
|
+
"@camstack/shm-ring": "1.1.12",
|
|
46
|
+
"@camstack/system": "1.2.79",
|
|
47
|
+
"@camstack/types": "1.2.59",
|
|
48
|
+
"@camstack/ui-library": "1.2.41",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|
|
51
51
|
"@fastify/cors": "^11.2.0",
|
|
@@ -1,89 +0,0 @@
|
|
|
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.seedBuiltinsFromClosure = seedBuiltinsFromClosure;
|
|
37
|
-
/**
|
|
38
|
-
* Self-heal the agent's builtins package before the infra scan.
|
|
39
|
-
*
|
|
40
|
-
* `bootCoreAddons` scans `config.addonsDir` and nothing else, the image seed
|
|
41
|
-
* ships no `@camstack/system`, and the running closure's copy is NOT loaded
|
|
42
|
-
* as an addon — so an agent whose `/data/addons/@camstack/system` is missing
|
|
43
|
-
* (greenfield container, an operator cleanup, a failed deploy) boots with
|
|
44
|
-
* zero infrastructure. Before D87 that produced a silently gutted node; with
|
|
45
|
-
* the D87 guard it produces a crash-loop. Both are wrong answers to a
|
|
46
|
-
* question the node can answer itself: the closure it is RUNNING carries the
|
|
47
|
-
* exact builtins package it needs.
|
|
48
|
-
*
|
|
49
|
-
* The copy is files-list shaped — `package.json` + `dist` — and NEVER
|
|
50
|
-
* `node_modules`: a nested `node_modules/@camstack/*` carries its own
|
|
51
|
-
* `package.json`s, the addon scan discovers those too, and the same addon
|
|
52
|
-
* initializing twice is a boot abort (measured live 2026-08-08, twice, two
|
|
53
|
-
* different vehicles).
|
|
54
|
-
*/
|
|
55
|
-
const fs = __importStar(require("node:fs"));
|
|
56
|
-
const path = __importStar(require("node:path"));
|
|
57
|
-
function seedBuiltinsFromClosure(addonsDir, log = console.log, resolveClosurePkg = () => require.resolve('@camstack/system/package.json')) {
|
|
58
|
-
const target = path.join(addonsDir, '@camstack', 'system');
|
|
59
|
-
if (fs.existsSync(path.join(target, 'package.json')))
|
|
60
|
-
return 'present';
|
|
61
|
-
let closurePkgJson;
|
|
62
|
-
try {
|
|
63
|
-
closurePkgJson = resolveClosurePkg();
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
log('[Agent] builtins seed: @camstack/system not resolvable from the closure — cannot self-heal');
|
|
67
|
-
return 'unavailable';
|
|
68
|
-
}
|
|
69
|
-
const closureRoot = path.dirname(closurePkgJson);
|
|
70
|
-
const closureDist = path.join(closureRoot, 'dist');
|
|
71
|
-
if (!fs.existsSync(closureDist)) {
|
|
72
|
-
log(`[Agent] builtins seed: closure copy at ${closureRoot} has no dist — cannot self-heal`);
|
|
73
|
-
return 'unavailable';
|
|
74
|
-
}
|
|
75
|
-
// The WHOLE package, node_modules included. A deps-free copy was tried
|
|
76
|
-
// first and failed live (2026-08-08): the builtins' dist requires native
|
|
77
|
-
// deps (better-sqlite3) that resolve relative to the COPY, so the addon
|
|
78
|
-
// scan logged "Failed to scan" and the guard aborted with "no addon
|
|
79
|
-
// under /data/addons" — while the log right above it said the seed had
|
|
80
|
-
// run. Nested `node_modules` are safe: the scan reads one level of
|
|
81
|
-
// `addonsDir/@scope/*`, never inside a package. (The double-registration
|
|
82
|
-
// the deps-free copy was guarding against was actually the per-capability
|
|
83
|
-
// init loop — fixed by `planInfraBoot` — not nested discovery.)
|
|
84
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
85
|
-
fs.cpSync(closureRoot, target, { recursive: true });
|
|
86
|
-
log(`[Agent] builtins seed: @camstack/system was missing under ${addonsDir} — ` +
|
|
87
|
-
`seeded the full package from the running closure (${closureRoot})`);
|
|
88
|
-
return 'seeded';
|
|
89
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAddonsCustomProcedures = createAddonsCustomProcedures;
|
|
4
|
-
/**
|
|
5
|
-
* `api.addons.custom` — generic dispatcher for addon-defined custom actions.
|
|
6
|
-
*
|
|
7
|
-
* Task 7.2 of the device-proxy redesign. Addons declare a catalog of
|
|
8
|
-
* custom actions at boot via `AddonInitResult.customActions` + a single
|
|
9
|
-
* `handleCustomAction(action, input)` handler. The catalog is registered
|
|
10
|
-
* with a per-process `CustomActionRegistry` (Task 7.1). This endpoint is
|
|
11
|
-
* the single tRPC entry point that resolves an `(addonId, action)` pair,
|
|
12
|
-
* validates input + output against the action's Zod schemas, enforces the
|
|
13
|
-
* action's declared auth level, and dispatches to the addon handler.
|
|
14
|
-
*
|
|
15
|
-
* The factory returns a record of procedures (not a router) so the caller
|
|
16
|
-
* can spread it into the existing `addons` namespace:
|
|
17
|
-
*
|
|
18
|
-
* trpcRouter({
|
|
19
|
-
* ...existingAddonsProcedures,
|
|
20
|
-
* ...createAddonsCustomProcedures({ getCustomActionRegistry: ... }),
|
|
21
|
-
* })
|
|
22
|
-
*
|
|
23
|
-
* This avoids `mergeRouters` (which requires sharing the `t` instance
|
|
24
|
-
* across modules) while still mounting the procedure at `api.addons.custom`.
|
|
25
|
-
*/
|
|
26
|
-
const zod_1 = require("zod");
|
|
27
|
-
const server_1 = require("@trpc/server");
|
|
28
|
-
const trpc_middleware_js_1 = require("./trpc/trpc.middleware.js");
|
|
29
|
-
/**
|
|
30
|
-
* Build the procedure record for the `custom` endpoint.
|
|
31
|
-
*
|
|
32
|
-
* The OUTER procedure is `protectedProcedure` — every caller must be
|
|
33
|
-
* authenticated. The INNER per-action auth declared in `spec.auth` is
|
|
34
|
-
* enforced manually by `ensureAuth` because the auth level is not known
|
|
35
|
-
* until after the registry lookup.
|
|
36
|
-
*/
|
|
37
|
-
function createAddonsCustomProcedures(deps) {
|
|
38
|
-
return {
|
|
39
|
-
custom: trpc_middleware_js_1.protectedProcedure
|
|
40
|
-
.input(zod_1.z.object({
|
|
41
|
-
addonId: zod_1.z.string().min(1),
|
|
42
|
-
action: zod_1.z.string().min(1),
|
|
43
|
-
input: zod_1.z.unknown(),
|
|
44
|
-
}))
|
|
45
|
-
.output(zod_1.z.unknown())
|
|
46
|
-
.mutation(async ({ input, ctx }) => {
|
|
47
|
-
const registry = deps.getCustomActionRegistry();
|
|
48
|
-
const entry = registry.resolve(input.addonId, input.action);
|
|
49
|
-
if (!entry) {
|
|
50
|
-
throw new server_1.TRPCError({
|
|
51
|
-
code: 'NOT_FOUND',
|
|
52
|
-
message: `addon '${input.addonId}' has no custom action '${input.action}'`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Per-action authorization. The outer procedure already requires
|
|
56
|
-
// authentication; here we additionally enforce the declared role
|
|
57
|
-
// when it's stricter than 'protected'.
|
|
58
|
-
ensureAuth(ctx, entry.spec.auth);
|
|
59
|
-
// Validate input against the action's declared Zod schema.
|
|
60
|
-
const parsedInput = entry.spec.input.parse(input.input);
|
|
61
|
-
// Dispatch through the addon handler, forwarding the authenticated
|
|
62
|
-
// caller when the action declares `caller: 'required'`. The caller is
|
|
63
|
-
// derived server-side from the request principal (never trusted from
|
|
64
|
-
// input); `ctx.user` is guaranteed present because `ensureAuth` above
|
|
65
|
-
// rejects unauthenticated callers for any non-public action, and the
|
|
66
|
-
// outer `protectedProcedure` rejects them for public ones.
|
|
67
|
-
const caller = entry.spec.caller === 'required' && ctx.user
|
|
68
|
-
? { userId: ctx.user.id, isAdmin: ctx.user.isAdmin }
|
|
69
|
-
: undefined;
|
|
70
|
-
const result = await entry.handler(parsedInput, caller);
|
|
71
|
-
// Validate the addon's output. Crash-early on misbehaving addons.
|
|
72
|
-
return entry.spec.output.parse(result);
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Enforce the action's declared auth level.
|
|
78
|
-
*
|
|
79
|
-
* Mirrors the role checks performed by `protectedProcedure` and
|
|
80
|
-
* `adminProcedure` in trpc.middleware.ts:
|
|
81
|
-
* - public: no auth
|
|
82
|
-
* - protected: any authenticated user
|
|
83
|
-
* - admin: isAdmin only (scoped tokens bounce)
|
|
84
|
-
*/
|
|
85
|
-
function ensureAuth(ctx, level) {
|
|
86
|
-
if (level === 'public')
|
|
87
|
-
return;
|
|
88
|
-
if (!ctx.user) {
|
|
89
|
-
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
90
|
-
}
|
|
91
|
-
if (level === 'protected')
|
|
92
|
-
return;
|
|
93
|
-
if (level === 'admin') {
|
|
94
|
-
if (!ctx.user.isAdmin) {
|
|
95
|
-
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'custom action requires admin' });
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
}
|