@polygraphso/litmus 0.8.0 → 0.9.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.
@@ -0,0 +1,692 @@
1
+ import {
2
+ parseAuthFlags,
3
+ resolveTarget
4
+ } from "./chunk-M5HXKZVN.js";
5
+ import {
6
+ SKILL_METHODOLOGY_VERSION,
7
+ runLitmus,
8
+ runSkillLitmus,
9
+ runSkillQuality,
10
+ runSkillQualityJudged
11
+ } from "./chunk-DN2OX4RT.js";
12
+ import {
13
+ CATEGORY_STATUS_UINT8,
14
+ METHODOLOGY_VERSION,
15
+ parseServerRef,
16
+ parseSkillRef,
17
+ serverKey,
18
+ skillKey
19
+ } from "./chunk-44R4ZYOE.js";
20
+
21
+ // ../onchain/src/networks.ts
22
+ var NETWORKS = {
23
+ "base-sepolia": {
24
+ chainId: 84532,
25
+ rpc: "https://sepolia.base.org",
26
+ eas: "0x4200000000000000000000000000000000000021",
27
+ schemaRegistry: "0x4200000000000000000000000000000000000020",
28
+ easscan: "https://base-sepolia.easscan.org"
29
+ },
30
+ base: {
31
+ chainId: 8453,
32
+ rpc: "https://mainnet.base.org",
33
+ eas: "0x4200000000000000000000000000000000000021",
34
+ schemaRegistry: "0x4200000000000000000000000000000000000020",
35
+ easscan: "https://base.easscan.org"
36
+ }
37
+ };
38
+ function selectedNetwork() {
39
+ return process.env.NEXT_PUBLIC_POLYGRAPH_NETWORK === "base" ? "base" : "base-sepolia";
40
+ }
41
+ function networkConfig(net = selectedNetwork()) {
42
+ return NETWORKS[net];
43
+ }
44
+ function rpcUrl(net = selectedNetwork()) {
45
+ const override = net === "base" ? process.env.BASE_MAINNET_RPC_URL : process.env.BASE_SEPOLIA_RPC_URL;
46
+ return override && override.length > 0 ? override : NETWORKS[net].rpc;
47
+ }
48
+
49
+ // ../onchain/src/eas.ts
50
+ import { AbiCoder } from "ethers";
51
+ var LITMUS_SCHEMA = "string serverRef,bytes32 toolDefsFingerprint,uint8 gradeC01,uint8 gradeC02,uint8 gradeC03,string overallGrade,string reportCID,string methodologyVersion,uint64 ranAt,string resolvedVersion";
52
+ var LITMUS_ABI_TYPES = [
53
+ "string",
54
+ // serverRef
55
+ "bytes32",
56
+ // toolDefsFingerprint
57
+ "uint8",
58
+ // gradeC01
59
+ "uint8",
60
+ // gradeC02
61
+ "uint8",
62
+ // gradeC03
63
+ "string",
64
+ // overallGrade
65
+ "string",
66
+ // reportCID
67
+ "string",
68
+ // methodologyVersion
69
+ "uint64",
70
+ // ranAt
71
+ "string"
72
+ // resolvedVersion
73
+ ];
74
+ var LITMUS_ABI_NAMES = [
75
+ "serverRef",
76
+ "toolDefsFingerprint",
77
+ "gradeC01",
78
+ "gradeC02",
79
+ "gradeC03",
80
+ "overallGrade",
81
+ "reportCID",
82
+ "methodologyVersion",
83
+ "ranAt",
84
+ "resolvedVersion"
85
+ ];
86
+ function categoryUint8(bundle, code) {
87
+ const status = bundle.categories.find((c) => c.code === code)?.status;
88
+ return status ? CATEGORY_STATUS_UINT8[status] : CATEGORY_STATUS_UINT8.skipped;
89
+ }
90
+ function litmusFields(bundle, reportCID) {
91
+ return {
92
+ serverRef: bundle.serverRef,
93
+ toolDefsFingerprint: bundle.toolDefsFingerprint,
94
+ gradeC01: categoryUint8(bundle, "C-01"),
95
+ gradeC02: categoryUint8(bundle, "C-02"),
96
+ gradeC03: categoryUint8(bundle, "C-03"),
97
+ overallGrade: bundle.grade,
98
+ reportCID,
99
+ methodologyVersion: bundle.methodologyVersion || METHODOLOGY_VERSION,
100
+ ranAt: BigInt(Math.floor(Date.parse(bundle.ranAt) / 1e3)),
101
+ resolvedVersion: bundle.resolvedVersion ?? ""
102
+ };
103
+ }
104
+ function encodeLitmusAttestation(bundle, reportCID) {
105
+ const f = litmusFields(bundle, reportCID);
106
+ return AbiCoder.defaultAbiCoder().encode(
107
+ [...LITMUS_ABI_TYPES],
108
+ [
109
+ f.serverRef,
110
+ f.toolDefsFingerprint,
111
+ f.gradeC01,
112
+ f.gradeC02,
113
+ f.gradeC03,
114
+ f.overallGrade,
115
+ f.reportCID,
116
+ f.methodologyVersion,
117
+ f.ranAt,
118
+ f.resolvedVersion
119
+ ]
120
+ );
121
+ }
122
+ function decodeLitmusAttestation(encoded) {
123
+ const values = AbiCoder.defaultAbiCoder().decode([...LITMUS_ABI_TYPES], encoded);
124
+ const out = {};
125
+ LITMUS_ABI_NAMES.forEach((name, i) => {
126
+ out[name] = values[i];
127
+ });
128
+ return out;
129
+ }
130
+
131
+ // ../onchain/src/eas-skill.ts
132
+ import { AbiCoder as AbiCoder2 } from "ethers";
133
+ var LITMUS_SKILL_SCHEMA = "string skillRef,bytes32 contentHash,uint8 gradeS01,uint8 gradeS03,uint8 gradeS04,string overallGrade,string reportCID,string methodologyVersion,uint64 ranAt,string resolvedRef";
134
+ var SKILL_ABI_TYPES = [
135
+ "string",
136
+ // skillRef
137
+ "bytes32",
138
+ // contentHash
139
+ "uint8",
140
+ // gradeS01
141
+ "uint8",
142
+ // gradeS03
143
+ "uint8",
144
+ // gradeS04
145
+ "string",
146
+ // overallGrade
147
+ "string",
148
+ // reportCID
149
+ "string",
150
+ // methodologyVersion
151
+ "uint64",
152
+ // ranAt
153
+ "string"
154
+ // resolvedRef
155
+ ];
156
+ var SKILL_ABI_NAMES = [
157
+ "skillRef",
158
+ "contentHash",
159
+ "gradeS01",
160
+ "gradeS03",
161
+ "gradeS04",
162
+ "overallGrade",
163
+ "reportCID",
164
+ "methodologyVersion",
165
+ "ranAt",
166
+ "resolvedRef"
167
+ ];
168
+ function categoryUint82(g, code) {
169
+ const status = g.categories.find((c) => c.code === code)?.status;
170
+ return status ? CATEGORY_STATUS_UINT8[status] : CATEGORY_STATUS_UINT8.skipped;
171
+ }
172
+ function skillAttestationFields(g, reportCID, resolvedRef) {
173
+ return {
174
+ skillRef: g.skillRef,
175
+ contentHash: g.contentHash,
176
+ gradeS01: categoryUint82(g, "S-01"),
177
+ gradeS03: categoryUint82(g, "S-03"),
178
+ gradeS04: categoryUint82(g, "S-04"),
179
+ overallGrade: g.grade,
180
+ reportCID,
181
+ methodologyVersion: g.methodologyVersion,
182
+ ranAt: BigInt(Math.floor(Date.parse(g.ranAt) / 1e3)),
183
+ resolvedRef: resolvedRef ?? ""
184
+ };
185
+ }
186
+ function encodeSkillAttestationFields(f) {
187
+ return AbiCoder2.defaultAbiCoder().encode(
188
+ [...SKILL_ABI_TYPES],
189
+ [
190
+ f.skillRef,
191
+ f.contentHash,
192
+ f.gradeS01,
193
+ f.gradeS03,
194
+ f.gradeS04,
195
+ f.overallGrade,
196
+ f.reportCID,
197
+ f.methodologyVersion,
198
+ f.ranAt,
199
+ f.resolvedRef
200
+ ]
201
+ );
202
+ }
203
+ function encodeSkillAttestation(g, reportCID, resolvedRef) {
204
+ return encodeSkillAttestationFields(skillAttestationFields(g, reportCID, resolvedRef));
205
+ }
206
+ function decodeSkillAttestation(encoded) {
207
+ const values = AbiCoder2.defaultAbiCoder().decode([...SKILL_ABI_TYPES], encoded);
208
+ const out = {};
209
+ SKILL_ABI_NAMES.forEach((name, i) => {
210
+ out[name] = values[i];
211
+ });
212
+ return out;
213
+ }
214
+
215
+ // ../onchain/src/read.ts
216
+ import { Contract, JsonRpcProvider, ZeroHash } from "ethers";
217
+ var EAS_ABI = [
218
+ "function getAttestation(bytes32 uid) view returns ((bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))"
219
+ ];
220
+ function litmusSchemaUID() {
221
+ const uid = process.env.NEXT_PUBLIC_EAS_SCHEMA_UID;
222
+ if (!uid) throw new Error("NEXT_PUBLIC_EAS_SCHEMA_UID is required \u2014 register the schema first.");
223
+ return uid;
224
+ }
225
+ async function readAttestation(uid) {
226
+ const cfg = networkConfig();
227
+ const provider = new JsonRpcProvider(rpcUrl(), cfg.chainId);
228
+ const eas = new Contract(cfg.eas, EAS_ABI, provider);
229
+ const att = await eas.getAttestation(uid);
230
+ if (!att || att.uid === ZeroHash) return null;
231
+ if (String(att.schema).toLowerCase() !== litmusSchemaUID().toLowerCase()) return null;
232
+ const d = decodeLitmusAttestation(att.data);
233
+ return {
234
+ uid: att.uid,
235
+ serverRef: String(d.serverRef),
236
+ toolDefsFingerprint: String(d.toolDefsFingerprint),
237
+ overallGrade: String(d.overallGrade),
238
+ reportCID: String(d.reportCID),
239
+ resolvedVersion: d.resolvedVersion || null,
240
+ revoked: att.revocationTime > 0n,
241
+ attester: String(att.attester),
242
+ expirationTime: BigInt(att.expirationTime ?? 0n)
243
+ };
244
+ }
245
+
246
+ // ../onchain/src/read-skill.ts
247
+ import { Contract as Contract2, JsonRpcProvider as JsonRpcProvider2, ZeroHash as ZeroHash2 } from "ethers";
248
+ var EAS_ABI2 = [
249
+ "function getAttestation(bytes32 uid) view returns ((bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))"
250
+ ];
251
+ function skillSchemaUID() {
252
+ const uid = process.env.NEXT_PUBLIC_EAS_SKILL_SCHEMA_UID;
253
+ if (!uid) throw new Error("NEXT_PUBLIC_EAS_SKILL_SCHEMA_UID is required \u2014 register the skill schema first.");
254
+ return uid;
255
+ }
256
+ async function readSkillAttestation(uid) {
257
+ const cfg = networkConfig();
258
+ const provider = new JsonRpcProvider2(rpcUrl(), cfg.chainId);
259
+ const eas = new Contract2(cfg.eas, EAS_ABI2, provider);
260
+ const att = await eas.getAttestation(uid);
261
+ if (!att || att.uid === ZeroHash2) return null;
262
+ if (String(att.schema).toLowerCase() !== skillSchemaUID().toLowerCase()) return null;
263
+ const d = decodeSkillAttestation(att.data);
264
+ return {
265
+ uid: att.uid,
266
+ skillRef: String(d.skillRef),
267
+ contentHash: String(d.contentHash),
268
+ overallGrade: String(d.overallGrade),
269
+ reportCID: String(d.reportCID),
270
+ resolvedRef: d.resolvedRef || null,
271
+ revoked: att.revocationTime > 0n,
272
+ attester: String(att.attester),
273
+ expirationTime: BigInt(att.expirationTime ?? 0n)
274
+ };
275
+ }
276
+
277
+ // src/tools/run-litmus.ts
278
+ import { z } from "zod";
279
+ var RUN_LITMUS_TOOL_NAME = "run_litmus";
280
+ var RUN_LITMUS_TOOL_TITLE = "Run a behavioral litmus on an MCP server";
281
+ var RUN_LITMUS_TOOL_DESCRIPTION = [
282
+ `Grade an MCP server A\u2013F against the open behavioral litmus (${METHODOLOGY_VERSION}).`,
283
+ "The harness connects the way an agent would, fingerprints the tool surface, and",
284
+ "runs four checks: C-01 tool-output injection, C-02 permission/egress overreach",
285
+ "(egress in a hardened default-deny Docker sandbox, plus a declared-permission",
286
+ "honesty check), C-03 sensitive-data handling (planted canaries), and C-04",
287
+ "adversarial-input handling (malformed/oversized and jailbreak inputs).",
288
+ "",
289
+ "This is ACTIVE: it launches the target server's code to exercise it (egress-",
290
+ "sandboxed when Docker is available) and takes ~20\u201360s. It is not a lookup \u2014 for",
291
+ "a server's already-published grade, use `verify_attestation`. No wallet or RPC",
292
+ "needed.",
293
+ "",
294
+ "server_ref examples: npm/@modelcontextprotocol/server-filesystem \xB7",
295
+ "https://example.com/mcp \xB7 ./build/index.js. For a token-gated https:// target,",
296
+ "pass `bearer`. If Docker is unavailable, C-02 is skipped and the grade is capped",
297
+ "at B for that run."
298
+ ].join("\n");
299
+ var runLitmusInputShape = {
300
+ server_ref: z.string().min(1).max(512).describe("What to grade: a registry ref (npm/@scope/server), an https:// MCP URL, or a local path to an MCP entry file."),
301
+ bearer: z.string().min(1).max(8192).optional().describe("Bearer token for a token-gated https:// MCP server. Sent as `Authorization: Bearer <token>` to the target origin only. Ignored for stdio/local targets."),
302
+ header: z.array(z.string()).max(20).optional().describe('Extra HTTP headers for a gated https:// target, each "Key: Value" (e.g. "X-Api-Key: \u2026"). Overrides the bearer-derived Authorization for the same key. Ignored for stdio/local targets.')
303
+ };
304
+ var PROGRESS_TOTAL = 5;
305
+ async function handleRunLitmus({ server_ref, bearer, header }, extra) {
306
+ try {
307
+ const argv = [
308
+ ...bearer ? ["--bearer", bearer] : [],
309
+ ...(header ?? []).flatMap((h) => ["--header", h])
310
+ ];
311
+ const { headers } = parseAuthFlags(argv, {});
312
+ const progressToken = extra._meta?.progressToken;
313
+ const sendProgress = progressToken !== void 0 ? (progress, message) => void extra.sendNotification({
314
+ method: "notifications/progress",
315
+ params: { progressToken, progress, total: PROGRESS_TOTAL, message }
316
+ }) : void 0;
317
+ sendProgress?.(0, `Connecting to ${server_ref}\u2026`);
318
+ const bundle = await runLitmus(resolveTarget(server_ref), {
319
+ ...Object.keys(headers).length ? { headers } : {},
320
+ ...sendProgress ? { onProgress: (done, _total, label) => sendProgress(done, label) } : {}
321
+ });
322
+ const payload = summarize(bundle);
323
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
324
+ } catch (err) {
325
+ const message = err instanceof Error ? err.message : String(err);
326
+ return { isError: true, content: [{ type: "text", text: `run_litmus failed: ${message}` }] };
327
+ }
328
+ }
329
+ var CATEGORY_LABEL = {
330
+ "C-01": "tool-output injection",
331
+ "C-02": "permission / egress overreach",
332
+ "C-03": "sensitive-data handling",
333
+ "C-04": "adversarial-input handling"
334
+ };
335
+ function summarize(b) {
336
+ const find = (code) => b.categories.find((c) => c.code === code);
337
+ const categories = ["C-01", "C-02", "C-03", "C-04"].map((code) => {
338
+ const c = find(code);
339
+ const findings = c?.status === "fail" ? c.probes.flatMap((p) => p.findings).filter((f) => f.severity === "high").slice(0, 5).map((f) => ({ tool: f.tool, kind: f.kind, match: truncate(f.match, 120), host: f.host, port: f.port })) : [];
340
+ return { code, check: CATEGORY_LABEL[code], status: c?.status ?? "unknown", reason: c?.reason ?? null, findings };
341
+ });
342
+ return {
343
+ grade: b.grade,
344
+ summary: b.gradeRationale,
345
+ serverRef: b.serverRef,
346
+ resolvedVersion: b.resolvedVersion,
347
+ fingerprint: b.toolDefsFingerprint,
348
+ ranAt: b.ranAt,
349
+ methodologyVersion: b.methodologyVersion,
350
+ categories
351
+ };
352
+ }
353
+ function truncate(s, n) {
354
+ return s.length > n ? `${s.slice(0, n)}\u2026` : s;
355
+ }
356
+
357
+ // src/tools/run-skill-litmus.ts
358
+ import { z as z2 } from "zod";
359
+ import { statSync } from "fs";
360
+ var RUN_SKILL_LITMUS_TOOL_NAME = "run_skill_litmus";
361
+ var RUN_SKILL_LITMUS_TOOL_TITLE = "Run a safety litmus on a Claude Code skill";
362
+ var RUN_SKILL_LITMUS_TOOL_DESCRIPTION = [
363
+ `Grade a Claude Code / Agent Skill A/B/D/F against the open static safety litmus (${SKILL_METHODOLOGY_VERSION}).`,
364
+ "A skill is a SKILL.md (instructions + frontmatter) plus an optional bundle. The",
365
+ "litmus scans the bytes for S-01 prompt-injection / context-poisoning in the body,",
366
+ "S-03 data-exfiltration instructions, and S-04 dangerous commands in bundled",
367
+ "executable scripts. It content-hashes the whole directory (the anti-tamper anchor).",
368
+ "",
369
+ "The SAFETY letter is a STATIC read: it does NOT execute the skill or its scripts",
370
+ "and is fast \u2014 therefore NOT behavioral proof. An A means the static checks found no",
371
+ "injection, exfil instruction, or dangerous bundled command, not that the skill is",
372
+ "safe to run unsupervised. A command a skill constructs or fetches at runtime is not",
373
+ "visible to static scanning (a disclosed limit).",
374
+ "",
375
+ "It also returns a SEPARATE, advisory `quality` signal (well-formed / issues /",
376
+ "malformed) \u2014 never an A\u2013F letter, never minted, never affecting the safety letter.",
377
+ "Its deterministic checks always run; its optional LLM-judged axes (honesty,",
378
+ "coherence) run only when a judge is available \u2014 the host agent's own model via MCP",
379
+ "sampling (no key), or a user-provided OpenAI-compatible key \u2014 and are skipped",
380
+ "otherwise.",
381
+ "",
382
+ "skill_ref (v1): a LOCAL path to a skill directory containing SKILL.md, e.g.",
383
+ "./skills/my-skill. Remote refs (github/<owner>/<repo>#path, marketplace/<owner>/<name>)",
384
+ "are not yet supported."
385
+ ].join("\n");
386
+ var runSkillLitmusInputShape = {
387
+ skill_ref: z2.string().min(1).max(1024).describe("Local path to a skill directory (must contain SKILL.md). Remote refs are not yet supported in this version.")
388
+ };
389
+ async function handleRunSkillLitmus({ skill_ref }, ctx = {}) {
390
+ try {
391
+ let st;
392
+ try {
393
+ st = statSync(skill_ref);
394
+ } catch {
395
+ return errorResult(`no such path: ${skill_ref} (v1 grades a local skill directory; remote refs are not yet supported)`);
396
+ }
397
+ if (!st.isDirectory()) {
398
+ return errorResult(`not a directory: ${skill_ref} (pass the skill folder that contains SKILL.md)`);
399
+ }
400
+ const safety = runSkillLitmus(skill_ref, { skillRef: skill_ref });
401
+ const quality = ctx.judge ? await runSkillQualityJudged(skill_ref, ctx.judge, { skillRef: skill_ref }) : runSkillQuality(skill_ref, { skillRef: skill_ref });
402
+ return { content: [{ type: "text", text: JSON.stringify({ safety: summarize2(safety), quality: summarizeQuality(quality) }, null, 2) }] };
403
+ } catch (err) {
404
+ return errorResult(err instanceof Error ? err.message : String(err));
405
+ }
406
+ }
407
+ function errorResult(message) {
408
+ return { isError: true, content: [{ type: "text", text: `run_skill_litmus failed: ${message}` }] };
409
+ }
410
+ var CATEGORY_LABEL2 = {
411
+ "S-01": "prompt injection / context poisoning",
412
+ "S-03": "data-exfiltration instructions",
413
+ "S-04": "dangerous bundled commands"
414
+ };
415
+ function summarize2(b) {
416
+ const categories = b.categories.map((c) => ({
417
+ code: c.code,
418
+ check: CATEGORY_LABEL2[c.code] ?? c.code,
419
+ status: c.status,
420
+ reason: c.reason ?? null,
421
+ findings: c.status === "fail" ? c.findings.filter((f) => f.severity === "high").slice(0, 5).map((f) => ({ kind: f.kind, match: truncate2(f.match, 120), file: f.file })) : []
422
+ }));
423
+ return {
424
+ grade: b.grade,
425
+ summary: b.gradeRationale,
426
+ skillRef: b.skillRef,
427
+ contentHash: b.contentHash,
428
+ ranAt: b.ranAt,
429
+ methodologyVersion: b.methodologyVersion,
430
+ categories,
431
+ advisories: b.advisories.slice(0, 10).map((f) => ({ kind: f.kind, severity: f.severity, match: truncate2(f.match, 120), file: f.file })),
432
+ disclaimer: b.disclaimer
433
+ };
434
+ }
435
+ function summarizeQuality(q) {
436
+ return {
437
+ qualityVersion: q.qualityVersion,
438
+ verdict: q.verdict,
439
+ // well-formed | issues | malformed — NOT an A–F letter
440
+ checks: q.checks.map((c) => ({ id: c.id, status: c.status, detail: c.detail })),
441
+ judged: q.judged ? { judge: q.judged.judge, samples: q.judged.samples, agreement: q.judged.agreement, axes: q.judged.axes } : null,
442
+ disclaimer: q.disclaimer
443
+ };
444
+ }
445
+ function truncate2(s, n) {
446
+ return s.length > n ? `${s.slice(0, n)}\u2026` : s;
447
+ }
448
+
449
+ // ../mcp/src/tools/verify-attestation.ts
450
+ import { z as z3 } from "zod";
451
+ function canonicalRef(ref) {
452
+ try {
453
+ return serverKey(parseServerRef(ref)).toLowerCase();
454
+ } catch {
455
+ return ref.trim().toLowerCase();
456
+ }
457
+ }
458
+ var VERIFY_TOOL_NAME = "verify_attestation";
459
+ var VERIFY_TOOL_TITLE = "Verify a server's polygraph attestation";
460
+ var VERIFY_TOOL_DESCRIPTION = [
461
+ "Read a server's already-published polygraph (litmus) grade \u2014 without running",
462
+ "anything \u2014 before an agent trusts or, in agentic commerce, pays it.",
463
+ "",
464
+ "When a grade is published it returns the behavioral grade (A\u2013F), the attestation",
465
+ "UID, the evidence CID, and the graded tool-surface fingerprint. The caller must",
466
+ "still recompute the LIVE fingerprint and require it to equal the attested one",
467
+ "before paying \u2014 a passing attestation can otherwise front for a tool surface the",
468
+ "server no longer serves (rug pull).",
469
+ "",
470
+ "Grade publishing is still rolling out, so this commonly returns not_available",
471
+ "today: that means UNEVALUATED (neither safe nor unsafe), not a failing grade \u2014 to",
472
+ "grade the server yourself right now, use `run_litmus`. A `lookup_failed` result",
473
+ "means the lookup itself failed (the index or chain was unreachable); the grade is",
474
+ "unknown, which is not the same as unevaluated.",
475
+ "",
476
+ "Input: server_ref \u2014 e.g. npm/@modelcontextprotocol/server-filesystem."
477
+ ].join("\n");
478
+ var verifyInputShape = {
479
+ server_ref: z3.string().min(1).max(512).describe("Registry-prefixed server identifier, e.g. npm/@scope/server.")
480
+ };
481
+ async function handleVerify({ server_ref }) {
482
+ const found = await resolveUid(server_ref);
483
+ if (found.kind === "error") {
484
+ return {
485
+ isError: true,
486
+ content: [
487
+ {
488
+ type: "text",
489
+ text: `lookup_failed \u2014 could not reach the polygraph grade index for ${server_ref} (${found.detail}). The lookup itself failed, so the grade is unknown \u2014 retry or report it as unchecked, NOT as unevaluated.`
490
+ }
491
+ ]
492
+ };
493
+ }
494
+ let att = null;
495
+ if (found.kind === "found") {
496
+ try {
497
+ att = await readAttestation(found.uid);
498
+ } catch (err) {
499
+ return {
500
+ isError: true,
501
+ content: [
502
+ {
503
+ type: "text",
504
+ text: `lookup_failed \u2014 the onchain read failed for ${server_ref} (${err instanceof Error ? err.message : String(err)}). Treat as unchecked (the chain/RPC was unreachable), not as "no grade".`
505
+ }
506
+ ]
507
+ };
508
+ }
509
+ }
510
+ if (!att) {
511
+ return {
512
+ content: [
513
+ {
514
+ type: "text",
515
+ text: `not_available \u2014 no published polygraph grade for ${server_ref}. Grade publishing is still rolling out, so this is expected for most servers; it means unevaluated (neither safe nor unsafe), not a failing grade. To grade it now, use run_litmus.`
516
+ }
517
+ ]
518
+ };
519
+ }
520
+ if (canonicalRef(att.serverRef) !== canonicalRef(server_ref)) {
521
+ return {
522
+ content: [
523
+ {
524
+ type: "text",
525
+ text: `not_available \u2014 the resolved attestation is for ${att.serverRef}, not ${server_ref} (discovery mismatch; treat as unevaluated)`
526
+ }
527
+ ]
528
+ };
529
+ }
530
+ const payload = {
531
+ status: "attested",
532
+ grade: att.overallGrade,
533
+ attestationUid: att.uid,
534
+ serverRef: att.serverRef,
535
+ // The version the grade was run against (null for HTTP/unresolved targets).
536
+ // Advisory: the live fingerprint, not this string, is the trust anchor.
537
+ resolvedVersion: att.resolvedVersion,
538
+ reportCID: att.reportCID,
539
+ toolDefsFingerprint: att.toolDefsFingerprint,
540
+ revoked: att.revoked,
541
+ network: selectedNetwork(),
542
+ liveFingerprintCheckRequired: "Recompute the live tool-surface fingerprint and require it to equal toolDefsFingerprint before paying."
543
+ };
544
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
545
+ }
546
+ async function resolveUid(serverRef) {
547
+ const base = process.env.POLYGRAPH_API_URL ?? "https://polygraph.so";
548
+ try {
549
+ const res = await fetch(`${base}/api/attestations?ref=${encodeURIComponent(serverRef)}`);
550
+ if (res.status === 404) return { kind: "none" };
551
+ if (!res.ok) return { kind: "error", detail: `grade index returned HTTP ${res.status}` };
552
+ const row = await res.json();
553
+ return row?.attestation_uid ? { kind: "found", uid: row.attestation_uid } : { kind: "none" };
554
+ } catch (err) {
555
+ return { kind: "error", detail: err instanceof Error ? err.message : String(err) };
556
+ }
557
+ }
558
+
559
+ // ../mcp/src/tools/verify-skill-attestation.ts
560
+ import { z as z4 } from "zod";
561
+ function canonicalRef2(ref) {
562
+ try {
563
+ return skillKey(parseSkillRef(ref)).toLowerCase();
564
+ } catch {
565
+ return ref.trim().toLowerCase();
566
+ }
567
+ }
568
+ var VERIFY_SKILL_TOOL_NAME = "verify_skill_attestation";
569
+ var VERIFY_SKILL_TOOL_TITLE = "Verify a skill's polygraph attestation";
570
+ var VERIFY_SKILL_TOOL_DESCRIPTION = [
571
+ "Read a Claude Code / Agent Skill's already-published polygraph grade \u2014 without",
572
+ "running anything \u2014 before an agent installs or trusts it.",
573
+ "",
574
+ "When a grade is published it returns the letter (A/B/D/F), the attestation UID,",
575
+ "the evidence CID, and the attested contentHash. The caller MUST then recompute the",
576
+ "skill's content hash (sha256 over every file the SKILL.md can load, including",
577
+ "lazily-referenced files) and require it to equal contentHash before installing \u2014 a",
578
+ "passing attestation can otherwise front for different bytes (a swapped bundled",
579
+ "script). The ref/version is advisory; the contentHash is the trust anchor.",
580
+ "",
581
+ "Grade publishing for skills is rolling out, so this commonly returns not_available:",
582
+ "that means UNEVALUATED (neither safe nor unsafe), not a failing grade \u2014 to grade a",
583
+ "local skill yourself, use `run_skill_litmus`. A `lookup_failed` result means the",
584
+ "lookup itself failed (index/chain unreachable); the grade is unknown, not unevaluated.",
585
+ "",
586
+ "Input: skill_ref \u2014 e.g. github/<owner>/<repo>#<path> or marketplace/<owner>/<name>."
587
+ ].join("\n");
588
+ var verifySkillInputShape = {
589
+ skill_ref: z4.string().min(1).max(1024).describe("Skill identifier, e.g. github/<owner>/<repo>#<path> or marketplace/<owner>/<name>.")
590
+ };
591
+ async function handleVerifySkill({ skill_ref }) {
592
+ const found = await resolveUid2(skill_ref);
593
+ if (found.kind === "error") {
594
+ return errorResult2(
595
+ `lookup_failed \u2014 could not reach the polygraph skill-grade index for ${skill_ref} (${found.detail}). The lookup itself failed, so the grade is unknown \u2014 retry or report it as unchecked, NOT as unevaluated.`
596
+ );
597
+ }
598
+ let att = null;
599
+ if (found.kind === "found") {
600
+ try {
601
+ att = await readSkillAttestation(found.uid);
602
+ } catch (err) {
603
+ return errorResult2(
604
+ `lookup_failed \u2014 the onchain read failed for ${skill_ref} (${err instanceof Error ? err.message : String(err)}). Treat as unchecked (the chain/RPC was unreachable), not as "no grade".`
605
+ );
606
+ }
607
+ }
608
+ if (!att) {
609
+ return text(
610
+ `not_available \u2014 no published polygraph grade for ${skill_ref}. Skill grade publishing is still rolling out, so this is expected; it means unevaluated (neither safe nor unsafe), not a failing grade. To grade a local skill now, use run_skill_litmus.`
611
+ );
612
+ }
613
+ if (canonicalRef2(att.skillRef) !== canonicalRef2(skill_ref)) {
614
+ return text(
615
+ `not_available \u2014 the resolved attestation is for ${att.skillRef}, not ${skill_ref} (discovery mismatch; treat as unevaluated).`
616
+ );
617
+ }
618
+ const payload = {
619
+ status: "attested",
620
+ grade: att.overallGrade,
621
+ attestationUid: att.uid,
622
+ skillRef: att.skillRef,
623
+ contentHash: att.contentHash,
624
+ resolvedRef: att.resolvedRef,
625
+ reportCID: att.reportCID,
626
+ revoked: att.revoked,
627
+ network: selectedNetwork(),
628
+ contentHashCheckRequired: "Recompute sha256 over every file the SKILL.md can load (including lazily-referenced files) and require it to equal contentHash before installing. The ref/version is advisory."
629
+ };
630
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
631
+ }
632
+ function text(t) {
633
+ return { content: [{ type: "text", text: t }] };
634
+ }
635
+ function errorResult2(t) {
636
+ return { isError: true, content: [{ type: "text", text: t }] };
637
+ }
638
+ async function resolveUid2(skillRef) {
639
+ const base = process.env.POLYGRAPH_API_URL ?? "https://polygraph.so";
640
+ try {
641
+ const res = await fetch(`${base}/api/skill-attestations?ref=${encodeURIComponent(skillRef)}`);
642
+ if (res.status === 404) return { kind: "none" };
643
+ if (!res.ok) return { kind: "error", detail: `grade index returned HTTP ${res.status}` };
644
+ const row = await res.json();
645
+ return row?.attestation_uid ? { kind: "found", uid: row.attestation_uid } : { kind: "none" };
646
+ } catch (err) {
647
+ return { kind: "error", detail: err instanceof Error ? err.message : String(err) };
648
+ }
649
+ }
650
+
651
+ // ../mcp/src/index.ts
652
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
653
+
654
+ export {
655
+ NETWORKS,
656
+ selectedNetwork,
657
+ networkConfig,
658
+ rpcUrl,
659
+ LITMUS_SCHEMA,
660
+ litmusFields,
661
+ encodeLitmusAttestation,
662
+ decodeLitmusAttestation,
663
+ LITMUS_SKILL_SCHEMA,
664
+ skillAttestationFields,
665
+ encodeSkillAttestationFields,
666
+ encodeSkillAttestation,
667
+ decodeSkillAttestation,
668
+ litmusSchemaUID,
669
+ readAttestation,
670
+ skillSchemaUID,
671
+ readSkillAttestation,
672
+ RUN_LITMUS_TOOL_NAME,
673
+ RUN_LITMUS_TOOL_TITLE,
674
+ RUN_LITMUS_TOOL_DESCRIPTION,
675
+ runLitmusInputShape,
676
+ handleRunLitmus,
677
+ RUN_SKILL_LITMUS_TOOL_NAME,
678
+ RUN_SKILL_LITMUS_TOOL_TITLE,
679
+ RUN_SKILL_LITMUS_TOOL_DESCRIPTION,
680
+ runSkillLitmusInputShape,
681
+ handleRunSkillLitmus,
682
+ VERIFY_TOOL_NAME,
683
+ VERIFY_TOOL_TITLE,
684
+ VERIFY_TOOL_DESCRIPTION,
685
+ verifyInputShape,
686
+ handleVerify,
687
+ VERIFY_SKILL_TOOL_NAME,
688
+ VERIFY_SKILL_TOOL_TITLE,
689
+ VERIFY_SKILL_TOOL_DESCRIPTION,
690
+ verifySkillInputShape,
691
+ handleVerifySkill
692
+ };