loadout-ai 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/MASTER_PLAN.md +9 -9
- package/README.md +84 -121
- package/catalog/discovered.json +59800 -10065
- package/catalog/packages.json +4 -3
- package/dist/src/cli.js +26 -2979
- package/dist/src/commands/agents.js +168 -0
- package/dist/src/commands/catalog.js +708 -0
- package/dist/src/commands/health.js +98 -0
- package/dist/src/commands/inventory.js +417 -0
- package/dist/src/commands/lifecycle.js +411 -0
- package/dist/src/commands/mcp.js +424 -0
- package/dist/src/commands/setup.js +67 -0
- package/dist/src/commands/sharing.js +358 -0
- package/dist/src/commands/support.js +246 -0
- package/dist/src/core/active-limit.js +2 -0
- package/dist/src/core/active-policy.js +2 -1
- package/dist/src/core/catalog-install.js +47 -10
- package/dist/src/core/catalog.js +15 -96
- package/dist/src/core/cli-guide.js +43 -37
- package/dist/src/core/completion.js +2 -20
- package/dist/src/core/doctor.js +73 -24
- package/dist/src/core/github.js +3 -2
- package/dist/src/core/handoff.js +161 -0
- package/dist/src/core/health-score-evidence.js +2 -2
- package/dist/src/core/health.js +83 -0
- package/dist/src/core/install.js +8 -3
- package/dist/src/core/loadout-card.js +14 -2
- package/dist/src/core/review-queue.js +7 -2
- package/dist/src/core/route.js +248 -0
- package/dist/src/core/skill-inventory.js +3 -3
- package/dist/src/core/skills.js +6 -7
- package/dist/src/core/source.js +10 -3
- package/dist/src/core/upgrade.js +10 -9
- package/docs/ACTIVE_SET.md +1 -1
- package/docs/ACTIVE_SET_POLICY.md +1 -1
- package/docs/CATALOG.md +2 -2
- package/docs/DEMO_SCRIPT.md +5 -5
- package/docs/DISCOVERED.md +250 -253
- package/docs/RELEASE_REVIEW.md +28 -143
- package/docs/SUBMISSION_COPY.md +4 -4
- package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
- package/docs/USER_TEST_GUIDE.md +1 -1
- package/docs/evidence/readme-claims.json +2 -14
- package/package.json +4 -3
- package/dist/src/core/benchmark-campaign.js +0 -496
- package/dist/src/core/benchmark-evidence.js +0 -458
- package/dist/src/core/benchmark-fixtures.js +0 -1157
- package/dist/src/core/benchmark-runner.js +0 -301
- package/dist/src/core/benchmark-trust.js +0 -795
- package/dist/src/core/catalog-release.js +0 -287
- package/dist/src/core/compatibility-intelligence.js +0 -465
- package/dist/src/core/ecosystem-import.js +0 -1072
- package/dist/src/core/head-to-head.js +0 -437
- package/dist/src/core/intelligence-feed-build.js +0 -59
- package/dist/src/core/intelligence-feed.js +0 -353
- package/dist/src/core/registry-api.js +0 -97
- package/dist/src/core/release-claims.js +0 -175
- package/dist/src/core/signing.js +0 -115
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { readFile, rm, stat } from "node:fs/promises";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { isDeepStrictEqual } from "node:util";
|
|
5
|
-
import { writeFileAtomically } from "./atomic-file.js";
|
|
6
|
-
import { catalogTrustPath, loadEffectiveCatalog, readCatalogTrustState, readTrustedCatalogState, trustedCatalogPath, validateCatalog, } from "./catalog.js";
|
|
7
|
-
import { ensureDirectory } from "./paths.js";
|
|
8
|
-
import { loadoutHome } from "./paths.js";
|
|
9
|
-
import { withFileLock } from "./file-lock.js";
|
|
10
|
-
import { createSnapshot, recordSnapshotPostMutationState } from "./snapshot.js";
|
|
11
|
-
import { verifyEnvelope } from "./signing.js";
|
|
12
|
-
import { beginTransaction, completeTransaction, markTransactionCommitting, rollbackTransaction, recoverPendingTransactions, } from "./transaction.js";
|
|
13
|
-
const MAX_RELEASE_BYTES = 5 * 1024 * 1024;
|
|
14
|
-
const MAX_PUBLIC_KEY_BYTES = 64 * 1024;
|
|
15
|
-
const catalogReleaseLockPath = () => join(loadoutHome(), "catalog-releases", "apply.lock");
|
|
16
|
-
const envelopeDigest = (envelope) => createHash("sha256").update(envelope.signature).digest("hex");
|
|
17
|
-
function parseEnvelope(value) {
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
19
|
-
throw new Error("Signed catalog snapshot must be an object");
|
|
20
|
-
const envelope = value;
|
|
21
|
-
if (envelope.schemaVersion !== 1 ||
|
|
22
|
-
envelope.algorithm !== "Ed25519" ||
|
|
23
|
-
typeof envelope.createdAt !== "string" ||
|
|
24
|
-
Number.isNaN(Date.parse(envelope.createdAt)) ||
|
|
25
|
-
typeof envelope.publicKeyFingerprint !== "string" ||
|
|
26
|
-
typeof envelope.signature !== "string" ||
|
|
27
|
-
envelope.payload === undefined)
|
|
28
|
-
throw new Error("Signed catalog envelope schema is invalid");
|
|
29
|
-
return envelope;
|
|
30
|
-
}
|
|
31
|
-
async function readReleaseSource(source, fetcher = fetch, requestTimeoutMs = 15_000) {
|
|
32
|
-
if (/^https:\/\//i.test(source)) {
|
|
33
|
-
if (!Number.isInteger(requestTimeoutMs) ||
|
|
34
|
-
requestTimeoutMs < 1 ||
|
|
35
|
-
requestTimeoutMs > 120_000)
|
|
36
|
-
throw new Error("Catalog release timeout must be 1-120000 milliseconds");
|
|
37
|
-
let current = new URL(source);
|
|
38
|
-
const signal = AbortSignal.timeout(requestTimeoutMs);
|
|
39
|
-
let response;
|
|
40
|
-
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
41
|
-
try {
|
|
42
|
-
response = await fetcher(current, {
|
|
43
|
-
headers: { accept: "application/json" },
|
|
44
|
-
redirect: "manual",
|
|
45
|
-
signal,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (signal.aborted)
|
|
50
|
-
throw new Error(`Catalog release request timed out after ${requestTimeoutMs}ms`, { cause: error });
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
if (response.status < 300 || response.status >= 400)
|
|
54
|
-
break;
|
|
55
|
-
const location = response.headers.get("location");
|
|
56
|
-
if (!location)
|
|
57
|
-
throw new Error("Catalog release redirect has no Location");
|
|
58
|
-
if (redirects === 5)
|
|
59
|
-
throw new Error("Catalog release exceeded five redirects");
|
|
60
|
-
const next = new URL(location, current);
|
|
61
|
-
if (next.protocol !== "https:" || next.username || next.password)
|
|
62
|
-
throw new Error("Catalog release redirected to a non-HTTPS source");
|
|
63
|
-
current = next;
|
|
64
|
-
}
|
|
65
|
-
if (!response)
|
|
66
|
-
throw new Error("Catalog release request did not run");
|
|
67
|
-
if (!response.ok)
|
|
68
|
-
throw new Error(`Catalog release request failed with HTTP ${response.status}`);
|
|
69
|
-
if (response.url && new URL(response.url).protocol !== "https:")
|
|
70
|
-
throw new Error("Catalog release redirected to a non-HTTPS source");
|
|
71
|
-
const length = Number(response.headers.get("content-length") ?? 0);
|
|
72
|
-
if (length > MAX_RELEASE_BYTES)
|
|
73
|
-
throw new Error("Catalog release exceeds the 5 MiB limit");
|
|
74
|
-
if (!response.body)
|
|
75
|
-
throw new Error("Catalog release response has no body");
|
|
76
|
-
const reader = response.body.getReader();
|
|
77
|
-
const chunks = [];
|
|
78
|
-
let bytes = 0;
|
|
79
|
-
while (true) {
|
|
80
|
-
const { done, value } = await reader.read();
|
|
81
|
-
if (done)
|
|
82
|
-
break;
|
|
83
|
-
bytes += value.byteLength;
|
|
84
|
-
if (bytes > MAX_RELEASE_BYTES) {
|
|
85
|
-
await reader.cancel();
|
|
86
|
-
throw new Error("Catalog release exceeds the 5 MiB limit");
|
|
87
|
-
}
|
|
88
|
-
chunks.push(value);
|
|
89
|
-
}
|
|
90
|
-
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
91
|
-
}
|
|
92
|
-
if (/^[a-z]+:\/\//i.test(source))
|
|
93
|
-
throw new Error("Remote catalog sources must use HTTPS");
|
|
94
|
-
return readBoundedFile(resolve(source), MAX_RELEASE_BYTES, "catalog release");
|
|
95
|
-
}
|
|
96
|
-
async function readBoundedFile(path, maximum, label) {
|
|
97
|
-
const info = await stat(path);
|
|
98
|
-
if (!info.isFile() || info.size > maximum)
|
|
99
|
-
throw new Error(`${label} exceeds the ${maximum} byte limit`);
|
|
100
|
-
const content = await readFile(path, "utf8");
|
|
101
|
-
if (Buffer.byteLength(content) > maximum)
|
|
102
|
-
throw new Error(`${label} exceeds the ${maximum} byte limit`);
|
|
103
|
-
return content;
|
|
104
|
-
}
|
|
105
|
-
function diffCatalogs(current, candidate) {
|
|
106
|
-
const currentById = new Map(current.map((item) => [item.id, item]));
|
|
107
|
-
const candidateById = new Map(candidate.map((item) => [item.id, item]));
|
|
108
|
-
const added = candidate
|
|
109
|
-
.filter((item) => !currentById.has(item.id))
|
|
110
|
-
.map((item) => item.id)
|
|
111
|
-
.sort();
|
|
112
|
-
const removed = current
|
|
113
|
-
.filter((item) => !candidateById.has(item.id))
|
|
114
|
-
.map((item) => item.id)
|
|
115
|
-
.sort();
|
|
116
|
-
const updated = candidate
|
|
117
|
-
.filter((item) => {
|
|
118
|
-
const previous = currentById.get(item.id);
|
|
119
|
-
return previous && !isDeepStrictEqual(previous, item);
|
|
120
|
-
})
|
|
121
|
-
.map((item) => item.id)
|
|
122
|
-
.sort();
|
|
123
|
-
return {
|
|
124
|
-
added,
|
|
125
|
-
updated,
|
|
126
|
-
removed,
|
|
127
|
-
unchanged: candidate.length - added.length - updated.length,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
export async function previewCatalogRelease(options) {
|
|
131
|
-
const [raw, publicKeyPem, trusted, trust] = await Promise.all([
|
|
132
|
-
readReleaseSource(options.source, options.fetcher, options.requestTimeoutMs),
|
|
133
|
-
readBoundedFile(resolve(options.publicKeyPath), MAX_PUBLIC_KEY_BYTES, "public key"),
|
|
134
|
-
readTrustedCatalogState(),
|
|
135
|
-
readCatalogTrustState(),
|
|
136
|
-
]);
|
|
137
|
-
let parsed;
|
|
138
|
-
try {
|
|
139
|
-
parsed = JSON.parse(raw);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
throw new Error("Catalog release is not valid JSON");
|
|
143
|
-
}
|
|
144
|
-
const envelope = parseEnvelope(parsed);
|
|
145
|
-
const verification = verifyEnvelope(envelope, publicKeyPem);
|
|
146
|
-
if (!verification.valid)
|
|
147
|
-
throw new Error(`Catalog release signature is invalid for trusted key ${verification.fingerprint}`);
|
|
148
|
-
const pendingIsActive = Boolean(trust?.pendingCreatedAt &&
|
|
149
|
-
trusted?.envelope.createdAt === trust.pendingCreatedAt &&
|
|
150
|
-
envelopeDigest(trusted.envelope) === trust.pendingEnvelopeSha256);
|
|
151
|
-
const effectiveTrust = trust?.pendingFirstPin && !pendingIsActive ? undefined : trust;
|
|
152
|
-
if (effectiveTrust && verification.fingerprint !== effectiveTrust.fingerprint)
|
|
153
|
-
throw new Error(`Catalog signer ${verification.fingerprint} does not match pinned key ${effectiveTrust.fingerprint}; key rotation requires an explicit trust reset`);
|
|
154
|
-
validateCatalog(envelope.payload, { requireEvidence: true });
|
|
155
|
-
const catalogEnvelope = envelope;
|
|
156
|
-
const previousTime = Math.max(trusted ? Date.parse(trusted.envelope.createdAt) : -Infinity, effectiveTrust ? Date.parse(effectiveTrust.highWaterCreatedAt) : -Infinity);
|
|
157
|
-
return {
|
|
158
|
-
source: options.source,
|
|
159
|
-
createdAt: catalogEnvelope.createdAt,
|
|
160
|
-
fingerprint: verification.fingerprint,
|
|
161
|
-
packageCount: catalogEnvelope.payload.length,
|
|
162
|
-
diff: diffCatalogs(options.currentCatalog, catalogEnvelope.payload),
|
|
163
|
-
replay: Date.parse(catalogEnvelope.createdAt) <= previousTime,
|
|
164
|
-
envelope: catalogEnvelope,
|
|
165
|
-
publicKeyPem,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
export async function applyCatalogRelease(preview, options = {}) {
|
|
169
|
-
return withFileLock(catalogReleaseLockPath(), async () => {
|
|
170
|
-
await recoverPendingTransactions();
|
|
171
|
-
const verification = verifyEnvelope(preview.envelope, preview.publicKeyPem);
|
|
172
|
-
if (!verification.valid || verification.fingerprint !== preview.fingerprint)
|
|
173
|
-
throw new Error("Catalog release preview no longer has a valid trusted signature");
|
|
174
|
-
validateCatalog(preview.envelope.payload, { requireEvidence: true });
|
|
175
|
-
let trust = await readCatalogTrustState();
|
|
176
|
-
const trusted = await readTrustedCatalogState();
|
|
177
|
-
let exactPending;
|
|
178
|
-
if (trust?.pendingCreatedAt) {
|
|
179
|
-
const pendingIsActive = trusted?.envelope.createdAt === trust.pendingCreatedAt &&
|
|
180
|
-
envelopeDigest(trusted.envelope) === trust.pendingEnvelopeSha256;
|
|
181
|
-
if (pendingIsActive &&
|
|
182
|
-
envelopeDigest(preview.envelope) === trust.pendingEnvelopeSha256)
|
|
183
|
-
exactPending = trusted;
|
|
184
|
-
const base = { ...trust };
|
|
185
|
-
delete base.pendingCreatedAt;
|
|
186
|
-
delete base.pendingEnvelopeSha256;
|
|
187
|
-
delete base.pendingFirstPin;
|
|
188
|
-
if (!pendingIsActive && trust.pendingFirstPin) {
|
|
189
|
-
await rm(catalogTrustPath(), { force: true });
|
|
190
|
-
trust = undefined;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
trust = {
|
|
194
|
-
...base,
|
|
195
|
-
...(pendingIsActive
|
|
196
|
-
? {
|
|
197
|
-
highWaterCreatedAt: new Date(Math.max(Date.parse(trust.highWaterCreatedAt), Date.parse(trust.pendingCreatedAt))).toISOString(),
|
|
198
|
-
}
|
|
199
|
-
: {}),
|
|
200
|
-
};
|
|
201
|
-
await writeFileAtomically(catalogTrustPath(), `${JSON.stringify(trust, null, 2)}\n`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (exactPending) {
|
|
205
|
-
if (!exactPending.snapshotId)
|
|
206
|
-
throw new Error("Finalized the active signed catalog, but its legacy state has no rollback snapshot id");
|
|
207
|
-
return {
|
|
208
|
-
path: trustedCatalogPath(),
|
|
209
|
-
snapshotId: exactPending.snapshotId,
|
|
210
|
-
diff: diffCatalogs(await loadEffectiveCatalog(), preview.envelope.payload),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (trust && verification.fingerprint !== trust.fingerprint)
|
|
214
|
-
throw new Error(`Catalog signer ${verification.fingerprint} does not match pinned key ${trust.fingerprint}; key rotation requires an explicit trust reset`);
|
|
215
|
-
const previousTime = Math.max(trusted ? Date.parse(trusted.envelope.createdAt) : -Infinity, trust ? Date.parse(trust.highWaterCreatedAt) : -Infinity);
|
|
216
|
-
if (Date.parse(preview.envelope.createdAt) <= previousTime)
|
|
217
|
-
throw new Error("Refusing an older or already-applied signed catalog release");
|
|
218
|
-
const diff = diffCatalogs(await loadEffectiveCatalog(), preview.envelope.payload);
|
|
219
|
-
if (diff.removed.length && !options.allowRemovals)
|
|
220
|
-
throw new Error(`Release removes ${diff.removed.length} package(s); review and pass --allow-removals explicitly`);
|
|
221
|
-
const now = options.now ?? new Date();
|
|
222
|
-
const target = trustedCatalogPath();
|
|
223
|
-
const pinned = trust ??
|
|
224
|
-
{
|
|
225
|
-
schemaVersion: 1,
|
|
226
|
-
fingerprint: verification.fingerprint,
|
|
227
|
-
publicKeyPem: preview.publicKeyPem,
|
|
228
|
-
highWaterCreatedAt: new Date(0).toISOString(),
|
|
229
|
-
pinnedAt: now.toISOString(),
|
|
230
|
-
};
|
|
231
|
-
await ensureDirectory(dirname(target));
|
|
232
|
-
const snapshot = await createSnapshot([target], { persist: false });
|
|
233
|
-
const transaction = await beginTransaction(snapshot, [target]);
|
|
234
|
-
const state = {
|
|
235
|
-
schemaVersion: 1,
|
|
236
|
-
appliedAt: now.toISOString(),
|
|
237
|
-
source: preview.source,
|
|
238
|
-
publicKeyPem: pinned.publicKeyPem,
|
|
239
|
-
envelope: preview.envelope,
|
|
240
|
-
snapshotId: snapshot.id,
|
|
241
|
-
};
|
|
242
|
-
try {
|
|
243
|
-
await writeFileAtomically(catalogTrustPath(), `${JSON.stringify({
|
|
244
|
-
...pinned,
|
|
245
|
-
pendingCreatedAt: preview.envelope.createdAt,
|
|
246
|
-
pendingEnvelopeSha256: envelopeDigest(preview.envelope),
|
|
247
|
-
...(!trust ? { pendingFirstPin: true } : {}),
|
|
248
|
-
}, null, 2)}\n`);
|
|
249
|
-
await markTransactionCommitting(transaction);
|
|
250
|
-
await writeFileAtomically(target, `${JSON.stringify(state, null, 2)}\n`);
|
|
251
|
-
await recordSnapshotPostMutationState(snapshot);
|
|
252
|
-
await completeTransaction(transaction, { releaseLock: false });
|
|
253
|
-
await writeFileAtomically(catalogTrustPath(), `${JSON.stringify({
|
|
254
|
-
...pinned,
|
|
255
|
-
highWaterCreatedAt: preview.envelope.createdAt,
|
|
256
|
-
}, null, 2)}\n`);
|
|
257
|
-
await transaction.mutationLock.release();
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
await rollbackTransaction(transaction);
|
|
261
|
-
if (!trust)
|
|
262
|
-
await rm(catalogTrustPath(), { force: true });
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
return { path: target, snapshotId: snapshot.id, diff };
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
export function formatCatalogReleasePreview(preview) {
|
|
269
|
-
return [
|
|
270
|
-
`Signed catalog: ${preview.packageCount} package(s)`,
|
|
271
|
-
`Created: ${preview.createdAt}`,
|
|
272
|
-
`Trusted key: ${preview.fingerprint}`,
|
|
273
|
-
`Diff: +${preview.diff.added.length} ~${preview.diff.updated.length} -${preview.diff.removed.length} =${preview.diff.unchanged}`,
|
|
274
|
-
...(preview.diff.added.length
|
|
275
|
-
? [`Added: ${preview.diff.added.join(", ")}`]
|
|
276
|
-
: []),
|
|
277
|
-
...(preview.diff.updated.length
|
|
278
|
-
? [`Updated: ${preview.diff.updated.join(", ")}`]
|
|
279
|
-
: []),
|
|
280
|
-
...(preview.diff.removed.length
|
|
281
|
-
? [`Removed: ${preview.diff.removed.join(", ")}`]
|
|
282
|
-
: []),
|
|
283
|
-
preview.replay
|
|
284
|
-
? "Status: older or already applied; apply will be refused"
|
|
285
|
-
: "Status: signature and evidence valid; no changes applied yet",
|
|
286
|
-
].join("\n");
|
|
287
|
-
}
|