protect-mcp 0.9.1 → 0.9.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.
package/dist/cli.js CHANGED
@@ -456,7 +456,13 @@ function signDecision(entry) {
456
456
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
457
457
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
458
458
  // - uncertified = unsigned receipt (shadow mode, no signing configured)
459
- issuer_certification: signerState ? "self-signed" : "uncertified"
459
+ issuer_certification: signerState ? "self-signed" : "uncertified",
460
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
461
+ // self-contained: any verifier (including the record viewer, in-browser)
462
+ // can check the signature without a side channel. Binding the key inside
463
+ // the signature means it cannot be swapped without breaking the signature;
464
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
465
+ public_key: signerState.publicKey
460
466
  };
461
467
  if (entry.tier) payload.tier = entry.tier;
462
468
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -5156,7 +5162,7 @@ var init_receipt_enrichment = __esm({
5156
5162
  "use strict";
5157
5163
  init_sha256();
5158
5164
  init_utils();
5159
- ENRICHMENT_VERSION = 1;
5165
+ ENRICHMENT_VERSION = 2;
5160
5166
  RULES = [
5161
5167
  { cap: "exec.shell", tool: /bash|shell|exec|terminal|run_command|command/ },
5162
5168
  { cap: "fs.read", tool: /(^|[_.])(read|cat|glob|grep|search|ls|view|list_files|open)/ },
@@ -5176,12 +5182,19 @@ var init_receipt_enrichment = __esm({
5176
5182
  // src/claim.ts
5177
5183
  var claim_exports = {};
5178
5184
  __export(claim_exports, {
5185
+ ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
5179
5186
  CLAIM_TYPE: () => CLAIM_TYPE,
5187
+ DEFAULT_LOG: () => DEFAULT_LOG,
5188
+ anchorClaim: () => anchorClaim,
5189
+ buildAnchorEnvelope: () => buildAnchorEnvelope,
5180
5190
  buildClaim: () => buildClaim,
5191
+ checkClaimAnchor: () => checkClaimAnchor,
5192
+ claimDigest: () => claimDigest,
5181
5193
  evaluate: () => evaluate,
5182
5194
  leafHash: () => leafHash,
5183
5195
  merkleRoot: () => merkleRoot,
5184
5196
  receiptToLeaf: () => receiptToLeaf,
5197
+ verifyAnchorEnvelope: () => verifyAnchorEnvelope,
5185
5198
  verifyClaim: () => verifyClaim
5186
5199
  });
5187
5200
  function sha256Hex3(input) {
@@ -5286,7 +5299,145 @@ function verifyClaim(pack, overridePublicKey) {
5286
5299
  reasons
5287
5300
  };
5288
5301
  }
5289
- var CLAIM_TYPE;
5302
+ function anchorDeepSort(o) {
5303
+ if (o === null || typeof o !== "object") return o;
5304
+ if (Array.isArray(o)) return o.map(anchorDeepSort);
5305
+ const src = o;
5306
+ const out = {};
5307
+ for (const k of Object.keys(src).sort()) out[k] = anchorDeepSort(src[k]);
5308
+ return out;
5309
+ }
5310
+ function toBase64(bytes) {
5311
+ let bin = "";
5312
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
5313
+ return btoa(bin);
5314
+ }
5315
+ function claimDigest(pack) {
5316
+ return sha256Hex3(canonicalJson(pack));
5317
+ }
5318
+ function buildAnchorEnvelope(pack, key, issuedAt) {
5319
+ const signed = {
5320
+ type: "evidence_pack",
5321
+ schema: ANCHOR_SCHEMA,
5322
+ anchors: "protect-mcp-claim",
5323
+ claim_digest: claimDigest(pack),
5324
+ record_root: pack.record.root,
5325
+ statement: pack.claim.statement,
5326
+ holds: pack.claim.holds,
5327
+ matched: pack.claim.matched,
5328
+ total: pack.scope.total,
5329
+ issued_at: issuedAt,
5330
+ verification_key: key.publicKey,
5331
+ disclosure: "internal"
5332
+ };
5333
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5334
+ const digest = bytesToHex(hash);
5335
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5336
+ return { ...signed, signature, digest };
5337
+ }
5338
+ function verifyAnchorEnvelope(pack, envelope) {
5339
+ const reasons = [];
5340
+ if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
5341
+ return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
5342
+ }
5343
+ const expected = claimDigest(pack);
5344
+ if (envelope.claim_digest !== expected) {
5345
+ reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
5346
+ }
5347
+ if (envelope.record_root !== pack.record.root) {
5348
+ reasons.push("anchored envelope commits to a different record root");
5349
+ }
5350
+ if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
5351
+ reasons.push("anchor was signed by a different key than the claim issuer");
5352
+ }
5353
+ try {
5354
+ const { signature, digest, ...signed } = envelope;
5355
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5356
+ if (bytesToHex(hash) !== String(digest).toLowerCase()) {
5357
+ reasons.push("envelope digest does not match its contents");
5358
+ } else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
5359
+ reasons.push("envelope signature does not verify");
5360
+ }
5361
+ } catch {
5362
+ reasons.push("envelope signature does not verify");
5363
+ }
5364
+ return { ok: reasons.length === 0, reasons };
5365
+ }
5366
+ async function checkClaimAnchor(pack, sidecar, opts) {
5367
+ const reasons = [];
5368
+ const envelope = sidecar && sidecar.envelope;
5369
+ if (!envelope) {
5370
+ return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
5371
+ }
5372
+ const local = verifyAnchorEnvelope(pack, envelope);
5373
+ reasons.push(...local.reasons);
5374
+ const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
5375
+ const out = {
5376
+ local_ok: local.ok,
5377
+ log_ok: null,
5378
+ seq: sidecar.seq,
5379
+ anchored_at: sidecar.anchored_at,
5380
+ entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
5381
+ reasons
5382
+ };
5383
+ if (opts?.offline) return out;
5384
+ const doFetch = opts?.fetchImpl || globalThis.fetch;
5385
+ if (!doFetch) return out;
5386
+ try {
5387
+ const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
5388
+ const data = await resp.json().catch(() => null);
5389
+ if (!resp.ok || !data) {
5390
+ out.log_ok = null;
5391
+ return out;
5392
+ }
5393
+ if (data.anchored !== true) {
5394
+ out.log_ok = false;
5395
+ out.reasons.push("the public log does not contain this anchor digest");
5396
+ return out;
5397
+ }
5398
+ if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
5399
+ out.log_ok = false;
5400
+ out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
5401
+ return out;
5402
+ }
5403
+ out.log_ok = true;
5404
+ if (typeof data.seq === "number") out.seq = data.seq;
5405
+ return out;
5406
+ } catch {
5407
+ out.log_ok = null;
5408
+ return out;
5409
+ }
5410
+ }
5411
+ async function anchorClaim(pack, key, opts) {
5412
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
5413
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5414
+ const doFetch = opts.fetchImpl || globalThis.fetch;
5415
+ if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
5416
+ const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
5417
+ try {
5418
+ const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
5419
+ method: "POST",
5420
+ headers: { "content-type": "application/json" },
5421
+ body: JSON.stringify({ encoded })
5422
+ });
5423
+ const data = await resp.json().catch(() => null);
5424
+ if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
5425
+ return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
5426
+ }
5427
+ return {
5428
+ ok: true,
5429
+ claim_digest: envelope.claim_digest,
5430
+ seq: data.seq,
5431
+ entry_url: `${base}/fn/log/${data.seq}`,
5432
+ anchored_at: data.anchored_at,
5433
+ already_anchored: !!data.already_anchored,
5434
+ envelope
5435
+ };
5436
+ } catch {
5437
+ return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
5438
+ }
5439
+ }
5440
+ var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
5290
5441
  var init_claim = __esm({
5291
5442
  "src/claim.ts"() {
5292
5443
  "use strict";
@@ -5295,6 +5446,8 @@ var init_claim = __esm({
5295
5446
  init_ed25519();
5296
5447
  init_receipt_enrichment();
5297
5448
  CLAIM_TYPE = "scopeblind.claim.v1";
5449
+ ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
5450
+ DEFAULT_LOG = "https://scopeblind.com";
5298
5451
  }
5299
5452
  });
5300
5453
 
@@ -10491,6 +10644,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10491
10644
  return null;
10492
10645
  }
10493
10646
  }).filter((x) => x !== null).map(mapRecordEntry);
10647
+ let pinnedKey = "";
10648
+ let pinnedKid = "";
10649
+ try {
10650
+ const kd = JSON.parse(readFileSync11(join8(dir, "keys", "gateway.json"), "utf-8"));
10651
+ if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
10652
+ pinnedKey = kd.publicKey;
10653
+ pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
10654
+ }
10655
+ } catch {
10656
+ }
10494
10657
  const openTarget = (target) => {
10495
10658
  if (argv.includes("--no-open")) return;
10496
10659
  const platform = process.platform;
@@ -10518,7 +10681,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10518
10681
  res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
10519
10682
  return;
10520
10683
  }
10521
- const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
10684
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
10522
10685
  const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
10523
10686
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
10524
10687
  res.end(page);
@@ -10543,7 +10706,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
10543
10706
  return;
10544
10707
  }
10545
10708
  const recs = readRecs(chosen);
10546
- const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
10709
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
10547
10710
  const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10548
10711
  const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10549
10712
  writeFileSync4(out, html);
@@ -10609,6 +10772,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10609
10772
  .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
10610
10773
  .badge.sgn{background:var(--gb);color:var(--g)}
10611
10774
  .badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
10775
+ .badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
10776
+ .badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
10777
+ .stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
10612
10778
  .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10613
10779
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10614
10780
  .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}
@@ -10655,6 +10821,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10655
10821
  </div>
10656
10822
  <script>
10657
10823
  var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
10824
+ // In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
10825
+ // preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
10826
+ // signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
10827
+ // public half, injected by the CLI) = authenticity; key embedded in the receipt
10828
+ // payload (0.9.3+) = self-consistency. Everything runs locally.
10829
+ var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
10830
+ function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
10831
+ function hexb(h){h=String(h||"");var a=new Uint8Array(h.length>>1);for(var i=0;i<a.length;i++)a[i]=parseInt(h.substr(i*2,2),16);return a}
10832
+ function canon(v){return JSON.stringify(v,function(k,x){if(x&&typeof x==="object"&&!Array.isArray(x)){var s={},ks=Object.keys(x).sort();for(var i=0;i<ks.length;i++)s[ks[i]]=x[ks[i]];return s}return x})}
10833
+ async function edv(sig,msg,pub){var key=await crypto.subtle.importKey("raw",hexb(pub),{name:"Ed25519"},false,["verify"]);return crypto.subtle.verify({name:"Ed25519"},key,hexb(sig),msg)}
10834
+ async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
10835
+ var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
10836
+ var msg=new TextEncoder().encode(canon(rest));
10837
+ var pin=String(META.pinned_key||"").toLowerCase();
10838
+ var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
10839
+ if(!/^[0-9a-f]{64}$/.test(emb))emb="";
10840
+ if(pin){if(await edv(raw.signature,msg,pin))return"ok";if(emb&&emb!==pin&&await edv(raw.signature,msg,emb))return"foreign";return"bad"}
10841
+ if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
10842
+ return"nokey"}
10843
+ function vsum(){var s={ok:0,bad:0,foreign:0,nokey:0};RECORDS.forEach(function(r){if(!r.signed)return;var v=VSTATE[vkey(r)];if(v&&s[v]!==undefined)s[v]++});return s}
10844
+ async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
10845
+ try{var rows=RECORDS.slice(0,1500);for(var i=0;i<rows.length;i++){var r=rows[i],kk=vkey(r);if(VSTATE[kk])continue;
10846
+ try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
10847
+ finally{VDONE=true;VBUSY=false;render()}}
10658
10848
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
10659
10849
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
10660
10850
  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"})}
@@ -10668,8 +10858,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
10668
10858
  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")}
10669
10859
  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)}}
10670
10860
  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)}}
10671
- 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("")}
10672
- 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>';}
10861
+ 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>');var st;if(!c.signed){st='0 signed, verifiable offline'}else if(VUNSUP||!(window.crypto&&crypto.subtle)){st=c.signed+' signed, verifiable offline <span class="dim2">(in-browser check unavailable here; run npx protect-mcp receipts)</span>'}else if(!VDONE){st=c.signed+' signed \xB7 verifying in your browser\u2026'}else{var s=vsum();st=s.ok+' of '+c.signed+' signatures verified in your browser';if(s.foreign)st+=' <span class="warnk">\xB7 '+s.foreign+' signed by an unpinned key</span>';if(s.bad)st+=' <span class="badk">\xB7 '+s.bad+' INVALID</span>';if(s.nokey)st+=' <span class="dim2">\xB7 '+s.nokey+' need a key to check</span>'}
10862
+ p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
10863
+ function renderList(rows){var html="";rows.slice(0,800).forEach(function(r){var vs=VSTATE[vkey(r)];var sig=!r.signed?'<span class="badge log">log</span>':vs==="ok"?'<span class="badge sgn">\u2713 verified</span>':vs==="bad"?'<span class="badge vbad">\u2717 invalid signature</span>':vs==="foreign"?'<span class="badge vfor">signed \xB7 unpinned key</span>':'<span class="badge sgn">signed</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>';}
10673
10864
  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";}
10674
10865
  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];});}
10675
10866
  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;}
@@ -10687,8 +10878,8 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
10687
10878
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
10688
10879
  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()});
10689
10880
  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");}});
10690
- render();
10691
- 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);}
10881
+ render();kickVerify();
10882
+ 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();kickVerify()}}).catch(function(){})};poll();setInterval(poll,2000);}
10692
10883
  </script></body></html>`;
10693
10884
  async function handleClaim(argv) {
10694
10885
  const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
@@ -10712,8 +10903,11 @@ Attest a signed, position-blind claim over your record:
10712
10903
  --only <c1,c2,...> all actions confined to these capabilities
10713
10904
  --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
10714
10905
  --count <verdict> how many, e.g. ${dim("--count blocked")}
10906
+ --anchor also record the claim digest in the public append-only
10907
+ log so a counterparty can trust it is complete (only the
10908
+ hash is sent; your record stays local)
10715
10909
 
10716
- Example: ${bold("npx protect-mcp claim --no net.egress")}
10910
+ Example: ${bold("npx protect-mcp claim --no net.egress --anchor")}
10717
10911
 
10718
10912
  `);
10719
10913
  process.exit(0);
@@ -10789,7 +10983,36 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
10789
10983
  process.stdout.write(` Written to ${out}
10790
10984
  `);
10791
10985
  process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
10792
-
10986
+ `);
10987
+ if (argv.indexOf("--anchor") !== -1) {
10988
+ const { anchorClaim: anchorClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
10989
+ const li = argv.indexOf("--log");
10990
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
10991
+ process.stdout.write(`
10992
+ ${dim("Anchoring the claim digest to the public append-only log (only the hash leaves your machine)...")}
10993
+ `);
10994
+ const res = await anchorClaim2(
10995
+ pack,
10996
+ { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" },
10997
+ { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() }
10998
+ );
10999
+ if (res.ok) {
11000
+ const sidecar = out.replace(/\.json$/, "") + ".anchor.json";
11001
+ 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");
11002
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)}${res.already_anchored ? dim(" (already present)") : ""} ${dim(res.entry_url || "")}
11003
+ `);
11004
+ 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.")}
11005
+ `);
11006
+ process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
11007
+ `);
11008
+ process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
11009
+ `);
11010
+ } else {
11011
+ process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
11012
+ `);
11013
+ }
11014
+ }
11015
+ process.stdout.write(`
10793
11016
  `);
10794
11017
  process.exit(0);
10795
11018
  }
@@ -10842,17 +11065,64 @@ ${bold("protect-mcp verify-claim")}
10842
11065
  `);
10843
11066
  process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
10844
11067
  `);
11068
+ const ai = argv.indexOf("--anchor-file");
11069
+ const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
11070
+ const requireAnchor = argv.includes("--check-anchor");
11071
+ let anchorOk = true;
11072
+ if (existsSync9(sidecarPath)) {
11073
+ const { checkClaimAnchor: checkClaimAnchor2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
11074
+ let sidecar = null;
11075
+ try {
11076
+ sidecar = JSON.parse(readFileSync11(sidecarPath, "utf-8"));
11077
+ } catch {
11078
+ }
11079
+ if (!sidecar) {
11080
+ anchorOk = false;
11081
+ process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
11082
+ `);
11083
+ } else {
11084
+ const a = await checkClaimAnchor2(pack, sidecar, { offline: argv.includes("--offline") });
11085
+ anchorOk = a.local_ok && a.log_ok !== false;
11086
+ if (a.local_ok) {
11087
+ process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
11088
+ `);
11089
+ process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
11090
+ `);
11091
+ } else {
11092
+ for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
11093
+ `);
11094
+ }
11095
+ if (a.log_ok === true) {
11096
+ process.stdout.write(` ${green("\u2713")} public log confirms it${typeof a.seq === "number" ? ": entry " + bold("#" + a.seq) : ""}${a.anchored_at ? dim(" \xB7 anchored " + a.anchored_at) : ""}
11097
+ `);
11098
+ } else if (a.log_ok === false) {
11099
+ process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
11100
+ `);
11101
+ } else if (a.local_ok) {
11102
+ process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
11103
+ `);
11104
+ }
11105
+ }
11106
+ } else if (requireAnchor) {
11107
+ anchorOk = false;
11108
+ process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
11109
+ `);
11110
+ } else {
11111
+ process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
11112
+ `);
11113
+ }
11114
+ const finalValid = v.valid && anchorOk;
10845
11115
  process.stdout.write(`
10846
- ${v.valid ? green("VALID") : red("INVALID")} attestation.
11116
+ ${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
10847
11117
  `);
10848
11118
  process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
10849
11119
  `);
10850
11120
  process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
10851
11121
  `);
10852
- process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; pin the key or anchor to the log for more.")}
11122
+ process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
10853
11123
 
10854
11124
  `);
10855
- process.exit(v.valid ? 0 : 1);
11125
+ process.exit(finalValid ? 0 : 1);
10856
11126
  }
10857
11127
  async function handleBundle(argv) {
10858
11128
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");