protect-mcp 0.7.6 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -469,6 +469,7 @@ function signDecision(entry) {
469
469
  if (entry.timing) payload.timing = entry.timing;
470
470
  if (entry.swarm) payload.swarm = entry.swarm;
471
471
  if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
472
+ if (entry.enrichment) payload.enrichment = entry.enrichment;
472
473
  if (entry.action_readback) payload.action_readback = entry.action_readback;
473
474
  if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
474
475
  const result = artifactsModule.createSignedArtifact(
@@ -5081,6 +5082,294 @@ var init_sha256 = __esm({
5081
5082
  }
5082
5083
  });
5083
5084
 
5085
+ // src/receipt-enrichment.ts
5086
+ function canonicalJson(value) {
5087
+ const seen = /* @__PURE__ */ new WeakSet();
5088
+ const enc = (v) => {
5089
+ if (v === null || v === void 0) return "null";
5090
+ const t = typeof v;
5091
+ if (t === "number") return Number.isFinite(v) ? JSON.stringify(v) : "null";
5092
+ if (t === "boolean" || t === "string") return JSON.stringify(v);
5093
+ if (t === "bigint") return JSON.stringify(v.toString());
5094
+ if (t === "function" || t === "symbol") return "null";
5095
+ if (Array.isArray(v)) return "[" + v.map(enc).join(",") + "]";
5096
+ if (t === "object") {
5097
+ const o = v;
5098
+ if (seen.has(o)) return '"[circular]"';
5099
+ seen.add(o);
5100
+ const body = Object.keys(o).sort().map((k) => JSON.stringify(k) + ":" + enc(o[k])).join(",");
5101
+ seen.delete(o);
5102
+ return "{" + body + "}";
5103
+ }
5104
+ return "null";
5105
+ };
5106
+ return enc(value);
5107
+ }
5108
+ function sha256Hex2(s) {
5109
+ return bytesToHex(sha2562(new TextEncoder().encode(s)));
5110
+ }
5111
+ function deriveCapabilities(tool, input) {
5112
+ const t = String(tool || "").toLowerCase();
5113
+ let text = "";
5114
+ try {
5115
+ text = canonicalJson(input).toLowerCase();
5116
+ } catch {
5117
+ }
5118
+ const caps = /* @__PURE__ */ new Set();
5119
+ for (const r of RULES) {
5120
+ if (r.tool && r.tool.test(t)) caps.add(r.cap);
5121
+ if (r.text && r.text.test(text)) caps.add(r.cap);
5122
+ }
5123
+ return Array.from(caps).sort();
5124
+ }
5125
+ function deriveResource(input) {
5126
+ const o = input && typeof input === "object" ? input : {};
5127
+ const path = o.file_path ?? o.path ?? o.filePath ?? o.notebook_path ?? o.filename;
5128
+ if (typeof path === "string" && path.trim()) return { kind: "path", digest: sha256Hex2(path.replace(/\\/g, "/")) };
5129
+ const url = o.url ?? o.uri ?? o.endpoint ?? o.href;
5130
+ if (typeof url === "string" && url.trim()) {
5131
+ try {
5132
+ return { kind: "host", digest: sha256Hex2(new URL(url).host.toLowerCase()) };
5133
+ } catch {
5134
+ }
5135
+ }
5136
+ const cmd = o.command ?? o.cmd ?? o.script;
5137
+ if (typeof cmd === "string" && cmd.trim()) {
5138
+ const first = cmd.trim().split(/\s+/)[0];
5139
+ if (first) return { kind: "command", digest: sha256Hex2(first) };
5140
+ }
5141
+ return void 0;
5142
+ }
5143
+ function buildEnrichment(tool, input) {
5144
+ const e = {
5145
+ v: ENRICHMENT_VERSION,
5146
+ input_digest: sha256Hex2(canonicalJson(input ?? {})),
5147
+ capabilities: deriveCapabilities(tool, input)
5148
+ };
5149
+ const resource = deriveResource(input);
5150
+ if (resource) e.resource = resource;
5151
+ return e;
5152
+ }
5153
+ var ENRICHMENT_VERSION, RULES;
5154
+ var init_receipt_enrichment = __esm({
5155
+ "src/receipt-enrichment.ts"() {
5156
+ "use strict";
5157
+ init_sha256();
5158
+ init_utils();
5159
+ ENRICHMENT_VERSION = 1;
5160
+ RULES = [
5161
+ { cap: "exec.shell", tool: /bash|shell|exec|terminal|run_command|command/ },
5162
+ { cap: "fs.read", tool: /(^|[_.])(read|cat|glob|grep|search|ls|view|list_files|open)/ },
5163
+ { cap: "fs.write", tool: /write|create_file|save|append|edit|patch|replace|update_file|multiedit|notebook/ },
5164
+ { cap: "fs.delete", tool: /delete|remove|unlink|trash|(^|[_.])rm/ },
5165
+ { cap: "net.egress", tool: /fetch|http|curl|wget|request|download|browse|navigate|webfetch|web_search|scrape/ },
5166
+ { cap: "vcs", tool: /(^|[_.])git/, text: /\bgit\s+(commit|push|pull|clone|reset|checkout|branch|rebase|merge|tag)\b/ },
5167
+ { cap: "package.install", text: /\b(npm|pnpm|yarn)\s+(i|install|add)\b|\bpip3?\s+install\b|\bgo\s+get\b|\bcargo\s+add\b|\bbrew\s+install\b|\bapt(-get)?\s+install\b|\bgem\s+install\b/ },
5168
+ { cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
5169
+ { cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
5170
+ { cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
5171
+ { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
5172
+ ];
5173
+ }
5174
+ });
5175
+
5176
+ // src/claim.ts
5177
+ var claim_exports = {};
5178
+ __export(claim_exports, {
5179
+ ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
5180
+ CLAIM_TYPE: () => CLAIM_TYPE,
5181
+ DEFAULT_LOG: () => DEFAULT_LOG,
5182
+ anchorClaim: () => anchorClaim,
5183
+ buildAnchorEnvelope: () => buildAnchorEnvelope,
5184
+ buildClaim: () => buildClaim,
5185
+ claimDigest: () => claimDigest,
5186
+ evaluate: () => evaluate,
5187
+ leafHash: () => leafHash,
5188
+ merkleRoot: () => merkleRoot,
5189
+ receiptToLeaf: () => receiptToLeaf,
5190
+ verifyClaim: () => verifyClaim
5191
+ });
5192
+ function sha256Hex3(input) {
5193
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
5194
+ return bytesToHex(sha2562(bytes));
5195
+ }
5196
+ function receiptToLeaf(e) {
5197
+ const p = e && typeof e.payload === "object" && e.payload || e;
5198
+ const dec = String(p.decision || e.decision || "").toLowerCase();
5199
+ const v = /den|block|reject|refus/.test(dec) ? "blocked" : /ask|approv|hold|escal|review|pending/.test(dec) ? "held" : "allowed";
5200
+ const enr = p.enrichment;
5201
+ const c = enr && Array.isArray(enr.capabilities) ? enr.capabilities.map(String).sort() : [];
5202
+ const tsRaw = e.issued_at || p.timestamp || p.issued_at;
5203
+ const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
5204
+ const t = isFinite(ms) ? new Date(ms).toISOString() : "";
5205
+ const d = sha256Hex3(canonicalJson(e));
5206
+ return { d, v, c, t };
5207
+ }
5208
+ function leafHash(leaf) {
5209
+ return sha256Hex3(canonicalJson(leaf));
5210
+ }
5211
+ function merkleRoot(leafHashes) {
5212
+ if (leafHashes.length === 0) return sha256Hex3("scopeblind.claim.empty");
5213
+ let level = [...leafHashes].sort();
5214
+ while (level.length > 1) {
5215
+ const next = [];
5216
+ for (let i = 0; i < level.length; i += 2) {
5217
+ const a = level[i];
5218
+ const b = i + 1 < level.length ? level[i + 1] : level[i];
5219
+ next.push(sha256Hex3(a + b));
5220
+ }
5221
+ level = next;
5222
+ }
5223
+ return level[0];
5224
+ }
5225
+ function evaluate(pred, leaves) {
5226
+ if (pred.kind === "no_capability") {
5227
+ const matched2 = leaves.filter((l) => l.c.indexOf(pred.capability) >= 0).length;
5228
+ return { statement: `No action used capability "${pred.capability}"`, holds: matched2 === 0, matched: matched2 };
5229
+ }
5230
+ if (pred.kind === "only_capabilities") {
5231
+ const allow = new Set(pred.capabilities);
5232
+ const matched2 = leaves.filter((l) => !l.c.every((c) => allow.has(c))).length;
5233
+ return { statement: `All actions were confined to capabilities {${pred.capabilities.join(", ")}}`, holds: matched2 === 0, matched: matched2 };
5234
+ }
5235
+ if (pred.kind === "no_verdict") {
5236
+ const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
5237
+ return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
5238
+ }
5239
+ const matched = leaves.filter((l) => l.v === pred.verdict).length;
5240
+ return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
5241
+ }
5242
+ function messageHash(unsigned) {
5243
+ return sha2562(new TextEncoder().encode(canonicalJson(unsigned)));
5244
+ }
5245
+ function buildClaim(receipts, predicate, key, issuedAt) {
5246
+ const leaves = receipts.map(receiptToLeaf);
5247
+ const root = merkleRoot(leaves.map(leafHash));
5248
+ const claim = evaluate(predicate, leaves);
5249
+ const times = leaves.map((l) => l.t).filter(Boolean).sort();
5250
+ const unsigned = {
5251
+ type: CLAIM_TYPE,
5252
+ predicate,
5253
+ claim,
5254
+ scope: { total: leaves.length, from: times[0] || "", to: times[times.length - 1] || "" },
5255
+ record: { root },
5256
+ leaves,
5257
+ issuer: { kid: key.kid, publicKey: key.publicKey, issuer: key.issuer || "protect-mcp" },
5258
+ issued_at: issuedAt
5259
+ };
5260
+ const signature = bytesToHex(ed25519.sign(messageHash(unsigned), hexToBytes(key.privateKey)));
5261
+ return { ...unsigned, signature };
5262
+ }
5263
+ function verifyClaim(pack, overridePublicKey) {
5264
+ const reasons = [];
5265
+ const leaves = Array.isArray(pack.leaves) ? pack.leaves : [];
5266
+ const recomputedRoot = merkleRoot(leaves.map(leafHash));
5267
+ const root_ok = !!pack.record && recomputedRoot === pack.record.root;
5268
+ if (!root_ok) reasons.push("record commitment (Merkle root) does not match the disclosed decisions");
5269
+ const recomputed = evaluate(pack.predicate, leaves);
5270
+ const predicate_ok = !!pack.claim && recomputed.holds === pack.claim.holds && recomputed.matched === pack.claim.matched;
5271
+ if (!predicate_ok) reasons.push("claim result does not match the predicate recomputed over the disclosed decisions");
5272
+ let authentic = false;
5273
+ try {
5274
+ const { signature, ...unsigned } = pack;
5275
+ const pub = overridePublicKey || pack.issuer && pack.issuer.publicKey;
5276
+ if (pub && signature) {
5277
+ authentic = ed25519.verify(hexToBytes(signature), messageHash(unsigned), hexToBytes(pub));
5278
+ }
5279
+ } catch {
5280
+ }
5281
+ if (!authentic) reasons.push("signature does not verify against the issuer public key");
5282
+ return {
5283
+ valid: authentic && root_ok && predicate_ok,
5284
+ authentic,
5285
+ root_ok,
5286
+ predicate_ok,
5287
+ holds: !!(pack.claim && pack.claim.holds),
5288
+ matched: pack.claim ? pack.claim.matched : recomputed.matched,
5289
+ total: leaves.length,
5290
+ statement: pack.claim ? pack.claim.statement : recomputed.statement,
5291
+ reasons
5292
+ };
5293
+ }
5294
+ function anchorDeepSort(o) {
5295
+ if (o === null || typeof o !== "object") return o;
5296
+ if (Array.isArray(o)) return o.map(anchorDeepSort);
5297
+ const src = o;
5298
+ const out = {};
5299
+ for (const k of Object.keys(src).sort()) out[k] = anchorDeepSort(src[k]);
5300
+ return out;
5301
+ }
5302
+ function toBase64(bytes) {
5303
+ let bin = "";
5304
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
5305
+ return btoa(bin);
5306
+ }
5307
+ function claimDigest(pack) {
5308
+ return sha256Hex3(canonicalJson(pack));
5309
+ }
5310
+ function buildAnchorEnvelope(pack, key, issuedAt) {
5311
+ const signed = {
5312
+ type: "evidence_pack",
5313
+ schema: ANCHOR_SCHEMA,
5314
+ anchors: "protect-mcp-claim",
5315
+ claim_digest: claimDigest(pack),
5316
+ record_root: pack.record.root,
5317
+ statement: pack.claim.statement,
5318
+ holds: pack.claim.holds,
5319
+ matched: pack.claim.matched,
5320
+ total: pack.scope.total,
5321
+ issued_at: issuedAt,
5322
+ verification_key: key.publicKey,
5323
+ disclosure: "internal"
5324
+ };
5325
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5326
+ const digest = bytesToHex(hash);
5327
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5328
+ return { ...signed, signature, digest };
5329
+ }
5330
+ async function anchorClaim(pack, key, opts) {
5331
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
5332
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5333
+ const doFetch = opts.fetchImpl || globalThis.fetch;
5334
+ if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
5335
+ const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
5336
+ try {
5337
+ const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
5338
+ method: "POST",
5339
+ headers: { "content-type": "application/json" },
5340
+ body: JSON.stringify({ encoded })
5341
+ });
5342
+ const data = await resp.json().catch(() => null);
5343
+ if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
5344
+ return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
5345
+ }
5346
+ return {
5347
+ ok: true,
5348
+ claim_digest: envelope.claim_digest,
5349
+ seq: data.seq,
5350
+ entry_url: `${base}/fn/log/${data.seq}`,
5351
+ anchored_at: data.anchored_at,
5352
+ already_anchored: !!data.already_anchored,
5353
+ envelope
5354
+ };
5355
+ } catch {
5356
+ return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
5357
+ }
5358
+ }
5359
+ var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
5360
+ var init_claim = __esm({
5361
+ "src/claim.ts"() {
5362
+ "use strict";
5363
+ init_sha256();
5364
+ init_utils();
5365
+ init_ed25519();
5366
+ init_receipt_enrichment();
5367
+ CLAIM_TYPE = "scopeblind.claim.v1";
5368
+ ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
5369
+ DEFAULT_LOG = "https://scopeblind.com";
5370
+ }
5371
+ });
5372
+
5084
5373
  // src/commitments/merkle.ts
5085
5374
  function hashLeaf(leafBytes) {
5086
5375
  const buf = new Uint8Array(leafBytes.length + 1);
@@ -5095,7 +5384,7 @@ function hashInternal(left, right) {
5095
5384
  buf.set(right, 1 + left.length);
5096
5385
  return sha2562(buf);
5097
5386
  }
5098
- function merkleRoot(leafHashes) {
5387
+ function merkleRoot2(leafHashes) {
5099
5388
  if (leafHashes.length === 0) {
5100
5389
  throw new Error("merkleRoot: cannot compute root of empty leaf set");
5101
5390
  }
@@ -5104,8 +5393,8 @@ function merkleRoot(leafHashes) {
5104
5393
  }
5105
5394
  const n = leafHashes.length;
5106
5395
  const k = largestPowerOfTwoLessThan(n);
5107
- const left = merkleRoot(leafHashes.slice(0, k));
5108
- const right = merkleRoot(leafHashes.slice(k));
5396
+ const left = merkleRoot2(leafHashes.slice(0, k));
5397
+ const right = merkleRoot2(leafHashes.slice(k));
5109
5398
  return hashInternal(left, right);
5110
5399
  }
5111
5400
  function generateProof(leafHashes, index) {
@@ -5131,21 +5420,21 @@ function collectPath(leaves, index, out) {
5131
5420
  const k = largestPowerOfTwoLessThan(n);
5132
5421
  if (index < k) {
5133
5422
  collectPath(leaves.slice(0, k), index, out);
5134
- out.push(merkleRoot(leaves.slice(k)));
5423
+ out.push(merkleRoot2(leaves.slice(k)));
5135
5424
  } else {
5136
5425
  collectPath(leaves.slice(k), index - k, out);
5137
- out.push(merkleRoot(leaves.slice(0, k)));
5426
+ out.push(merkleRoot2(leaves.slice(0, k)));
5138
5427
  }
5139
5428
  }
5140
- function verifyProof(expectedRootHex, leafHash, proof) {
5429
+ function verifyProof(expectedRootHex, leafHash2, proof) {
5141
5430
  if (proof.index < 0 || proof.index >= proof.treeSize) return false;
5142
5431
  if (proof.treeSize === 1) {
5143
- return proof.siblings.length === 0 && bytesToHex(leafHash).toLowerCase() === expectedRootHex.toLowerCase();
5432
+ return proof.siblings.length === 0 && bytesToHex(leafHash2).toLowerCase() === expectedRootHex.toLowerCase();
5144
5433
  }
5145
5434
  let result;
5146
5435
  try {
5147
5436
  result = reconstructRoot(
5148
- leafHash,
5437
+ leafHash2,
5149
5438
  proof.index,
5150
5439
  proof.treeSize,
5151
5440
  proof.siblings
@@ -5155,12 +5444,12 @@ function verifyProof(expectedRootHex, leafHash, proof) {
5155
5444
  }
5156
5445
  return bytesToHex(result).toLowerCase() === expectedRootHex.toLowerCase();
5157
5446
  }
5158
- function reconstructRoot(leafHash, index, treeSize, siblings) {
5447
+ function reconstructRoot(leafHash2, index, treeSize, siblings) {
5159
5448
  if (treeSize === 1) {
5160
5449
  if (siblings.length !== 0) {
5161
5450
  throw new Error("reconstructRoot: extra siblings at single-leaf level");
5162
5451
  }
5163
- return leafHash;
5452
+ return leafHash2;
5164
5453
  }
5165
5454
  if (siblings.length === 0) {
5166
5455
  throw new Error("reconstructRoot: ran out of siblings before single-leaf");
@@ -5169,11 +5458,11 @@ function reconstructRoot(leafHash, index, treeSize, siblings) {
5169
5458
  const outermostSibling = hexToBytes(siblings[siblings.length - 1]);
5170
5459
  const innerSiblings = siblings.slice(0, -1);
5171
5460
  if (index < k) {
5172
- const leftHash = reconstructRoot(leafHash, index, k, innerSiblings);
5461
+ const leftHash = reconstructRoot(leafHash2, index, k, innerSiblings);
5173
5462
  return hashInternal(leftHash, outermostSibling);
5174
5463
  } else {
5175
5464
  const rightHash = reconstructRoot(
5176
- leafHash,
5465
+ leafHash2,
5177
5466
  index - k,
5178
5467
  treeSize - k,
5179
5468
  innerSiblings
@@ -5319,7 +5608,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
5319
5608
  if (committedFields.length > 0) {
5320
5609
  const { sorted, leafBytes } = leavesFromFields(committedFields);
5321
5610
  const leafHashes = leafBytes.map(hashLeaf);
5322
- const root = merkleRoot(leafHashes);
5611
+ const root = merkleRoot2(leafHashes);
5323
5612
  committedFieldsRoot = bytesToHex(root);
5324
5613
  sorted.forEach((f, i) => {
5325
5614
  openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
@@ -5337,8 +5626,8 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
5337
5626
  payload.committed_field_names = committedFields.map((f) => f.name);
5338
5627
  }
5339
5628
  const canonical = jcs(payload);
5340
- const messageHash = sha2562(new TextEncoder().encode(canonical));
5341
- const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
5629
+ const messageHash2 = sha2562(new TextEncoder().encode(canonical));
5630
+ const signatureBytes = ed25519.sign(messageHash2, hexToBytes(signingKey));
5342
5631
  const signedReceipt = {
5343
5632
  ...payload,
5344
5633
  signature: {
@@ -5490,9 +5779,9 @@ function verifyCommittedReceiptSignature(receipt) {
5490
5779
  return null;
5491
5780
  }
5492
5781
  const { signature: _signature, ...payloadWithoutSig } = receipt;
5493
- const messageHash = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
5782
+ const messageHash2 = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
5494
5783
  try {
5495
- return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
5784
+ return ed25519.verify(base64urlDecode(sig.sig), messageHash2, hexToBytes(sig.public_key));
5496
5785
  } catch {
5497
5786
  return false;
5498
5787
  }
@@ -6065,6 +6354,9 @@ async function handlePreToolUse(input, state) {
6065
6354
  });
6066
6355
  const payloadDigest = computePayloadDigest(input.toolInput);
6067
6356
  const actionReadback = buildActionReadback(toolName, input.toolInput || {});
6357
+ const enrichment = buildEnrichment(toolName, input.toolInput || {});
6358
+ const inflightRec = state.inflightTools.get(requestId);
6359
+ if (inflightRec) inflightRec.enrichment = enrichment;
6068
6360
  const swarm = {
6069
6361
  ...state.swarmContext,
6070
6362
  ...input.agentId && { agent_id: input.agentId },
@@ -6484,6 +6776,8 @@ function emitDecisionLog(state, entry) {
6484
6776
  ...entry.sandbox_state && { sandbox_state: entry.sandbox_state },
6485
6777
  ...entry.plan_receipt_id && { plan_receipt_id: entry.plan_receipt_id }
6486
6778
  };
6779
+ const enr = state.inflightTools.get(log.request_id)?.enrichment;
6780
+ if (enr) log.enrichment = enr;
6487
6781
  process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
6488
6782
  `);
6489
6783
  try {
@@ -6846,6 +7140,7 @@ var init_hook_server = __esm({
6846
7140
  init_http_server();
6847
7141
  init_scopeblind_bridge();
6848
7142
  init_action_readback();
7143
+ init_receipt_enrichment();
6849
7144
  DEFAULT_PORT = 9377;
6850
7145
  LOG_FILE3 = ".protect-mcp-log.jsonl";
6851
7146
  RECEIPTS_FILE2 = ".protect-mcp-receipts.jsonl";
@@ -8295,6 +8590,8 @@ Usage:
8295
8590
  protect-mcp digest [--today] [--dir <path>]
8296
8591
  protect-mcp receipts [--last <n>] [--dir <path>]
8297
8592
  protect-mcp record [--dir <path>] [--live] [--no-open]
8593
+ protect-mcp claim [--no <cap>] [--only <c,c>] [--count <verdict>] [--dir <path>] [--output <path>]
8594
+ protect-mcp verify-claim <claim.json> [--key <public-hex>]
8298
8595
  protect-mcp bundle [--output <path>] [--dir <path>]
8299
8596
  protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
8300
8597
  protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
@@ -8333,6 +8630,8 @@ Commands:
8333
8630
  digest Generate a human-readable summary of agent activity
8334
8631
  receipts Show recent persisted signed receipts
8335
8632
  record Open a local, searchable view of your record in the browser
8633
+ claim Attest a signed, position-blind claim over your record (e.g. no egress)
8634
+ verify-claim Verify a claim attestation offline (signature + predicate + commitment)
8336
8635
  bundle Export an offline-verifiable audit bundle
8337
8636
 
8338
8637
  Examples:
@@ -10224,7 +10523,13 @@ function mapRecordEntry(e) {
10224
10523
  if (e.receipt_hash) digest = String(e.receipt_hash);
10225
10524
  else if (e.digest) digest = String(e.digest);
10226
10525
  else if (p.payload_digest && p.payload_digest.output_hash) digest = String(p.payload_digest.output_hash);
10227
- return { ts, tool, verdict, reason, hook, signed, id: String(e.request_id || p.request_id || ""), digest };
10526
+ const enr = p && typeof p.enrichment === "object" && p.enrichment || typeof e.enrichment === "object" && e.enrichment || null;
10527
+ const caps = enr && Array.isArray(enr.capabilities) ? enr.capabilities.map(String) : [];
10528
+ const sw = p && typeof p.swarm === "object" && p.swarm || null;
10529
+ const agent = sw && (sw.agent_name || sw.agent_id || sw.agent_type) ? String(sw.agent_name || sw.agent_id || sw.agent_type) : "main agent";
10530
+ const tm = p && typeof p.timing === "object" && p.timing || null;
10531
+ const dur = tm && typeof tm.tool_duration_ms === "number" ? tm.tool_duration_ms : 0;
10532
+ return { ts, tool, verdict, reason, hook, signed, caps, agent, dur, id: String(e.request_id || p.request_id || ""), digest, raw: e };
10228
10533
  }
10229
10534
  async function handleRecord(argv) {
10230
10535
  const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
@@ -10340,53 +10645,319 @@ var RECORD_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><m
10340
10645
  body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
10341
10646
  .wrap{max-width:1000px;margin:0 auto;padding:26px 22px 60px}
10342
10647
  h1{font-size:24px;margin:0 0 4px;letter-spacing:-.012em}
10343
- .meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace}
10344
- .bar{margin:18px 0 12px}
10648
+ .meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace;display:flex;align-items:center}
10649
+ .pulse{width:7px;height:7px;border-radius:100px;background:var(--g);display:inline-block;margin-left:8px;animation:pl 1.6s ease-in-out infinite}
10650
+ @keyframes pl{0%,100%{opacity:.3}50%{opacity:1}}
10651
+ @media (prefers-reduced-motion:reduce){.pulse{animation:none}}
10652
+ .stats{display:flex;gap:15px;flex-wrap:wrap;align-items:center;margin:14px 0 10px;font-size:13px}
10653
+ .stat{display:flex;align-items:center;gap:6px;color:var(--soft)}
10654
+ .stat b{color:var(--ink);font-weight:680}
10655
+ .dot{width:8px;height:8px;border-radius:100px;display:inline-block}
10656
+ .dot.g{background:var(--g)}.dot.a{background:var(--a)}.dot.r{background:var(--r)}
10657
+ .stat.sig{margin-left:auto;color:var(--g);font-weight:600}
10658
+ .actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 16px}
10659
+ .btn{cursor:pointer;font:inherit;font-size:12.5px;padding:7px 13px;border-radius:8px;border:1px solid var(--line);background:#fff;color:var(--ink);transition:border-color .12s}
10660
+ .btn:hover{border-color:var(--ink)}
10661
+ .btn.p{background:var(--ink);color:var(--paper);border-color:var(--ink)}
10662
+ .btn:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
10663
+ .vhint{font-size:12px;color:var(--faint);margin-left:auto;font-family:ui-monospace,Menlo,monospace}
10664
+ .attest{margin:0 0 12px;font-size:12.5px;color:var(--soft);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
10665
+ .cmd{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#efece4;border:1px solid var(--line);border-radius:6px;padding:3px 8px;color:var(--ink)}
10666
+ .btn2{cursor:pointer;font:inherit;font-size:12px;padding:4px 9px;border-radius:7px;border:1px solid var(--line);background:#fff;color:var(--ink)}
10667
+ .btn2:hover{border-color:var(--ink)}
10668
+ .bar{margin:6px 0 12px}
10345
10669
  input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit}
10346
10670
  .chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
10347
10671
  .chip{cursor:pointer;font-size:12px;padding:3px 10px;border-radius:100px;border:1px solid var(--line);background:#fff;color:var(--soft)}
10348
10672
  .chip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
10349
10673
  .count{color:var(--faint);font-size:12px;font-family:ui-monospace,Menlo,monospace;margin-bottom:8px}
10350
10674
  .row{border:1px solid var(--line);border-radius:9px;background:#fcfbf7;padding:11px 13px;margin-bottom:8px;cursor:pointer}
10675
+ .row.blocked{background:#fbf3f0}.row.held{background:#fbf7ee}
10351
10676
  .top{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
10352
10677
  .pill{font-size:11px;font-weight:600;padding:2px 9px;border-radius:100px}
10353
10678
  .pill.allowed{background:var(--gb);color:var(--g)}.pill.held{background:var(--ab);color:var(--a)}.pill.blocked{background:var(--rb);color:var(--r)}
10354
10679
  .tag{font-size:11px;padding:1px 7px;border-radius:100px;background:var(--paper);border:1px solid var(--line);color:var(--faint)}
10680
+ .cap{font-size:10px;padding:1px 6px;border-radius:100px;background:#eef0ea;border:1px solid var(--line);color:var(--soft)}
10681
+ .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
10682
+ .badge.sgn{background:var(--gb);color:var(--g)}
10683
+ .badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
10684
+ .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10355
10685
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10356
10686
  .det{margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:12px;color:var(--soft);font-family:ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all;display:none}
10357
10687
  .row.open .det{display:block}
10358
10688
  .foot{margin-top:22px;color:var(--faint);font-size:12px;line-height:1.6;border-top:1px solid var(--line);padding-top:14px}
10359
10689
  .foot b{color:var(--soft)}
10690
+ .viewtoggle{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
10691
+ .viewtoggle button{border:0;background:#fff;color:var(--soft);font:inherit;font-size:12.5px;padding:7px 12px;cursor:pointer}
10692
+ .viewtoggle button.on{background:var(--ink);color:var(--paper)}
10693
+ .agent{border:1px solid var(--line);border-radius:10px;background:#fcfbf7;margin-bottom:10px;overflow:hidden}
10694
+ .ahead{display:flex;gap:9px;align-items:center;flex-wrap:wrap;padding:11px 13px;cursor:pointer}
10695
+ .atwist{color:var(--faint);font-size:11px;transition:transform .12s;display:inline-block}
10696
+ .agent.open .atwist{transform:rotate(90deg)}
10697
+ .acount{font-size:12px;color:var(--faint)}
10698
+ .akids{display:none;padding:2px 12px 12px 24px;border-top:1px solid var(--line)}
10699
+ .agent.open .akids{display:block}
10700
+ .act{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:9px 4px;cursor:pointer;border-bottom:1px solid var(--line)}
10701
+ .akids .act:last-child{border-bottom:0}
10702
+ .act:hover{background:rgba(0,0,0,.02)}
10703
+ .act.blocked{background:#fbf3f0}.act.held{background:#fbf7ee}
10704
+ .act .det{flex-basis:100%}
10705
+ .act.open .det{display:block}
10706
+ .ev{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--faint);padding:5px 10px;margin-top:6px}
10707
+ .evdot{width:6px;height:6px;border-radius:100px;background:var(--faint);display:inline-block}
10708
+ .evre{color:var(--soft)}
10709
+ .badge.blk{background:var(--rb);color:var(--r)}
10360
10710
  </style></head><body><div class="wrap">
10361
10711
  <h1>Your record</h1>
10362
- <div class="meta" id="meta"></div>
10712
+ <div class="meta"><span id="meta"></span><span id="live"></span></div>
10713
+ <div class="stats" id="stats"></div>
10714
+ <div class="actions">
10715
+ <div class="viewtoggle"><button id="vlist" class="on" onclick="setView('list')">List</button><button id="vtree" onclick="setView('tree')">Tree</button></div>
10716
+ <button class="btn p" onclick="exportJsonl()">Export receipts (.jsonl)</button>
10717
+ <button class="btn" onclick="exportMd()">Export report (.md)</button>
10718
+ <button class="btn" id="cpv" onclick="copyVerify()">Copy verify command</button>
10719
+ <span class="vhint">verify offline: npx @veritasacta/verify</span>
10720
+ </div>
10363
10721
  <div class="bar"><input id="q" placeholder="Search your record: tool, reason, verdict"></div>
10364
10722
  <div class="chips" id="chips"></div>
10365
10723
  <div class="count" id="count"></div>
10724
+ <div class="attest" id="attest"></div>
10366
10725
  <div id="list"></div>
10367
- <div class="foot">Signed decisions from your own gate. This page is local and nothing was uploaded. Verify authoritatively with <b>npx protect-mcp receipts</b> or the open verifier. protect-mcp governs proposed actions before they run.</div>
10726
+ <div class="foot">Signed decisions from your own gate, on this machine. Nothing was uploaded. Each row is Ed25519-signed, and the exports carry the signatures, so anyone you hand them to (an allocator, an auditor, a counterparty) verifies offline with <b>npx @veritasacta/verify</b>, our code removed. For a Merkle-rooted evidence pack: <b>npx protect-mcp bundle</b>. To prove a claim over this record without revealing it (e.g. no egress): <b>npx protect-mcp claim --no net.egress</b>, checked offline with <b>npx protect-mcp verify-claim</b>. protect-mcp governs proposed actions before they run.</div>
10368
10727
  </div>
10369
10728
  <script>
10370
- var RECORDS=__DATA__;var META=__META__;var Q="",ACT={};
10729
+ var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
10371
10730
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
10372
10731
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
10373
10732
  function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+d.toLocaleTimeString(undefined,{hour:"2-digit",minute:"2-digit"})}
10374
- function fvals(key){var m={};RECORDS.forEach(function(r){var v=key==="Decision"?vlabel(r.verdict):r[key.toLowerCase()];if(v){m[v]=(m[v]||0)+1}});return Object.keys(m).sort(function(a,b){return m[b]-m[a]}).slice(0,8).map(function(v){return[v,m[v]]})}
10375
- function match(r){if(ACT.Decision&&vlabel(r.verdict)!==ACT.Decision)return false;if(ACT.Tool&&r.tool!==ACT.Tool)return false;if(ACT.Reason&&r.reason!==ACT.Reason)return false;if(Q){var h=(r.tool+" "+r.reason+" "+vlabel(r.verdict)+" "+r.hook).toLowerCase();if(h.indexOf(Q)<0)return false}return true}
10733
+ function counts(rows){var c={allowed:0,held:0,blocked:0,signed:0};rows.forEach(function(r){c[r.verdict]=(c[r.verdict]||0)+1;if(r.signed)c.signed++});return c}
10734
+ function fvals(key){var m={};RECORDS.forEach(function(r){var vs=key==="Decision"?[vlabel(r.verdict)]:(key==="Capability"?(r.caps||[]):[r[key.toLowerCase()]]);vs.forEach(function(v){if(v){m[v]=(m[v]||0)+1}})});return Object.keys(m).sort(function(a,b){return m[b]-m[a]}).slice(0,10).map(function(v){return[v,m[v]]})}
10735
+ function match(r){if(ACT.Decision&&vlabel(r.verdict)!==ACT.Decision)return false;if(ACT.Tool&&r.tool!==ACT.Tool)return false;if(ACT.Reason&&r.reason!==ACT.Reason)return false;if(ACT.Capability&&(r.caps||[]).indexOf(ACT.Capability)<0)return false;if(Q){var h=(r.tool+" "+r.reason+" "+vlabel(r.verdict)+" "+r.hook+" "+(r.caps||[]).join(" ")).toLowerCase();if(h.indexOf(Q)<0)return false}return true}
10736
+ function filtered(){return RECORDS.filter(match)}
10737
+ function dl(name,text,type){var b=new Blob([text],{type:type||"text/plain"});var u=URL.createObjectURL(b);var a=document.createElement("a");a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(function(){URL.revokeObjectURL(u)},1500)}
10738
+ function stamp(){return new Date().toISOString().replace(/[:.]/g,"-").slice(0,19)}
10739
+ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows.map(function(r){return JSON.stringify(r.raw||r)}).join(NL);dl("protect-mcp-record-"+stamp()+".jsonl",lines,"application/x-ndjson")}
10740
+ function exportMd(){var rows=filtered();if(!rows.length)return;var c=counts(rows);var head=["# Agent decision record","",rows.length+" decisions from "+META.file,c.allowed+" allowed, "+c.held+" held, "+c.blocked+" blocked, "+c.signed+" signed.","","Generated locally by protect-mcp. These are signed receipts; verify offline with npx @veritasacta/verify (our code removed).","","| When | Decision | Tool | Reason | Hook | Signed |","|---|---|---|---|---|---|"];var body=rows.slice(0,3000).map(function(r){return "| "+(r.ts||"")+" | "+vlabel(r.verdict)+" | "+String(r.tool||"").replace(/\\|/g,"/")+" | "+String(r.reason||"").replace(/\\|/g,"/")+" | "+(r.hook||"")+" | "+(r.signed?"yes":"no")+" |"});dl("protect-mcp-record-"+stamp()+".md",head.concat(body).join(NL)+NL,"text/markdown")}
10741
+ function copyAttest(){var a=document.getElementById("attest");var cmd=a?a.getAttribute("data-cmd"):"";try{navigator.clipboard&&cmd&&navigator.clipboard.writeText(cmd)}catch(e){}var b=document.getElementById("cpa");if(b){var t=b.textContent;b.textContent="Copied";setTimeout(function(){b.textContent=t},1200)}}
10742
+ function copyVerify(){var cmd="npx @veritasacta/verify";try{navigator.clipboard&&navigator.clipboard.writeText(cmd)}catch(e){}var b=document.getElementById("cpv");if(b){var t=b.textContent;b.textContent="Copied";setTimeout(function(){b.textContent=t},1200)}}
10743
+ function renderStats(){var c=counts(RECORDS);var p=[];p.push('<span class="stat"><b>'+RECORDS.length+'</b> decisions</span>');p.push('<span class="stat"><span class="dot g"></span>'+c.allowed+' allowed</span>');if(c.held)p.push('<span class="stat"><span class="dot a"></span>'+c.held+' held</span>');p.push('<span class="stat"><span class="dot r"></span>'+c.blocked+' blocked</span>');p.push('<span class="stat sig">'+c.signed+' signed, verifiable offline</span>');document.getElementById("stats").innerHTML=p.join("")}
10744
+ function renderList(rows){var html="";rows.slice(0,800).forEach(function(r){var sig=r.signed?'<span class="badge sgn">signed</span>':'<span class="badge log">log</span>';var dg=r.digest?'<span class="dg">'+esc(String(r.digest).slice(0,10))+'</span>':'';var ct=(r.caps||[]).map(function(c){return '<span class="cap">'+esc(c)+'</span>'}).join('');var rk="row:"+(r.id||"")+"|"+(r.ts||"");html+='<div class="row '+r.verdict+(OPEN[rk]?" open":"")+'" data-k="'+esc(rk)+'"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+ct+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+sig+dg+'<span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r.raw||r,null,2))+"</div></div>"});document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>';}
10745
+ function isLifecycle(r){var h=r.hook||"";return h==="SessionStart"||h==="SessionEnd"||h==="Stop"||h==="SubagentStart"||h==="SubagentStop"||h==="TaskCreated"||h==="TaskCompleted"||h==="ConfigChange"||h==="Notification"||h==="PreCompact";}
10746
+ function buildTree(rows){var ags={},order=[];rows.forEach(function(r){var a=r.agent||"main agent";if(!ags[a]){ags[a]={name:a,byId:{},items:[],caps:{},blocked:0,actions:0};order.push(a);}var g=ags[a];(r.caps||[]).forEach(function(c){g.caps[c]=(g.caps[c]||0)+1;});if(isLifecycle(r)){g.items.push({t:"e",ts:r.ts,r:r});return;}var id=r.id||("_"+r.ts);var n=g.byId[id];if(!n){n={t:"a",id:id,tool:r.tool,verdict:r.verdict,caps:(r.caps||[]).slice(),ts:r.ts,dur:0,signed:!!r.signed,raw:r.raw};g.byId[id]=n;g.items.push(n);g.actions++;}if(r.hook==="PostToolUse"){if(r.dur)n.dur=r.dur;if(!n.raw)n.raw=r.raw;}else{n.verdict=r.verdict;if((r.caps||[]).length)n.caps=r.caps.slice();n.raw=r.raw;n.ts=r.ts;}if(r.signed)n.signed=true;});order.forEach(function(a){var g=ags[a];g.blocked=g.items.filter(function(it){return it.t==="a"&&it.verdict==="blocked";}).length;g.items.sort(function(x,y){return (x.ts<y.ts)?-1:1;});});return order.map(function(a){return ags[a];});}
10747
+ function renderTree(ags){if(!ags.length){document.getElementById("list").innerHTML='<p style="color:#8a837a">No records match.</p>';return;}var html="",N=0;ags.forEach(function(g,gi){var capstr=Object.keys(g.caps).sort(function(a,b){return g.caps[b]-g.caps[a];}).slice(0,5).map(function(c){return '<span class="cap">'+esc(c)+'</span>';}).join('');var ak="ag:"+g.name;var op=(OPEN.hasOwnProperty(ak)?OPEN[ak]:(ags.length===1||gi===0))?" open":"";html+='<div class="agent'+op+'" data-k="'+esc(ak)+'"><div class="ahead"><span class="atwist">\u25B8</span><b>'+esc(g.name)+'</b><span class="acount">'+g.actions+' action'+(g.actions===1?'':'s')+'</span>'+(g.blocked?'<span class="badge blk">'+g.blocked+' blocked</span>':'')+capstr+'</div><div class="akids">';g.items.forEach(function(it){if(N++>1500)return;if(it.t==="e"){var r=it.r;html+='<div class="ev"><span class="evdot"></span>'+esc(r.hook||r.tool)+' <span class="evre">'+esc(r.reason)+'</span><span class="when">'+esc(when(r.ts))+'</span></div>';}else{var ct=(it.caps||[]).map(function(c){return '<span class="cap">'+esc(c)+'</span>';}).join('');var dur=it.dur?'<span class="dg">'+it.dur+'ms</span>':'';var ik="act:"+it.id;html+='<div class="act '+it.verdict+(OPEN[ik]?" open":"")+'" data-k="'+esc(ik)+'"><span class="pill '+it.verdict+'">'+vlabel(it.verdict)+'</span><b>'+esc(it.tool)+'</b>'+ct+(it.signed?'<span class="badge sgn">signed</span>':'')+dur+'<span class="when">'+esc(when(it.ts))+'</span><div class="det">'+esc(JSON.stringify(it.raw||{},null,2))+'</div></div>';}});html+='</div></div>';});if(N>1500)html+='<p style="color:#8a837a;font-size:12px;margin-top:10px">Showing the first 1500 items. Search or pick a facet to narrow.</p>';document.getElementById("list").innerHTML=html;}
10748
+ function setView(v){VIEW=v;document.getElementById("vlist").className=v==="list"?"on":"";document.getElementById("vtree").className=v==="tree"?"on":"";render();}
10376
10749
  function render(){
10377
10750
  document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local"+(META.live?" \xB7 live, updating":"");
10378
- var chips="";["Decision","Tool","Reason"].forEach(function(key){fvals(key).forEach(function(p){var on=ACT[key]===p[0];chips+='<span class="chip'+(on?" on":"")+'" data-k="'+key+'" data-v="'+esc(p[0])+'">'+esc(p[0])+" "+p[1]+"</span>"})});
10751
+ document.getElementById("live").innerHTML=META.live?'<span class="pulse"></span>':"";
10752
+ renderStats();
10753
+ var chips="";["Decision","Tool","Reason","Capability"].forEach(function(key){fvals(key).forEach(function(p){var on=ACT[key]===p[0];chips+='<span class="chip'+(on?" on":"")+'" data-k="'+key+'" data-v="'+esc(p[0])+'">'+esc(p[0])+" "+p[1]+"</span>"})});
10379
10754
  document.getElementById("chips").innerHTML=chips;
10380
10755
  var rows=RECORDS.filter(match);
10381
- document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records";
10382
- var html="";rows.slice(0,800).forEach(function(r){html+='<div class="row"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+'<span class="tag">'+(r.signed?"signed":"log")+'</span><span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r,null,2))+"</div></div>"});
10383
- document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>'}
10756
+ document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records"+(VIEW==="tree"?" \xB7 grouped by agent":"");
10757
+ var _at=document.getElementById("attest");if(ACT.Capability){var _cmd="npx protect-mcp claim --no "+ACT.Capability;_at.setAttribute("data-cmd",_cmd);_at.innerHTML='Prove it over this record, revealing nothing: <span class="cmd">'+esc(_cmd)+'</span><button class="btn2" id="cpa" onclick="copyAttest()">Copy</button>';}else{_at.innerHTML="";_at.removeAttribute("data-cmd");}
10758
+ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
10384
10759
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
10385
10760
  document.getElementById("chips").addEventListener("click",function(e){var c=e.target.closest(".chip");if(!c)return;var k=c.getAttribute("data-k"),v=c.getAttribute("data-v");ACT[k]=ACT[k]===v?undefined:v;render()});
10386
- document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
10761
+ document.getElementById("list").addEventListener("click",function(e){var ah=e.target.closest(".ahead");if(ah){var ag=ah.parentNode;ag.classList.toggle("open");var ak=ag.getAttribute("data-k");if(ak)OPEN[ak]=ag.classList.contains("open");return;}var el=e.target.closest(".act")||e.target.closest(".row");if(el){el.classList.toggle("open");var k=el.getAttribute("data-k");if(k)OPEN[k]=el.classList.contains("open");}});
10387
10762
  render();
10388
- if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){RECORDS=d.recs||[];META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;render()}).catch(function(){})};poll();setInterval(poll,2000);}
10763
+ if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){var nr=d.recs||[];var changed=nr.length!==RECORDS.length;RECORDS=nr;META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;if(changed)render()}).catch(function(){})};poll();setInterval(poll,2000);}
10389
10764
  </script></body></html>`;
10765
+ async function handleClaim(argv) {
10766
+ const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
10767
+ const { join: join8 } = await import("path");
10768
+ const { buildClaim: buildClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
10769
+ let dir = process.cwd();
10770
+ const di = argv.indexOf("--dir");
10771
+ if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
10772
+ let predicate = null;
10773
+ const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count");
10774
+ if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
10775
+ else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
10776
+ else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
10777
+ else if (cvIdx !== -1 && argv[cvIdx + 1]) predicate = { kind: "count_verdict", verdict: argv[cvIdx + 1] };
10778
+ if (!predicate) {
10779
+ process.stderr.write(`
10780
+ ${bold("protect-mcp claim")}
10781
+
10782
+ Attest a signed, position-blind claim over your record:
10783
+ --no <capability> no action used it, e.g. ${dim("--no net.egress")}
10784
+ --only <c1,c2,...> all actions confined to these capabilities
10785
+ --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
10786
+ --count <verdict> how many, e.g. ${dim("--count blocked")}
10787
+ --anchor also record the claim digest in the public append-only
10788
+ log so a counterparty can trust it is complete (only the
10789
+ hash is sent; your record stays local)
10790
+
10791
+ Example: ${bold("npx protect-mcp claim --no net.egress --anchor")}
10792
+
10793
+ `);
10794
+ process.exit(0);
10795
+ return;
10796
+ }
10797
+ const keyPath = join8(dir, "keys", "gateway.json");
10798
+ if (!existsSync9(keyPath)) {
10799
+ process.stderr.write(`
10800
+ ${bold("protect-mcp claim")}
10801
+
10802
+ No signing key at ${keyPath}. A claim must be signed. Run ${bold("npx protect-mcp init")} first.
10803
+
10804
+ `);
10805
+ process.exit(1);
10806
+ return;
10807
+ }
10808
+ let key;
10809
+ try {
10810
+ key = JSON.parse(readFileSync11(keyPath, "utf-8"));
10811
+ } catch {
10812
+ process.stderr.write(`
10813
+ protect-mcp claim: ${keyPath} is not valid JSON.
10814
+
10815
+ `);
10816
+ process.exit(1);
10817
+ return;
10818
+ }
10819
+ if (!key.privateKey || !key.publicKey) {
10820
+ process.stderr.write(`
10821
+ protect-mcp claim: ${keyPath} is missing privateKey/publicKey.
10822
+
10823
+ `);
10824
+ process.exit(1);
10825
+ return;
10826
+ }
10827
+ const recPath = join8(dir, ".protect-mcp-receipts.jsonl");
10828
+ if (!existsSync9(recPath)) {
10829
+ process.stderr.write(`
10830
+ ${bold("protect-mcp claim")}
10831
+
10832
+ No signed receipts in ${dir}. Run the gate with signing on, then try again.
10833
+
10834
+ `);
10835
+ process.exit(0);
10836
+ return;
10837
+ }
10838
+ const receipts = readFileSync11(recPath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
10839
+ try {
10840
+ return JSON.parse(l);
10841
+ } catch {
10842
+ return null;
10843
+ }
10844
+ }).filter((x) => x !== null);
10845
+ if (!receipts.length) {
10846
+ process.stderr.write(`
10847
+ protect-mcp claim: no readable receipts in ${recPath}.
10848
+
10849
+ `);
10850
+ process.exit(0);
10851
+ return;
10852
+ }
10853
+ const pack = buildClaim2(receipts, predicate, { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" }, (/* @__PURE__ */ new Date()).toISOString());
10854
+ const oi = argv.indexOf("--output");
10855
+ const out = oi !== -1 && argv[oi + 1] ? argv[oi + 1] : join8(dir, "claim-" + Date.now() + ".json");
10856
+ writeFileSync4(out, JSON.stringify(pack, null, 2) + "\n");
10857
+ process.stdout.write(`
10858
+ ${bold("\u{1F6E1}\uFE0F Signed claim")}
10859
+ `);
10860
+ process.stdout.write(` ${pack.claim.statement}: ${pack.claim.holds ? green("holds") : yellow("does not hold")} ${dim("(" + pack.claim.matched + " matched of " + pack.scope.total + " decisions)")}
10861
+ `);
10862
+ process.stdout.write(` ${dim("Position-blind: reveals decision categories, never tool inputs, outputs, or data. Ed25519-signed.")}
10863
+ `);
10864
+ process.stdout.write(` Written to ${out}
10865
+ `);
10866
+ process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
10867
+ `);
10868
+ if (argv.indexOf("--anchor") !== -1) {
10869
+ const { anchorClaim: anchorClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
10870
+ const li = argv.indexOf("--log");
10871
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
10872
+ process.stdout.write(`
10873
+ ${dim("Anchoring the claim digest to the public append-only log (only the hash leaves your machine)...")}
10874
+ `);
10875
+ const res = await anchorClaim2(
10876
+ pack,
10877
+ { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" },
10878
+ { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() }
10879
+ );
10880
+ if (res.ok) {
10881
+ const sidecar = out.replace(/\.json$/, "") + ".anchor.json";
10882
+ writeFileSync4(sidecar, JSON.stringify({ log: logBase || "https://scopeblind.com", seq: res.seq, entry_url: res.entry_url, anchored_at: res.anchored_at, claim_digest: res.claim_digest, envelope: res.envelope }, null, 2) + "\n");
10883
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)}${res.already_anchored ? dim(" (already present)") : ""} ${dim(res.entry_url || "")}
10884
+ `);
10885
+ process.stdout.write(` ${dim("A counterparty can now confirm this exact claim existed at " + (res.anchored_at || "this time") + " and cannot be quietly re-cut.")}
10886
+ `);
10887
+ process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
10888
+ `);
10889
+ process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
10890
+ `);
10891
+ } else {
10892
+ process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
10893
+ `);
10894
+ }
10895
+ }
10896
+ process.stdout.write(`
10897
+ `);
10898
+ process.exit(0);
10899
+ }
10900
+ async function handleVerifyClaim(argv) {
10901
+ const { readFileSync: readFileSync11, existsSync: existsSync9 } = await import("fs");
10902
+ const { verifyClaim: verifyClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
10903
+ const file = argv.find((a) => !a.startsWith("--"));
10904
+ if (!file || !existsSync9(file)) {
10905
+ process.stderr.write(`
10906
+ ${bold("protect-mcp verify-claim")} <claim.json> [--key <public-hex>]
10907
+
10908
+ Provide a claim pack file.
10909
+
10910
+ `);
10911
+ process.exit(2);
10912
+ return;
10913
+ }
10914
+ let pack;
10915
+ try {
10916
+ pack = JSON.parse(readFileSync11(file, "utf-8"));
10917
+ } catch {
10918
+ process.stderr.write(`
10919
+ protect-mcp verify-claim: ${file} is not valid JSON.
10920
+
10921
+ `);
10922
+ process.exit(2);
10923
+ return;
10924
+ }
10925
+ if (!pack || pack.type !== "scopeblind.claim.v1") {
10926
+ process.stderr.write(`
10927
+ protect-mcp verify-claim: not a scopeblind.claim.v1 pack.
10928
+
10929
+ `);
10930
+ process.exit(2);
10931
+ return;
10932
+ }
10933
+ const ki = argv.indexOf("--key");
10934
+ const v = verifyClaim2(pack, ki !== -1 ? argv[ki + 1] : void 0);
10935
+ const ok = (b) => b ? green("\u2713") : red("\u2717");
10936
+ process.stdout.write(`
10937
+ ${bold("protect-mcp verify-claim")}
10938
+ `);
10939
+ process.stdout.write(` Claim: ${pack.claim ? pack.claim.statement : "(none)"}
10940
+ `);
10941
+ process.stdout.write(` Holds: ${v.holds ? green("yes") : yellow("no")} ${dim("(" + v.matched + " matched of " + v.total + " decisions)")}
10942
+ `);
10943
+ process.stdout.write(` Signature: ${ok(v.authentic)} ${v.authentic ? "valid" : "INVALID"} ${dim("issuer kid " + (pack.issuer && pack.issuer.kid || "?"))}
10944
+ `);
10945
+ process.stdout.write(` Commitment: ${ok(v.root_ok)} ${v.root_ok ? "Merkle root matches the " + v.total + " disclosed decisions" : "MISMATCH"}
10946
+ `);
10947
+ process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
10948
+ `);
10949
+ process.stdout.write(`
10950
+ ${v.valid ? green("VALID") : red("INVALID")} attestation.
10951
+ `);
10952
+ process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
10953
+ `);
10954
+ process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
10955
+ `);
10956
+ process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; pin the key or anchor to the log for more.")}
10957
+
10958
+ `);
10959
+ process.exit(v.valid ? 0 : 1);
10960
+ }
10390
10961
  async function handleBundle(argv) {
10391
10962
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
10392
10963
  const { join: join8 } = await import("path");
@@ -11884,6 +12455,14 @@ async function main() {
11884
12455
  await handleRecord(args.slice(1));
11885
12456
  return;
11886
12457
  }
12458
+ if (args[0] === "claim") {
12459
+ await handleClaim(args.slice(1));
12460
+ return;
12461
+ }
12462
+ if (args[0] === "verify-claim") {
12463
+ await handleVerifyClaim(args.slice(1));
12464
+ return;
12465
+ }
11887
12466
  if (args[0] === "init-hooks") {
11888
12467
  await handleInitHooks(args.slice(1));
11889
12468
  process.exit(0);