@proofofwork-agency/toolpin 0.2.3

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.
Files changed (61) hide show
  1. package/CONTRIBUTING.md +117 -0
  2. package/LICENSE +183 -0
  3. package/README.md +323 -0
  4. package/SECURITY.md +61 -0
  5. package/action.yml +134 -0
  6. package/dist/canonicalJson.js +38 -0
  7. package/dist/capabilities.js +139 -0
  8. package/dist/ci.js +26 -0
  9. package/dist/cli.js +1843 -0
  10. package/dist/clientSupport.js +76 -0
  11. package/dist/codexToml.js +213 -0
  12. package/dist/config.js +337 -0
  13. package/dist/constants.js +3 -0
  14. package/dist/continueYaml.js +76 -0
  15. package/dist/doctor.js +163 -0
  16. package/dist/install.js +191 -0
  17. package/dist/installed.js +405 -0
  18. package/dist/integrity.js +14 -0
  19. package/dist/inventory.js +169 -0
  20. package/dist/packageIntegrity.js +153 -0
  21. package/dist/plan.js +595 -0
  22. package/dist/policy.js +310 -0
  23. package/dist/registry.js +1610 -0
  24. package/dist/runtimeAdvisory.js +80 -0
  25. package/dist/safeFetch.js +157 -0
  26. package/dist/sarif.js +162 -0
  27. package/dist/scan.js +113 -0
  28. package/dist/search.js +44 -0
  29. package/dist/secrets.js +165 -0
  30. package/dist/signing.js +146 -0
  31. package/dist/tester.js +240 -0
  32. package/dist/trust.js +528 -0
  33. package/dist/tui/app.js +1731 -0
  34. package/dist/tui/command.js +50 -0
  35. package/dist/tui/configSnippet.js +11 -0
  36. package/dist/tui/constants.js +37 -0
  37. package/dist/tui/format.js +31 -0
  38. package/dist/tui/installedState.js +23 -0
  39. package/dist/tui/layout.js +65 -0
  40. package/dist/tui/selectors.js +282 -0
  41. package/dist/tui/types.js +1 -0
  42. package/dist/tui/ui/trust.js +77 -0
  43. package/dist/tui/views/installed.js +82 -0
  44. package/dist/tui/views/panels.js +637 -0
  45. package/dist/tui.js +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/verificationTrust.js +103 -0
  48. package/dist/verify.js +537 -0
  49. package/dist/version.js +1 -0
  50. package/dist/versions.js +127 -0
  51. package/docs/assets/readme/terminal-demo.svg +174 -0
  52. package/docs/assets/readme/tui-browse-overview.jpg +0 -0
  53. package/docs/assets/readme/tui-config-preview.jpg +0 -0
  54. package/docs/assets/readme/tui-help.jpg +0 -0
  55. package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
  56. package/docs/how-to/catch-drift-in-ci.md +189 -0
  57. package/docs/how-to/custom-registries.md +156 -0
  58. package/docs/how-to/toolpin-curated-registry.md +153 -0
  59. package/package.json +76 -0
  60. package/registry/README.md +92 -0
  61. package/registry/v0/servers +115 -0
package/dist/plan.js ADDED
@@ -0,0 +1,595 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, readFile, writeFile } from "node:fs/promises";
3
+ import { deriveCapabilityManifest, isCapabilityManifest } from "./capabilities.js";
4
+ import { canonicalJson } from "./canonicalJson.js";
5
+ import { exportClientConfig, isClientName, selectLaunchTarget } from "./config.js";
6
+ import { regateTrustReport, scoreServer, trustTier } from "./trust.js";
7
+ export const LOCKFILE_VERSION = 2;
8
+ export function buildInstallPlan(server, client, options = {}) {
9
+ if (server.installable === false) {
10
+ throw new Error(`Cannot install ${server.name}@${server.version}: ${server.installableReason ?? "registry entry is discovery-only"}.`);
11
+ }
12
+ const selected = selectLaunchTarget(server);
13
+ if (!selected) {
14
+ throw new Error(`No install target is available for ${server.name}@${server.version}`);
15
+ }
16
+ const exported = exportClientConfig(server, client);
17
+ const target = selected.kind === "remote"
18
+ ? {
19
+ kind: "remote",
20
+ type: selected.remote.type,
21
+ url: selected.remote.url,
22
+ }
23
+ : {
24
+ kind: "package",
25
+ registryType: selected.pkg.registryType,
26
+ identifier: selected.pkg.identifier,
27
+ version: selected.pkg.version,
28
+ fileSha256: selected.pkg.fileSha256,
29
+ transport: selected.pkg.transport?.type,
30
+ };
31
+ const resolvedAt = new Date().toISOString();
32
+ const capabilityManifest = options.capabilityManifest ?? deriveCapabilityManifest(server, { generatedAt: resolvedAt });
33
+ const trust = options.verificationReport ? mergeVerificationTrust(scoreServer(server), options.verificationReport) : scoreServer(server);
34
+ const plan = {
35
+ name: server.name,
36
+ version: server.version,
37
+ client,
38
+ scope: options.scope ?? "project",
39
+ selectedTarget: target,
40
+ trust,
41
+ config: exported.config,
42
+ notes: exported.notes,
43
+ capabilityManifest,
44
+ resolvedAt,
45
+ lockedAt: resolvedAt,
46
+ resolved: {
47
+ source: server.registrySource,
48
+ name: server.name,
49
+ version: server.version,
50
+ },
51
+ original: {
52
+ name: server.name,
53
+ version: server.version,
54
+ client,
55
+ },
56
+ locked: {
57
+ selectedTarget: target,
58
+ config: exported.config,
59
+ capabilityManifest,
60
+ },
61
+ };
62
+ return { ...plan, integrity: computePlanIntegrity(plan) };
63
+ }
64
+ function mergeVerificationTrust(base, report) {
65
+ const evidence = dedupeTrustEvidence([...(base.evidence ?? []), ...report.evidence]);
66
+ const issues = dedupeTrustIssues([...base.issues, ...report.issues]);
67
+ const badges = [...new Set([...base.badges, ...report.badges])];
68
+ return regateTrustReport({
69
+ ...base,
70
+ evidence,
71
+ issues,
72
+ badges,
73
+ verifiedProvenance: report.verifiedProvenance === true,
74
+ });
75
+ }
76
+ function dedupeTrustEvidence(evidence) {
77
+ const byKey = new Map();
78
+ for (const entry of evidence) {
79
+ const key = `${entry.code}:${entry.status}:${entry.message}`;
80
+ if (!byKey.has(key))
81
+ byKey.set(key, entry);
82
+ }
83
+ return [...byKey.values()];
84
+ }
85
+ function dedupeTrustIssues(issues) {
86
+ const byKey = new Map();
87
+ for (const issue of issues) {
88
+ const key = `${issue.severity}:${issue.code}:${issue.message}`;
89
+ if (!byKey.has(key))
90
+ byKey.set(key, issue);
91
+ }
92
+ return [...byKey.values()];
93
+ }
94
+ export async function writeLockfile(plan, path = "mcp-lock.json", key = lockKey(plan.name, plan.client)) {
95
+ const existing = await readExistingLockfile(path);
96
+ const now = new Date().toISOString();
97
+ const entry = finalizeLockEntry(plan, now);
98
+ const next = {
99
+ lockfileVersion: LOCKFILE_VERSION,
100
+ generatedAt: existing.generatedAt === new Date(0).toISOString() ? now : existing.generatedAt,
101
+ updatedAt: now,
102
+ servers: {
103
+ ...existing.servers,
104
+ [key]: entry,
105
+ },
106
+ };
107
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
108
+ return next;
109
+ }
110
+ export async function removeLockfileEntry(serverName, client, path = "mcp-lock.json") {
111
+ const existed = await fileExists(path);
112
+ const existing = await readExistingLockfile(path);
113
+ const nextServers = { ...existing.servers };
114
+ const key = lockKey(serverName, client);
115
+ let removed = false;
116
+ for (const candidate of [key, serverName]) {
117
+ const entry = nextServers[candidate];
118
+ if (entry?.name === serverName && entry.client === client) {
119
+ delete nextServers[candidate];
120
+ removed = true;
121
+ }
122
+ }
123
+ if (!removed && !existed) {
124
+ return { removed, key, lockfile: existing };
125
+ }
126
+ const now = new Date().toISOString();
127
+ const next = {
128
+ lockfileVersion: LOCKFILE_VERSION,
129
+ generatedAt: existing.generatedAt === new Date(0).toISOString() ? now : existing.generatedAt,
130
+ updatedAt: now,
131
+ servers: nextServers,
132
+ };
133
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
134
+ return { removed, key, lockfile: next };
135
+ }
136
+ export async function readLockfile(path = "mcp-lock.json") {
137
+ return readExistingLockfile(path);
138
+ }
139
+ export async function readLockfileDigest(path = "mcp-lock.json") {
140
+ return computeLockfileDigest(await readExistingLockfile(path));
141
+ }
142
+ export function computeLockfileDigest(lockfile) {
143
+ return `sha256-${createHash("sha256").update(stableJson(lockfileDigestPayload(lockfile))).digest("base64")}`;
144
+ }
145
+ export async function verifyAgainstLockfile(plan, path = "mcp-lock.json") {
146
+ const lockfile = await readExistingLockfile(path);
147
+ const key = lockKey(plan.name, plan.client);
148
+ const locked = lockfile.servers[key] ?? lockfile.servers[plan.name];
149
+ if (!locked) {
150
+ return { ok: true, key, messages: [] };
151
+ }
152
+ const messages = diffInstallPlans(locked, plan);
153
+ return { ok: messages.length === 0, key, messages, locked };
154
+ }
155
+ export function lockKey(serverName, client) {
156
+ return `${serverName}:${client}`;
157
+ }
158
+ async function readExistingLockfile(path) {
159
+ try {
160
+ const raw = await readFile(path, "utf8");
161
+ const parsed = JSON.parse(raw);
162
+ const lockfile = parseLockfile(parsed);
163
+ if (lockfile)
164
+ return lockfile;
165
+ throw new Error(`Invalid lockfile schema in ${path}`);
166
+ }
167
+ catch (error) {
168
+ if (error.code === "ENOENT") {
169
+ return emptyLockfile();
170
+ }
171
+ if (error instanceof SyntaxError) {
172
+ throw new Error(`Invalid lockfile JSON in ${path}: ${error.message}`);
173
+ }
174
+ throw error;
175
+ }
176
+ return emptyLockfile();
177
+ }
178
+ function emptyLockfile() {
179
+ return {
180
+ lockfileVersion: LOCKFILE_VERSION,
181
+ generatedAt: new Date(0).toISOString(),
182
+ servers: {},
183
+ };
184
+ }
185
+ function parseLockfile(value) {
186
+ if (!isRecord(value))
187
+ return undefined;
188
+ if (value.lockfileVersion === 1) {
189
+ throw new Error("Unsupported lockfileVersion 1 in v0.1; regenerate the lockfile with the current ToolPin release.");
190
+ }
191
+ if (value.lockfileVersion !== LOCKFILE_VERSION)
192
+ return undefined;
193
+ if (typeof value.generatedAt !== "string")
194
+ return undefined;
195
+ if (!isRecord(value.servers))
196
+ return undefined;
197
+ const servers = {};
198
+ for (const [key, entry] of Object.entries(value.servers)) {
199
+ servers[key] = parseInstallPlan(entry, key);
200
+ }
201
+ return {
202
+ lockfileVersion: value.lockfileVersion,
203
+ generatedAt: value.generatedAt,
204
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : undefined,
205
+ servers,
206
+ };
207
+ }
208
+ function parseInstallPlan(value, key) {
209
+ if (!isRecord(value))
210
+ throw new Error(`Invalid lockfile entry ${key}: expected object`);
211
+ if (typeof value.name !== "string")
212
+ throw new Error(`Invalid lockfile entry ${key}: missing name`);
213
+ if (typeof value.version !== "string")
214
+ throw new Error(`Invalid lockfile entry ${key}: missing version`);
215
+ if (!isClientName(value.client))
216
+ throw new Error(`Invalid lockfile entry ${key}: invalid client`);
217
+ if (!isRecord(value.selectedTarget))
218
+ throw new Error(`Invalid lockfile entry ${key}: invalid selectedTarget`);
219
+ const trust = parseTrust(value.trust, key);
220
+ if (!Array.isArray(value.notes))
221
+ throw new Error(`Invalid lockfile entry ${key}: invalid notes`);
222
+ if (value.capabilityManifest !== undefined && !isCapabilityManifest(value.capabilityManifest))
223
+ throw new Error(`Invalid lockfile entry ${key}: invalid capabilityManifest`);
224
+ if (value.integrity !== undefined && typeof value.integrity !== "string")
225
+ throw new Error(`Invalid lockfile entry ${key}: invalid integrity`);
226
+ return {
227
+ name: value.name,
228
+ version: value.version,
229
+ client: value.client,
230
+ scope: parseScope(value.scope, key),
231
+ selectedTarget: value.selectedTarget,
232
+ trust,
233
+ config: value.config,
234
+ notes: value.notes.filter((note) => typeof note === "string"),
235
+ capabilityManifest: value.capabilityManifest,
236
+ resolvedAt: typeof value.resolvedAt === "string" ? value.resolvedAt : new Date(0).toISOString(),
237
+ lockedAt: typeof value.lockedAt === "string" ? value.lockedAt : undefined,
238
+ resolved: parseResolved(value.resolved),
239
+ original: parseOriginal(value.original),
240
+ locked: parseLocked(value.locked),
241
+ integrity: value.integrity,
242
+ };
243
+ }
244
+ function parseTrust(value, key) {
245
+ if (!isRecord(value))
246
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust`);
247
+ if (typeof value.score !== "number" || !Number.isFinite(value.score))
248
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.score`);
249
+ if (value.tier !== undefined && !isTrustTier(value.tier))
250
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.tier`);
251
+ if (value.gatedBy !== undefined && (!Array.isArray(value.gatedBy) || !value.gatedBy.every((code) => typeof code === "string")))
252
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.gatedBy`);
253
+ if (value.evidence !== undefined && !Array.isArray(value.evidence))
254
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence`);
255
+ if (!Array.isArray(value.badges) || !value.badges.every((badge) => typeof badge === "string"))
256
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.badges`);
257
+ if (!Array.isArray(value.issues))
258
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.issues`);
259
+ if (value.tier !== undefined && !isTrustTier(value.tier))
260
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.tier`);
261
+ if (value.overallScore !== undefined && (typeof value.overallScore !== "number" || !Number.isFinite(value.overallScore)))
262
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.overallScore`);
263
+ if (value.metadataCompleteness !== undefined && (typeof value.metadataCompleteness !== "number" || !Number.isFinite(value.metadataCompleteness)))
264
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.metadataCompleteness`);
265
+ if (value.capReason !== undefined && typeof value.capReason !== "string")
266
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.capReason`);
267
+ if (value.verifiedProvenance !== undefined && typeof value.verifiedProvenance !== "boolean")
268
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.verifiedProvenance`);
269
+ if (value.gates !== undefined && (!Array.isArray(value.gates) || !value.gates.every(isTrustGate)))
270
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.gates`);
271
+ if (value.vetoes !== undefined && (!Array.isArray(value.vetoes) || !value.vetoes.every(isTrustGate)))
272
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.vetoes`);
273
+ if (value.pillars !== undefined && !isTrustPillars(value.pillars))
274
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.pillars`);
275
+ const trust = {
276
+ score: value.score,
277
+ ...(typeof value.overallScore === "number" ? { overallScore: value.overallScore } : {}),
278
+ ...(typeof value.metadataCompleteness === "number" ? { metadataCompleteness: value.metadataCompleteness } : {}),
279
+ ...(value.tier ? { tier: value.tier } : {}),
280
+ ...(typeof value.capReason === "string" ? { capReason: value.capReason } : {}),
281
+ ...(typeof value.verifiedProvenance === "boolean" ? { verifiedProvenance: value.verifiedProvenance } : {}),
282
+ ...(Array.isArray(value.gates) ? { gates: value.gates } : {}),
283
+ ...(Array.isArray(value.vetoes) ? { vetoes: value.vetoes } : {}),
284
+ ...(isTrustPillars(value.pillars) ? { pillars: value.pillars } : {}),
285
+ ...(Array.isArray(value.gatedBy) ? { gatedBy: value.gatedBy } : {}),
286
+ ...(Array.isArray(value.evidence) ? { evidence: value.evidence.map((entry, index) => parseTrustEvidence(entry, key, index)) } : {}),
287
+ badges: value.badges,
288
+ issues: value.issues.map((issue, index) => parseTrustIssue(issue, key, index)),
289
+ };
290
+ return trust;
291
+ }
292
+ function isTrustTier(value) {
293
+ return value === "verified" || value === "conditional" || value === "unverified" || value === "blocked";
294
+ }
295
+ function parseTrustIssue(value, key, index) {
296
+ if (!isRecord(value))
297
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.issues[${index}]`);
298
+ const severity = value.severity;
299
+ if (severity !== "info" && severity !== "warning" && severity !== "critical")
300
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.issues[${index}].severity`);
301
+ if (typeof value.code !== "string")
302
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.issues[${index}].code`);
303
+ if (typeof value.message !== "string")
304
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.issues[${index}].message`);
305
+ return {
306
+ severity,
307
+ code: value.code,
308
+ message: value.message,
309
+ };
310
+ }
311
+ function isTrustGate(value) {
312
+ if (!isRecord(value))
313
+ return false;
314
+ return typeof value.code === "string"
315
+ && typeof value.message === "string"
316
+ && (value.tier === "unverified" || value.tier === "blocked");
317
+ }
318
+ function isTrustPillars(value) {
319
+ if (!isRecord(value))
320
+ return false;
321
+ return typeof value.provenance === "number" && Number.isFinite(value.provenance)
322
+ && typeof value.integrity === "number" && Number.isFinite(value.integrity)
323
+ && typeof value.reputation === "number" && Number.isFinite(value.reputation)
324
+ && typeof value.metadataCompleteness === "number" && Number.isFinite(value.metadataCompleteness);
325
+ }
326
+ function parseTrustEvidence(value, key, index) {
327
+ if (!isRecord(value))
328
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}]`);
329
+ const status = value.status;
330
+ if (status !== "passed" && status !== "declared" && status !== "failed" && status !== "unavailable")
331
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].status`);
332
+ if (typeof value.code !== "string")
333
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].code`);
334
+ if (typeof value.message !== "string")
335
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].message`);
336
+ if (value.required !== undefined && typeof value.required !== "boolean")
337
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].required`);
338
+ if (value.source !== undefined && typeof value.source !== "string")
339
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].source`);
340
+ if (value.claim !== undefined && typeof value.claim !== "string")
341
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].claim`);
342
+ if (value.verificationMethod !== undefined && typeof value.verificationMethod !== "string")
343
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].verificationMethod`);
344
+ if (value.verifiedByToolPin !== undefined && typeof value.verifiedByToolPin !== "boolean")
345
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].verifiedByToolPin`);
346
+ if (value.trustedAnchor !== undefined && typeof value.trustedAnchor !== "boolean")
347
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].trustedAnchor`);
348
+ if (value.trustAnchor !== undefined && typeof value.trustAnchor !== "string")
349
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].trustAnchor`);
350
+ if (value.verifiedAt !== undefined && typeof value.verifiedAt !== "string")
351
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].verifiedAt`);
352
+ if (value.failureReason !== undefined && typeof value.failureReason !== "string")
353
+ throw new Error(`Invalid lockfile entry ${key}: invalid trust.evidence[${index}].failureReason`);
354
+ return {
355
+ code: value.code,
356
+ status,
357
+ message: value.message,
358
+ ...(typeof value.source === "string" ? { source: value.source } : {}),
359
+ ...(typeof value.claim === "string" ? { claim: value.claim } : {}),
360
+ ...(typeof value.verificationMethod === "string" ? { verificationMethod: value.verificationMethod } : {}),
361
+ ...(typeof value.verifiedByToolPin === "boolean" ? { verifiedByToolPin: value.verifiedByToolPin } : {}),
362
+ ...(typeof value.trustedAnchor === "boolean" ? { trustedAnchor: value.trustedAnchor } : {}),
363
+ ...(typeof value.trustAnchor === "string" ? { trustAnchor: value.trustAnchor } : {}),
364
+ ...(typeof value.verifiedAt === "string" ? { verifiedAt: value.verifiedAt } : {}),
365
+ ...(typeof value.failureReason === "string" ? { failureReason: value.failureReason } : {}),
366
+ ...(typeof value.required === "boolean" ? { required: value.required } : {}),
367
+ };
368
+ }
369
+ function diffInstallPlans(locked, current) {
370
+ const messages = [];
371
+ if (!locked.integrity)
372
+ messages.push("lock integrity is missing");
373
+ if (locked.integrity && computePlanIntegrity(locked) !== locked.integrity)
374
+ messages.push("locked entry integrity does not match its contents");
375
+ if (locked.version !== current.version)
376
+ messages.push(`version changed ${locked.version} -> ${current.version}`);
377
+ if ((locked.scope ?? "project") !== (current.scope ?? "project"))
378
+ messages.push(`scope changed ${locked.scope ?? "project"} -> ${current.scope ?? "project"}`);
379
+ if (stableJson(locked.selectedTarget) !== stableJson(current.selectedTarget))
380
+ messages.push("selected install target changed");
381
+ if (locked.trust.score > current.trust.score)
382
+ messages.push(`trust score decreased ${locked.trust.score} -> ${current.trust.score}`);
383
+ if (isTrustTierDowngrade(trustTier(locked.trust), trustTier(current.trust)))
384
+ messages.push(`trust tier decreased ${trustTier(locked.trust)} -> ${trustTier(current.trust)}`);
385
+ if (stableJson(locked.config) !== stableJson(current.config))
386
+ messages.push("client config changed");
387
+ if (stableJson(normalizeCapabilityManifestBase(locked.capabilityManifest)) !== stableJson(normalizeCapabilityManifestBase(current.capabilityManifest)))
388
+ messages.push("capability manifest changed");
389
+ if (hasToolDescriptionHash(locked.capabilityManifest) && hasToolDescriptionHash(current.capabilityManifest)) {
390
+ if (stableJson(normalizeToolDescriptionHash(locked.capabilityManifest.toolDescriptionHash)) !== stableJson(normalizeToolDescriptionHash(current.capabilityManifest.toolDescriptionHash))) {
391
+ messages.push("tool-description hash changed");
392
+ }
393
+ }
394
+ else if (hasToolDescriptionHash(locked.capabilityManifest) && !hasToolDescriptionHash(current.capabilityManifest)) {
395
+ messages.push("tool-description hash pin could not be refreshed");
396
+ }
397
+ if (hasToolManifestHash(locked.capabilityManifest) && hasToolManifestHash(current.capabilityManifest)) {
398
+ if (stableJson(normalizeToolManifestHash(locked.capabilityManifest.toolManifestHash)) !== stableJson(normalizeToolManifestHash(current.capabilityManifest.toolManifestHash))) {
399
+ messages.push("tool-manifest hash changed");
400
+ }
401
+ }
402
+ else if (hasToolManifestHash(locked.capabilityManifest) && !hasToolManifestHash(current.capabilityManifest)) {
403
+ messages.push("tool-manifest hash pin could not be refreshed");
404
+ }
405
+ return messages;
406
+ }
407
+ function isTrustTierDowngrade(locked, current) {
408
+ const rank = {
409
+ verified: 3,
410
+ conditional: 2,
411
+ unverified: 1,
412
+ blocked: 0,
413
+ };
414
+ return rank[current] < rank[locked];
415
+ }
416
+ export function computePlanIntegrity(plan) {
417
+ return `sha256-${createHash("sha256").update(stableJson(integrityPayload(plan))).digest("base64")}`;
418
+ }
419
+ function finalizeLockEntry(plan, lockedAt) {
420
+ const trust = withLockIntegrityEvidence(plan.trust);
421
+ const next = {
422
+ ...plan,
423
+ scope: plan.scope ?? "project",
424
+ trust,
425
+ resolvedAt: plan.resolvedAt ?? lockedAt,
426
+ lockedAt,
427
+ resolved: plan.resolved ?? {
428
+ source: plan.capabilityManifest?.registrySource ?? "official",
429
+ name: plan.name,
430
+ version: plan.version,
431
+ },
432
+ original: plan.original ?? {
433
+ name: plan.name,
434
+ version: plan.version,
435
+ client: plan.client,
436
+ },
437
+ locked: plan.locked ?? {
438
+ selectedTarget: plan.selectedTarget,
439
+ config: plan.config,
440
+ capabilityManifest: plan.capabilityManifest,
441
+ },
442
+ };
443
+ return { ...next, integrity: computePlanIntegrity(next) };
444
+ }
445
+ function withLockIntegrityEvidence(report) {
446
+ const evidence = [
447
+ ...(report.evidence ?? []),
448
+ {
449
+ code: "lock_integrity",
450
+ status: "passed",
451
+ message: "Lock entry integrity digest is computed over the reviewed install plan.",
452
+ source: "local-lockfile",
453
+ claim: "install plan integrity",
454
+ verificationMethod: "canonical-json-sha256",
455
+ verifiedByToolPin: true,
456
+ },
457
+ ];
458
+ const uniqueEvidence = dedupeTrustEvidence(evidence);
459
+ return regateTrustReport({
460
+ ...report,
461
+ evidence: uniqueEvidence,
462
+ });
463
+ }
464
+ function integrityPayload(plan) {
465
+ return {
466
+ name: plan.name,
467
+ version: plan.version,
468
+ client: plan.client,
469
+ scope: plan.scope ?? "project",
470
+ selectedTarget: plan.selectedTarget,
471
+ trust: plan.trust,
472
+ config: plan.config,
473
+ notes: plan.notes,
474
+ capabilityManifest: normalizeCapabilityManifest(plan.capabilityManifest),
475
+ resolvedAt: plan.resolvedAt,
476
+ lockedAt: plan.lockedAt,
477
+ resolved: plan.resolved,
478
+ original: plan.original,
479
+ locked: {
480
+ selectedTarget: plan.locked?.selectedTarget,
481
+ config: plan.locked?.config,
482
+ capabilityManifest: normalizeCapabilityManifest(plan.locked?.capabilityManifest),
483
+ },
484
+ };
485
+ }
486
+ function lockfileDigestPayload(lockfile) {
487
+ return {
488
+ lockfileVersion: lockfile.lockfileVersion,
489
+ servers: Object.fromEntries(Object.entries(lockfile.servers).map(([key, plan]) => [
490
+ key,
491
+ integrityPayload(plan),
492
+ ])),
493
+ };
494
+ }
495
+ function normalizeCapabilityManifest(manifest) {
496
+ const base = normalizeCapabilityManifestBase(manifest);
497
+ if (!base || (!manifest?.toolDescriptionHash && !manifest?.toolManifestHash))
498
+ return base;
499
+ const output = {
500
+ ...base,
501
+ ...(manifest.toolDescriptionHash ? { toolDescriptionHash: normalizeToolDescriptionHash(manifest.toolDescriptionHash) } : {}),
502
+ ...(manifest.toolManifestHash ? { toolManifestHash: normalizeToolManifestHash(manifest.toolManifestHash) } : {}),
503
+ };
504
+ return output;
505
+ }
506
+ function normalizeCapabilityManifestBase(manifest) {
507
+ if (!manifest)
508
+ return undefined;
509
+ return {
510
+ version: manifest.version,
511
+ serverName: manifest.serverName,
512
+ serverVersion: manifest.serverVersion,
513
+ registrySource: manifest.registrySource,
514
+ packageTypes: manifest.packageTypes,
515
+ transports: manifest.transports,
516
+ remoteHosts: manifest.remoteHosts,
517
+ secrets: manifest.secrets,
518
+ };
519
+ }
520
+ function hasToolDescriptionHash(manifest) {
521
+ return Boolean(manifest?.toolDescriptionHash);
522
+ }
523
+ function hasToolManifestHash(manifest) {
524
+ return Boolean(manifest?.toolManifestHash);
525
+ }
526
+ function normalizeToolDescriptionHash(hash) {
527
+ return {
528
+ algorithm: hash.algorithm,
529
+ value: hash.value,
530
+ toolCount: hash.toolCount,
531
+ };
532
+ }
533
+ function normalizeToolManifestHash(hash) {
534
+ return {
535
+ algorithm: hash.algorithm,
536
+ value: hash.value,
537
+ toolCount: hash.toolCount,
538
+ };
539
+ }
540
+ function stableJson(value) {
541
+ return canonicalJson(value);
542
+ }
543
+ function isRecord(value) {
544
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
545
+ }
546
+ function parseResolved(value) {
547
+ if (!isRecord(value))
548
+ return undefined;
549
+ if (typeof value.source !== "string" || typeof value.name !== "string" || typeof value.version !== "string")
550
+ return undefined;
551
+ return {
552
+ source: value.source,
553
+ name: value.name,
554
+ version: value.version,
555
+ };
556
+ }
557
+ function parseOriginal(value) {
558
+ if (!isRecord(value))
559
+ return undefined;
560
+ if (typeof value.name !== "string" || typeof value.version !== "string" || !isClientName(value.client))
561
+ return undefined;
562
+ return {
563
+ name: value.name,
564
+ version: value.version,
565
+ client: value.client,
566
+ };
567
+ }
568
+ function parseScope(value, key) {
569
+ if (value === undefined)
570
+ return undefined;
571
+ if (value === "project" || value === "global")
572
+ return value;
573
+ throw new Error(`Invalid lockfile entry ${key}: invalid scope`);
574
+ }
575
+ function parseLocked(value) {
576
+ if (!isRecord(value))
577
+ return undefined;
578
+ const capabilityManifest = value.capabilityManifest;
579
+ if (capabilityManifest !== undefined && !isCapabilityManifest(capabilityManifest))
580
+ return undefined;
581
+ return {
582
+ selectedTarget: value.selectedTarget,
583
+ config: value.config,
584
+ capabilityManifest,
585
+ };
586
+ }
587
+ async function fileExists(path) {
588
+ try {
589
+ await access(path);
590
+ return true;
591
+ }
592
+ catch {
593
+ return false;
594
+ }
595
+ }