protect-mcp 0.9.2 → 0.9.4

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.mjs CHANGED
@@ -11,11 +11,11 @@ import {
11
11
  readInstalledConnectorPilots,
12
12
  simulate,
13
13
  writeConnectorPilots
14
- } from "./chunk-CXW2EIRM.mjs";
14
+ } from "./chunk-7MHK5RF4.mjs";
15
15
  import {
16
16
  ProtectGateway,
17
17
  validateCredentials
18
- } from "./chunk-GHR65WVD.mjs";
18
+ } from "./chunk-PB3TC7E3.mjs";
19
19
  import {
20
20
  buildActionReadback,
21
21
  evaluateCedar,
@@ -26,7 +26,7 @@ import {
26
26
  policySetFromSource,
27
27
  runEvaluatorSelfTest,
28
28
  signDecision
29
- } from "./chunk-IDUH2O4Q.mjs";
29
+ } from "./chunk-XLJUZ4WO.mjs";
30
30
  import "./chunk-PQJP2ZCI.mjs";
31
31
 
32
32
  // src/cli.ts
@@ -100,8 +100,12 @@ Commands:
100
100
  digest Generate a human-readable summary of agent activity
101
101
  receipts Show recent persisted signed receipts
102
102
  record Open a local, searchable view of your record in the browser
103
- claim Attest a signed, position-blind claim over your record (e.g. no egress)
104
- verify-claim Verify a claim attestation offline (signature + predicate + commitment)
103
+ claim Attest a signed, position-blind claim over your record (e.g. no egress,
104
+ no payment, every payment under a cap)
105
+ verify-claim Verify a claim attestation offline (signature + predicate + commitment
106
+ + the anchor sidecar and issuer identity when present)
107
+ anchor-record Checkpoint the record's Merkle root + count into the public log
108
+ (heartbeat-friendly: skips when unchanged; only hashes leave)
105
109
  bundle Export an offline-verifiable audit bundle
106
110
 
107
111
  Examples:
@@ -2033,6 +2037,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
2033
2037
  return null;
2034
2038
  }
2035
2039
  }).filter((x) => x !== null).map(mapRecordEntry);
2040
+ let pinnedKey = "";
2041
+ let pinnedKid = "";
2042
+ try {
2043
+ const kd = JSON.parse(readFileSync(join(dir, "keys", "gateway.json"), "utf-8"));
2044
+ if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
2045
+ pinnedKey = kd.publicKey;
2046
+ pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
2047
+ }
2048
+ } catch {
2049
+ }
2036
2050
  const openTarget = (target) => {
2037
2051
  if (argv.includes("--no-open")) return;
2038
2052
  const platform = process.platform;
@@ -2060,7 +2074,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
2060
2074
  res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
2061
2075
  return;
2062
2076
  }
2063
- const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
2077
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
2064
2078
  const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
2065
2079
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2066
2080
  res.end(page);
@@ -2085,7 +2099,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
2085
2099
  return;
2086
2100
  }
2087
2101
  const recs = readRecs(chosen);
2088
- const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
2102
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
2089
2103
  const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
2090
2104
  const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
2091
2105
  writeFileSync(out, html);
@@ -2151,6 +2165,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
2151
2165
  .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
2152
2166
  .badge.sgn{background:var(--gb);color:var(--g)}
2153
2167
  .badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
2168
+ .badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
2169
+ .badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
2170
+ .stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
2154
2171
  .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
2155
2172
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
2156
2173
  .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}
@@ -2197,6 +2214,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
2197
2214
  </div>
2198
2215
  <script>
2199
2216
  var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
2217
+ // In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
2218
+ // preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
2219
+ // signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
2220
+ // public half, injected by the CLI) = authenticity; key embedded in the receipt
2221
+ // payload (0.9.3+) = self-consistency. Everything runs locally.
2222
+ var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
2223
+ function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
2224
+ 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}
2225
+ 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})}
2226
+ 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)}
2227
+ async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
2228
+ var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
2229
+ var msg=new TextEncoder().encode(canon(rest));
2230
+ var pin=String(META.pinned_key||"").toLowerCase();
2231
+ var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
2232
+ if(!/^[0-9a-f]{64}$/.test(emb))emb="";
2233
+ 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"}
2234
+ if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
2235
+ return"nokey"}
2236
+ 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}
2237
+ async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
2238
+ 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;
2239
+ try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
2240
+ finally{VDONE=true;VBUSY=false;render()}}
2200
2241
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
2201
2242
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
2202
2243
  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"})}
@@ -2210,8 +2251,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
2210
2251
  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")}
2211
2252
  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)}}
2212
2253
  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)}}
2213
- 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("")}
2214
- 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>';}
2254
+ 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>'}
2255
+ p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
2256
+ 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>';}
2215
2257
  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";}
2216
2258
  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];});}
2217
2259
  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;}
@@ -2229,31 +2271,34 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
2229
2271
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
2230
2272
  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()});
2231
2273
  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");}});
2232
- render();
2233
- 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);}
2274
+ render();kickVerify();
2275
+ 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);}
2234
2276
  </script></body></html>`;
2235
2277
  async function handleClaim(argv) {
2236
2278
  const { readFileSync, existsSync, writeFileSync } = await import("fs");
2237
2279
  const { join } = await import("path");
2238
- const { buildClaim } = await import("./claim-TUDH2WPB.mjs");
2280
+ const { buildClaim } = await import("./claim-RIXFOQM7.mjs");
2239
2281
  let dir = process.cwd();
2240
2282
  const di = argv.indexOf("--dir");
2241
2283
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
2242
2284
  let predicate = null;
2243
- const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count");
2285
+ const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count"), puIdx = argv.indexOf("--payment-under");
2244
2286
  if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
2245
2287
  else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
2246
2288
  else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
2247
2289
  else if (cvIdx !== -1 && argv[cvIdx + 1]) predicate = { kind: "count_verdict", verdict: argv[cvIdx + 1] };
2290
+ else if (puIdx !== -1 && argv[puIdx + 1] && isFinite(parseFloat(argv[puIdx + 1]))) predicate = { kind: "payment_under", cap: parseFloat(argv[puIdx + 1]) };
2248
2291
  if (!predicate) {
2249
2292
  process.stderr.write(`
2250
2293
  ${bold("protect-mcp claim")}
2251
2294
 
2252
2295
  Attest a signed, position-blind claim over your record:
2253
- --no <capability> no action used it, e.g. ${dim("--no net.egress")}
2296
+ --no <capability> no action used it, e.g. ${dim("--no net.egress")} or ${dim("--no payment")}
2254
2297
  --only <c1,c2,...> all actions confined to these capabilities
2255
2298
  --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
2256
2299
  --count <verdict> how many, e.g. ${dim("--count blocked")}
2300
+ --payment-under <cap> every agent payment stayed under the cap (amounts the
2301
+ gate could not read count as OVER, so this cannot lie)
2257
2302
  --anchor also record the claim digest in the public append-only
2258
2303
  log so a counterparty can trust it is complete (only the
2259
2304
  hash is sent; your record stays local)
@@ -2336,7 +2381,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2336
2381
  process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
2337
2382
  `);
2338
2383
  if (argv.indexOf("--anchor") !== -1) {
2339
- const { anchorClaim } = await import("./claim-TUDH2WPB.mjs");
2384
+ const { anchorClaim } = await import("./claim-RIXFOQM7.mjs");
2340
2385
  const li = argv.indexOf("--log");
2341
2386
  const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
2342
2387
  process.stdout.write(`
@@ -2356,8 +2401,18 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2356
2401
  `);
2357
2402
  process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
2358
2403
  `);
2359
- process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
2404
+ const { lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
2405
+ const who = await lookupPinnedIdentity(key.publicKey, { log: logBase });
2406
+ if (who && who.found && !who.revoked) {
2407
+ process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory" + (who.enrolled_at ? ", enrolled " + who.enrolled_at.slice(0, 10) : "") + ")")}
2360
2408
  `);
2409
+ } else if (who && who.found && who.revoked) {
2410
+ process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
2411
+ `);
2412
+ } else {
2413
+ process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). To anchor as a named org a counterparty can pin, see")} ${bold("scopeblind.com/enroll")}
2414
+ `);
2415
+ }
2361
2416
  } else {
2362
2417
  process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
2363
2418
  `);
@@ -2367,9 +2422,135 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2367
2422
  `);
2368
2423
  process.exit(0);
2369
2424
  }
2425
+ async function handleAnchorRecord(argv) {
2426
+ const { readFileSync, existsSync, appendFileSync } = await import("fs");
2427
+ const { join } = await import("path");
2428
+ const { anchorRecordCheckpoint, buildRecordCheckpoint, lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
2429
+ let dir = process.cwd();
2430
+ const di = argv.indexOf("--dir");
2431
+ if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
2432
+ const li = argv.indexOf("--log");
2433
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
2434
+ const keyPath = join(dir, "keys", "gateway.json");
2435
+ if (!existsSync(keyPath)) {
2436
+ process.stderr.write(`
2437
+ ${bold("protect-mcp anchor-record")}
2438
+
2439
+ No signing key at ${keyPath}. A checkpoint must be signed. Run ${bold("npx protect-mcp init")} first.
2440
+
2441
+ `);
2442
+ process.exit(1);
2443
+ return;
2444
+ }
2445
+ let key;
2446
+ try {
2447
+ key = JSON.parse(readFileSync(keyPath, "utf-8"));
2448
+ } catch {
2449
+ process.stderr.write(`
2450
+ protect-mcp anchor-record: ${keyPath} is not valid JSON.
2451
+
2452
+ `);
2453
+ process.exit(1);
2454
+ return;
2455
+ }
2456
+ if (!key.privateKey || !key.publicKey) {
2457
+ process.stderr.write(`
2458
+ protect-mcp anchor-record: ${keyPath} is missing privateKey/publicKey.
2459
+
2460
+ `);
2461
+ process.exit(1);
2462
+ return;
2463
+ }
2464
+ const recPath = join(dir, ".protect-mcp-receipts.jsonl");
2465
+ if (!existsSync(recPath)) {
2466
+ process.stderr.write(`
2467
+ ${bold("protect-mcp anchor-record")}
2468
+
2469
+ No signed receipts in ${dir}. Run the gate with signing on, then try again.
2470
+
2471
+ `);
2472
+ process.exit(0);
2473
+ return;
2474
+ }
2475
+ const receipts = readFileSync(recPath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
2476
+ try {
2477
+ return JSON.parse(l);
2478
+ } catch {
2479
+ return null;
2480
+ }
2481
+ }).filter((x) => x !== null);
2482
+ if (!receipts.length) {
2483
+ process.stderr.write(`
2484
+ protect-mcp anchor-record: no readable receipts in ${recPath}.
2485
+
2486
+ `);
2487
+ process.exit(0);
2488
+ return;
2489
+ }
2490
+ const claimKey = { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" };
2491
+ const historyPath = join(dir, ".protect-mcp-anchors.jsonl");
2492
+ const preview = buildRecordCheckpoint(receipts, claimKey, "preview");
2493
+ if (!argv.includes("--force") && existsSync(historyPath)) {
2494
+ const lines = readFileSync(historyPath, "utf-8").split(/\r?\n/).filter(Boolean);
2495
+ const last = lines.length ? (() => {
2496
+ try {
2497
+ return JSON.parse(lines[lines.length - 1]);
2498
+ } catch {
2499
+ return null;
2500
+ }
2501
+ })() : null;
2502
+ if (last && last.record_root === preview.record_root && last.total === preview.total) {
2503
+ process.stdout.write(`
2504
+ ${bold("\u{1F6E1}\uFE0F Record checkpoint")}
2505
+ `);
2506
+ process.stdout.write(` Unchanged since entry ${bold("#" + last.seq)} ${dim("(" + last.total + " receipts, anchored " + (last.anchored_at || "") + ")")}. Nothing new to anchor.
2507
+ `);
2508
+ process.stdout.write(` ${dim("Use --force to re-anchor anyway.")}
2509
+
2510
+ `);
2511
+ process.exit(0);
2512
+ return;
2513
+ }
2514
+ }
2515
+ const res = await anchorRecordCheckpoint(receipts, claimKey, { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() });
2516
+ process.stdout.write(`
2517
+ ${bold("\u{1F6E1}\uFE0F Record checkpoint")}
2518
+ `);
2519
+ process.stdout.write(` ${res.total} receipts ${dim("\xB7")} root ${dim(res.record_root.slice(0, 16) + "\u2026")} ${dim("(" + res.checkpoint.from.slice(0, 10) + " \u2192 " + res.checkpoint.to.slice(0, 10) + ")")}
2520
+ `);
2521
+ if (!res.ok) {
2522
+ process.stdout.write(` ${yellow("Anchor failed")} ${dim("(" + (res.error || "unavailable") + "). Nothing was recorded; try again.")}
2523
+
2524
+ `);
2525
+ process.exit(1);
2526
+ return;
2527
+ }
2528
+ appendFileSync(historyPath, JSON.stringify({ schema: res.checkpoint.schema, seq: res.seq, anchored_at: res.anchored_at, total: res.total, record_root: res.record_root, entry_url: res.entry_url, digest: res.checkpoint.digest }) + "\n");
2529
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)} ${dim(res.entry_url || "")}
2530
+ `);
2531
+ process.stdout.write(` ${dim("Only the root, count, and time range were sent. History: " + historyPath)}
2532
+ `);
2533
+ const who = await lookupPinnedIdentity(claimKey.publicKey, { log: logBase });
2534
+ if (who && who.found && !who.revoked) {
2535
+ process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory)")}
2536
+ `);
2537
+ } else if (who && who.found && who.revoked) {
2538
+ process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
2539
+ `);
2540
+ } else {
2541
+ process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). Named identity: scopeblind.com/enroll")}
2542
+ `);
2543
+ }
2544
+ process.stdout.write(` ${dim("A claim whose commitment matches this root is provably over the complete record as of")}
2545
+ `);
2546
+ process.stdout.write(` ${dim("this checkpoint. Run this on a heartbeat (e.g. cron) to keep the anchored history growing.")}
2547
+
2548
+ `);
2549
+ process.exit(0);
2550
+ }
2370
2551
  async function handleVerifyClaim(argv) {
2371
2552
  const { readFileSync, existsSync } = await import("fs");
2372
- const { verifyClaim } = await import("./claim-TUDH2WPB.mjs");
2553
+ const { verifyClaim } = await import("./claim-RIXFOQM7.mjs");
2373
2554
  const file = argv.find((a) => !a.startsWith("--"));
2374
2555
  if (!file || !existsSync(file)) {
2375
2556
  process.stderr.write(`
@@ -2416,17 +2597,79 @@ ${bold("protect-mcp verify-claim")}
2416
2597
  `);
2417
2598
  process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
2418
2599
  `);
2600
+ const ai = argv.indexOf("--anchor-file");
2601
+ const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
2602
+ const requireAnchor = argv.includes("--check-anchor");
2603
+ let anchorOk = true;
2604
+ if (existsSync(sidecarPath)) {
2605
+ const { checkClaimAnchor } = await import("./claim-RIXFOQM7.mjs");
2606
+ let sidecar = null;
2607
+ try {
2608
+ sidecar = JSON.parse(readFileSync(sidecarPath, "utf-8"));
2609
+ } catch {
2610
+ }
2611
+ if (!sidecar) {
2612
+ anchorOk = false;
2613
+ process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
2614
+ `);
2615
+ } else {
2616
+ const a = await checkClaimAnchor(pack, sidecar, { offline: argv.includes("--offline") });
2617
+ anchorOk = a.local_ok && a.log_ok !== false;
2618
+ if (a.local_ok) {
2619
+ process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
2620
+ `);
2621
+ process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
2622
+ `);
2623
+ } else {
2624
+ for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
2625
+ `);
2626
+ }
2627
+ if (a.log_ok === true) {
2628
+ 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) : ""}
2629
+ `);
2630
+ } else if (a.log_ok === false) {
2631
+ process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
2632
+ `);
2633
+ } else if (a.local_ok) {
2634
+ process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
2635
+ `);
2636
+ }
2637
+ if (!argv.includes("--offline") && sidecar.envelope) {
2638
+ const { lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
2639
+ const who = await lookupPinnedIdentity(sidecar.envelope.verification_key, {});
2640
+ if (who && who.found && !who.revoked) {
2641
+ process.stdout.write(` ${green("\u2713")} issuer key pinned to ${bold(who.name || who.slug || "an enrolled org")} ${dim("(ScopeBlind key directory)")}
2642
+ `);
2643
+ } else if (who && who.found && who.revoked) {
2644
+ anchorOk = false;
2645
+ process.stdout.write(` ${red("\u2717")} issuer key is REVOKED in the ScopeBlind key directory
2646
+ `);
2647
+ } else if (who && !who.found) {
2648
+ process.stdout.write(` ${dim("issuer key not enrolled (anonymous issuer); named identities pin via scopeblind.com/enroll")}
2649
+ `);
2650
+ }
2651
+ }
2652
+ }
2653
+ } else if (requireAnchor) {
2654
+ anchorOk = false;
2655
+ process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
2656
+ `);
2657
+ } else {
2658
+ process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
2659
+ `);
2660
+ }
2661
+ const finalValid = v.valid && anchorOk;
2419
2662
  process.stdout.write(`
2420
- ${v.valid ? green("VALID") : red("INVALID")} attestation.
2663
+ ${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
2421
2664
  `);
2422
2665
  process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
2423
2666
  `);
2424
2667
  process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
2425
2668
  `);
2426
- process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; pin the key or anchor to the log for more.")}
2669
+ process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
2427
2670
 
2428
2671
  `);
2429
- process.exit(v.valid ? 0 : 1);
2672
+ process.exit(finalValid ? 0 : 1);
2430
2673
  }
2431
2674
  async function handleBundle(argv) {
2432
2675
  const { readFileSync, writeFileSync, existsSync } = await import("fs");
@@ -3933,6 +4176,10 @@ async function main() {
3933
4176
  await handleVerifyClaim(args.slice(1));
3934
4177
  return;
3935
4178
  }
4179
+ if (args[0] === "anchor-record") {
4180
+ await handleAnchorRecord(args.slice(1));
4181
+ return;
4182
+ }
3936
4183
  if (args[0] === "init-hooks") {
3937
4184
  await handleInitHooks(args.slice(1));
3938
4185
  process.exit(0);
@@ -4121,7 +4368,7 @@ async function main() {
4121
4368
  if (useHttp) {
4122
4369
  const portIdx = args.indexOf("--port");
4123
4370
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
4124
- const { startHttpTransport } = await import("./http-transport-D7C64PIA.mjs");
4371
+ const { startHttpTransport } = await import("./http-transport-HLSMVBI6.mjs");
4125
4372
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
4126
4373
  return;
4127
4374
  }
@@ -307,7 +307,13 @@ function signDecision(entry) {
307
307
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
308
308
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
309
309
  // - uncertified = unsigned receipt (shadow mode, no signing configured)
310
- issuer_certification: signerState ? "self-signed" : "uncertified"
310
+ issuer_certification: signerState ? "self-signed" : "uncertified",
311
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
312
+ // self-contained: any verifier (including the record viewer, in-browser)
313
+ // can check the signature without a side channel. Binding the key inside
314
+ // the signature means it cannot be swapped without breaking the signature;
315
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
316
+ public_key: signerState.publicKey
311
317
  };
312
318
  if (entry.tier) payload.tier = entry.tier;
313
319
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -1039,7 +1045,7 @@ var sha256 = /* @__PURE__ */ createHasher(() => new SHA256());
1039
1045
  var sha2562 = sha256;
1040
1046
 
1041
1047
  // src/receipt-enrichment.ts
1042
- var ENRICHMENT_VERSION = 1;
1048
+ var ENRICHMENT_VERSION = 2;
1043
1049
  function canonicalJson(value) {
1044
1050
  const seen = /* @__PURE__ */ new WeakSet();
1045
1051
  const enc = (v) => {
@@ -1076,7 +1082,11 @@ var RULES = [
1076
1082
  { 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/ },
1077
1083
  { 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/ },
1078
1084
  { cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
1079
- { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
1085
+ { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
1086
+ // Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
1087
+ // only makes a `claim --no payment` harder to assert (conservative); a false
1088
+ // negative would let a real payment escape the record's payment claims.
1089
+ { cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
1080
1090
  ];
1081
1091
  function deriveCapabilities(tool, input) {
1082
1092
  const t = String(tool || "").toLowerCase();
@@ -1110,6 +1120,33 @@ function deriveResource(input) {
1110
1120
  }
1111
1121
  return void 0;
1112
1122
  }
1123
+ function findField(input, names, depth = 0) {
1124
+ if (depth > 4 || input === null || typeof input !== "object") return void 0;
1125
+ const o = input;
1126
+ const keys = Object.keys(o).sort();
1127
+ for (const k of keys) {
1128
+ if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
1129
+ }
1130
+ for (const k of keys) {
1131
+ const v = findField(o[k], names, depth + 1);
1132
+ if (v !== void 0) return v;
1133
+ }
1134
+ return void 0;
1135
+ }
1136
+ function derivePayment(tool, input) {
1137
+ if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
1138
+ const p = { amount: null, asset: null, recipient_digest: null };
1139
+ const amt = findField(input, ["amount"]);
1140
+ if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
1141
+ else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
1142
+ const asset = findField(input, ["asset", "currency", "token"]);
1143
+ if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
1144
+ const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
1145
+ if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex(to.trim().toLowerCase());
1146
+ const scheme = findField(input, ["scheme"]);
1147
+ if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
1148
+ return p;
1149
+ }
1113
1150
  function buildEnrichment(tool, input) {
1114
1151
  const e = {
1115
1152
  v: ENRICHMENT_VERSION,
@@ -1118,6 +1155,8 @@ function buildEnrichment(tool, input) {
1118
1155
  };
1119
1156
  const resource = deriveResource(input);
1120
1157
  if (resource) e.resource = resource;
1158
+ const payment = derivePayment(tool, input);
1159
+ if (payment) e.payment = payment;
1121
1160
  return e;
1122
1161
  }
1123
1162
 
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-744JMCY4.mjs";
4
- import "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-H332ZNJ6.mjs";
4
+ import "./chunk-WWPQNIVF.mjs";
5
5
  import "./chunk-AYNQIEN7.mjs";
6
- import "./chunk-IDUH2O4Q.mjs";
6
+ import "./chunk-XLJUZ4WO.mjs";
7
7
  import "./chunk-JIDDQUSQ.mjs";
8
8
  import "./chunk-D733KAPG.mjs";
9
9
  import "./chunk-PQJP2ZCI.mjs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ProtectGateway
3
- } from "./chunk-GHR65WVD.mjs";
4
- import "./chunk-IDUH2O4Q.mjs";
3
+ } from "./chunk-PB3TC7E3.mjs";
4
+ import "./chunk-XLJUZ4WO.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
 
7
7
  // src/http-transport.ts
package/dist/index.d.mts CHANGED
@@ -3,6 +3,16 @@ export { BUILTIN_PATTERNS, HookPattern, generateHookSettings, generateSampleCeda
3
3
  export { createSandboxServer } from './demo-server.mjs';
4
4
  import 'node:http';
5
5
 
6
+ interface PaymentInfo {
7
+ /** Normalized human amount in `asset` units, when derivable; else null. */
8
+ amount: number | null;
9
+ /** Asset symbol (e.g. 'USDC') or contract address, when derivable; else null. */
10
+ asset: string | null;
11
+ /** SHA-256 (hex) of the lowercased recipient: position-blind, when present. */
12
+ recipient_digest: string | null;
13
+ /** x402 scheme ('exact' | 'upto' | ...) when present. */
14
+ scheme?: string;
15
+ }
6
16
  interface ReceiptEnrichment {
7
17
  /** Rule/schema version, so derivations stay reproducible as rules evolve. */
8
18
  v: number;
@@ -15,6 +25,8 @@ interface ReceiptEnrichment {
15
25
  kind: 'path' | 'host' | 'command';
16
26
  digest: string;
17
27
  };
28
+ /** Minimum-disclosure facts about a value transfer (x402 / agent payment). */
29
+ payment?: PaymentInfo;
18
30
  }
19
31
 
20
32
  interface ProtectPolicy {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,16 @@ export { BUILTIN_PATTERNS, HookPattern, generateHookSettings, generateSampleCeda
3
3
  export { createSandboxServer } from './demo-server.js';
4
4
  import 'node:http';
5
5
 
6
+ interface PaymentInfo {
7
+ /** Normalized human amount in `asset` units, when derivable; else null. */
8
+ amount: number | null;
9
+ /** Asset symbol (e.g. 'USDC') or contract address, when derivable; else null. */
10
+ asset: string | null;
11
+ /** SHA-256 (hex) of the lowercased recipient: position-blind, when present. */
12
+ recipient_digest: string | null;
13
+ /** x402 scheme ('exact' | 'upto' | ...) when present. */
14
+ scheme?: string;
15
+ }
6
16
  interface ReceiptEnrichment {
7
17
  /** Rule/schema version, so derivations stay reproducible as rules evolve. */
8
18
  v: number;
@@ -15,6 +25,8 @@ interface ReceiptEnrichment {
15
25
  kind: 'path' | 'host' | 'command';
16
26
  digest: string;
17
27
  };
28
+ /** Minimum-disclosure facts about a value transfer (x402 / agent payment). */
29
+ payment?: PaymentInfo;
18
30
  }
19
31
 
20
32
  interface ProtectPolicy {