@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/policy.js ADDED
@@ -0,0 +1,310 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { canonicalJson } from "./canonicalJson.js";
4
+ import { isClientName } from "./config.js";
5
+ import { hasOciDigestMarker, hasValidOciDigestPin, isValidSha256Hex } from "./integrity.js";
6
+ import { hasFreshTrustedArtifactEvidence, trustedArtifactEvidenceProblem, trustTier } from "./trust.js";
7
+ const POLICY_KEYS = new Set([
8
+ "version",
9
+ "minTrustScore",
10
+ "minTrustTier",
11
+ "requireToolPinVerifiedEvidence",
12
+ "allowedSources",
13
+ "deniedSources",
14
+ "allowedClients",
15
+ "deniedClients",
16
+ "deniedServers",
17
+ "deniedPackageTypes",
18
+ "deniedTransports",
19
+ "deniedRemoteHosts",
20
+ "denyRemoteEndpoints",
21
+ "denyRequiredSecrets",
22
+ "requireDigestPinnedOci",
23
+ "requireMcpbSha256",
24
+ ]);
25
+ export async function readPolicy(path = ".toolpin/policy.json") {
26
+ let raw;
27
+ try {
28
+ raw = await readFile(path, "utf8");
29
+ }
30
+ catch (error) {
31
+ if (error.code === "ENOENT")
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ try {
36
+ const parsed = JSON.parse(raw);
37
+ return parsePolicy(parsed, path);
38
+ }
39
+ catch (error) {
40
+ if (error instanceof SyntaxError) {
41
+ throw new Error(`Invalid policy JSON in ${path}: ${error.message}`);
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ export async function readPolicyDigest(path = ".toolpin/policy.json") {
47
+ let raw;
48
+ try {
49
+ raw = await readFile(path, "utf8");
50
+ }
51
+ catch (error) {
52
+ if (error.code === "ENOENT")
53
+ return undefined;
54
+ throw error;
55
+ }
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ }
60
+ catch (error) {
61
+ if (error instanceof SyntaxError) {
62
+ throw new Error(`Invalid policy JSON in ${path}: ${error.message}`);
63
+ }
64
+ throw error;
65
+ }
66
+ const policy = parsePolicy(parsed, path);
67
+ return `sha256-${createHash("sha256").update(canonicalJson(policy)).digest("base64")}`;
68
+ }
69
+ export async function enforcePolicy(plan, path = ".toolpin/policy.json") {
70
+ const policy = await readPolicy(path);
71
+ return evaluatePolicy(plan, policy);
72
+ }
73
+ export function evaluatePolicy(plan, policy) {
74
+ const key = `${plan.name}:${plan.client}`;
75
+ if (!policy)
76
+ return { ok: true, key, issues: [] };
77
+ const issues = [];
78
+ const source = plan.resolved?.source ?? plan.capabilityManifest?.registrySource;
79
+ const packageTypes = plan.capabilityManifest?.packageTypes ?? selectedPackageTypes(plan);
80
+ const transports = plan.capabilityManifest?.transports ?? selectedTransports(plan);
81
+ const remoteHosts = plan.capabilityManifest?.remoteHosts ?? selectedRemoteHosts(plan);
82
+ const requiredSecrets = plan.capabilityManifest?.secrets?.filter((secret) => secret.required) ?? [];
83
+ if (typeof policy.minTrustScore === "number" && plan.trust.score < policy.minTrustScore) {
84
+ issues.push({
85
+ code: "trust_below_minimum",
86
+ message: `${plan.name} trust score ${plan.trust.score} is below required minimum ${policy.minTrustScore}`,
87
+ });
88
+ }
89
+ if (policy.minTrustTier && trustTierRank(trustTier(plan.trust)) < trustTierRank(policy.minTrustTier)) {
90
+ issues.push({
91
+ code: "trust_tier_below_minimum",
92
+ message: `${plan.name} trust tier ${trustTier(plan.trust)} is below required minimum ${policy.minTrustTier}`,
93
+ });
94
+ }
95
+ if (policy.requireToolPinVerifiedEvidence && !hasToolPinVerifiedEvidence(plan)) {
96
+ issues.push({
97
+ code: "toolpin_verified_evidence_required",
98
+ message: `${plan.name} does not have fresh trusted artifact evidence verified by ToolPin (${plan.trust.verifiedProvenance === true ? trustedArtifactEvidenceProblem(plan.trust.evidence ?? []) : "missing verified provenance"})`,
99
+ });
100
+ }
101
+ if (policy.allowedSources?.length && (!source || !policy.allowedSources.includes(source))) {
102
+ issues.push({
103
+ code: "source_not_allowed",
104
+ message: `${plan.name} registry source ${source ?? "unknown"} is not in allowedSources`,
105
+ });
106
+ }
107
+ if (policy.deniedSources?.length && source && policy.deniedSources.includes(source)) {
108
+ issues.push({
109
+ code: "source_denied",
110
+ message: `${plan.name} registry source ${source} is denied`,
111
+ });
112
+ }
113
+ if (policy.allowedClients?.length && !policy.allowedClients.includes(plan.client)) {
114
+ issues.push({
115
+ code: "client_not_allowed",
116
+ message: `${plan.client} is not in allowedClients`,
117
+ });
118
+ }
119
+ if (policy.deniedClients?.includes(plan.client)) {
120
+ issues.push({
121
+ code: "client_denied",
122
+ message: `${plan.client} is denied`,
123
+ });
124
+ }
125
+ if (policy.deniedServers?.includes(plan.name)) {
126
+ issues.push({
127
+ code: "server_denied",
128
+ message: `${plan.name} is denied`,
129
+ });
130
+ }
131
+ for (const packageType of packageTypes) {
132
+ if (policy.deniedPackageTypes?.includes(packageType)) {
133
+ issues.push({
134
+ code: "package_type_denied",
135
+ message: `${plan.name} uses denied package type ${packageType}`,
136
+ });
137
+ }
138
+ }
139
+ for (const transport of transports) {
140
+ if (policy.deniedTransports?.includes(transport)) {
141
+ issues.push({
142
+ code: "transport_denied",
143
+ message: `${plan.name} uses denied transport ${transport}`,
144
+ });
145
+ }
146
+ }
147
+ for (const host of remoteHosts) {
148
+ if (policy.deniedRemoteHosts?.includes(host)) {
149
+ issues.push({
150
+ code: "remote_host_denied",
151
+ message: `${plan.name} uses denied remote host ${host}`,
152
+ });
153
+ }
154
+ }
155
+ if (policy.denyRemoteEndpoints && remoteHosts.length > 0) {
156
+ issues.push({
157
+ code: "remote_endpoint_denied",
158
+ message: `${plan.name} declares remote endpoint host(s): ${remoteHosts.join(", ")}`,
159
+ });
160
+ }
161
+ if (policy.denyRequiredSecrets && requiredSecrets.length > 0) {
162
+ issues.push({
163
+ code: "required_secrets_denied",
164
+ message: `${plan.name} requires secret input(s): ${requiredSecrets.map((secret) => `${secret.source}:${secret.name}`).join(", ")}`,
165
+ });
166
+ }
167
+ if (policy.requireDigestPinnedOci && isSelectedPackage(plan, "oci")) {
168
+ const target = selectedTarget(plan);
169
+ const identifier = typeof target.identifier === "string" ? target.identifier : "";
170
+ if (!hasValidOciDigestPin(identifier)) {
171
+ const detail = hasOciDigestMarker(identifier) ? " with a valid sha256 digest" : " by digest";
172
+ issues.push({
173
+ code: "oci_digest_required",
174
+ message: `${plan.name} OCI target must be pinned${detail}`,
175
+ });
176
+ }
177
+ }
178
+ if (policy.requireMcpbSha256 && isSelectedPackage(plan, "mcpb")) {
179
+ const target = selectedTarget(plan);
180
+ if (!isValidSha256Hex(target.fileSha256)) {
181
+ issues.push({
182
+ code: "mcpb_sha256_required",
183
+ message: `${plan.name} MCPB target must declare a valid 64-character fileSha256`,
184
+ });
185
+ }
186
+ }
187
+ return { ok: issues.length === 0, key, issues, policy };
188
+ }
189
+ function parsePolicy(value, path) {
190
+ if (!isRecord(value))
191
+ throw new Error(`Invalid policy schema in ${path}: expected object`);
192
+ for (const key of Object.keys(value)) {
193
+ if (!POLICY_KEYS.has(key)) {
194
+ throw new Error(`Invalid policy schema in ${path}: unknown policy key ${key}`);
195
+ }
196
+ }
197
+ if (value.version !== undefined && value.version !== 1)
198
+ throw new Error(`Invalid policy schema in ${path}: unsupported version`);
199
+ if (value.minTrustScore !== undefined && (typeof value.minTrustScore !== "number" || value.minTrustScore < 0 || value.minTrustScore > 100)) {
200
+ throw new Error(`Invalid policy schema in ${path}: minTrustScore must be 0-100`);
201
+ }
202
+ if (value.minTrustTier !== undefined && !isTrustTier(value.minTrustTier)) {
203
+ throw new Error(`Invalid policy schema in ${path}: minTrustTier must be verified, conditional, unverified, or blocked`);
204
+ }
205
+ return {
206
+ version: value.version,
207
+ minTrustScore: value.minTrustScore,
208
+ minTrustTier: value.minTrustTier,
209
+ requireToolPinVerifiedEvidence: booleanValue(value.requireToolPinVerifiedEvidence, "requireToolPinVerifiedEvidence", path),
210
+ allowedSources: sourceArray(value.allowedSources, "allowedSources", path),
211
+ deniedSources: sourceArray(value.deniedSources, "deniedSources", path),
212
+ allowedClients: clientArray(value.allowedClients, "allowedClients", path),
213
+ deniedClients: clientArray(value.deniedClients, "deniedClients", path),
214
+ deniedServers: stringArray(value.deniedServers, "deniedServers", path),
215
+ deniedPackageTypes: stringArray(value.deniedPackageTypes, "deniedPackageTypes", path),
216
+ deniedTransports: stringArray(value.deniedTransports, "deniedTransports", path),
217
+ deniedRemoteHosts: stringArray(value.deniedRemoteHosts, "deniedRemoteHosts", path),
218
+ denyRemoteEndpoints: booleanValue(value.denyRemoteEndpoints, "denyRemoteEndpoints", path),
219
+ denyRequiredSecrets: booleanValue(value.denyRequiredSecrets, "denyRequiredSecrets", path),
220
+ requireDigestPinnedOci: booleanValue(value.requireDigestPinnedOci, "requireDigestPinnedOci", path),
221
+ requireMcpbSha256: booleanValue(value.requireMcpbSha256, "requireMcpbSha256", path),
222
+ };
223
+ }
224
+ function selectedPackageTypes(plan) {
225
+ const target = selectedTarget(plan);
226
+ return target.kind === "package" && typeof target.registryType === "string" ? [target.registryType] : [];
227
+ }
228
+ function selectedTransports(plan) {
229
+ const target = selectedTarget(plan);
230
+ if (target.kind === "remote" && typeof target.type === "string")
231
+ return [target.type];
232
+ if (target.kind === "package" && typeof target.transport === "string")
233
+ return [target.transport];
234
+ return [];
235
+ }
236
+ function selectedRemoteHosts(plan) {
237
+ const target = selectedTarget(plan);
238
+ if (target.kind !== "remote" || typeof target.url !== "string")
239
+ return [];
240
+ try {
241
+ return [new URL(target.url).host];
242
+ }
243
+ catch {
244
+ return [];
245
+ }
246
+ }
247
+ function isSelectedPackage(plan, registryType) {
248
+ const target = selectedTarget(plan);
249
+ return target.kind === "package" && target.registryType === registryType;
250
+ }
251
+ function hasToolPinVerifiedEvidence(plan) {
252
+ return plan.trust.verifiedProvenance === true && hasFreshTrustedArtifactEvidence(plan.trust.evidence ?? []);
253
+ }
254
+ function trustTierRank(tier) {
255
+ return {
256
+ blocked: 0,
257
+ unverified: 1,
258
+ conditional: 2,
259
+ verified: 3,
260
+ }[tier];
261
+ }
262
+ function isTrustTier(value) {
263
+ return value === "verified" || value === "conditional" || value === "unverified" || value === "blocked";
264
+ }
265
+ function selectedTarget(plan) {
266
+ return isRecord(plan.selectedTarget) ? plan.selectedTarget : {};
267
+ }
268
+ function sourceArray(value, field, path) {
269
+ const values = stringArray(value, field, path);
270
+ if (values === undefined)
271
+ return undefined;
272
+ const normalized = values.map(normalizePolicySource);
273
+ if (normalized.some((item) => item === undefined)) {
274
+ throw new Error(`Invalid policy schema in ${path}: ${field} contains an unknown registry source`);
275
+ }
276
+ return normalized;
277
+ }
278
+ function normalizePolicySource(value) {
279
+ if (value === "pulse")
280
+ return "pulsemcp";
281
+ if (["toolpin", "official", "docker", "pulsemcp", "smithery", "glama"].includes(value))
282
+ return value;
283
+ return undefined;
284
+ }
285
+ function clientArray(value, field, path) {
286
+ const values = stringArray(value, field, path);
287
+ if (values?.some((item) => !isClientName(item))) {
288
+ throw new Error(`Invalid policy schema in ${path}: ${field} contains an unknown client`);
289
+ }
290
+ return values;
291
+ }
292
+ function stringArray(value, field, path) {
293
+ if (value === undefined)
294
+ return undefined;
295
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
296
+ throw new Error(`Invalid policy schema in ${path}: ${field} must be an array of strings`);
297
+ }
298
+ return value;
299
+ }
300
+ function booleanValue(value, field, path) {
301
+ if (value === undefined)
302
+ return undefined;
303
+ if (typeof value !== "boolean") {
304
+ throw new Error(`Invalid policy schema in ${path}: ${field} must be a boolean`);
305
+ }
306
+ return value;
307
+ }
308
+ function isRecord(value) {
309
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
310
+ }