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.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;
@@ -5140,6 +5146,33 @@ function deriveResource(input) {
5140
5146
  }
5141
5147
  return void 0;
5142
5148
  }
5149
+ function findField(input, names, depth = 0) {
5150
+ if (depth > 4 || input === null || typeof input !== "object") return void 0;
5151
+ const o = input;
5152
+ const keys = Object.keys(o).sort();
5153
+ for (const k of keys) {
5154
+ if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
5155
+ }
5156
+ for (const k of keys) {
5157
+ const v = findField(o[k], names, depth + 1);
5158
+ if (v !== void 0) return v;
5159
+ }
5160
+ return void 0;
5161
+ }
5162
+ function derivePayment(tool, input) {
5163
+ if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
5164
+ const p = { amount: null, asset: null, recipient_digest: null };
5165
+ const amt = findField(input, ["amount"]);
5166
+ if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
5167
+ else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
5168
+ const asset = findField(input, ["asset", "currency", "token"]);
5169
+ if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
5170
+ const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
5171
+ if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex2(to.trim().toLowerCase());
5172
+ const scheme = findField(input, ["scheme"]);
5173
+ if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
5174
+ return p;
5175
+ }
5143
5176
  function buildEnrichment(tool, input) {
5144
5177
  const e = {
5145
5178
  v: ENRICHMENT_VERSION,
@@ -5148,6 +5181,8 @@ function buildEnrichment(tool, input) {
5148
5181
  };
5149
5182
  const resource = deriveResource(input);
5150
5183
  if (resource) e.resource = resource;
5184
+ const payment = derivePayment(tool, input);
5185
+ if (payment) e.payment = payment;
5151
5186
  return e;
5152
5187
  }
5153
5188
  var ENRICHMENT_VERSION, RULES;
@@ -5156,7 +5191,7 @@ var init_receipt_enrichment = __esm({
5156
5191
  "use strict";
5157
5192
  init_sha256();
5158
5193
  init_utils();
5159
- ENRICHMENT_VERSION = 1;
5194
+ ENRICHMENT_VERSION = 2;
5160
5195
  RULES = [
5161
5196
  { cap: "exec.shell", tool: /bash|shell|exec|terminal|run_command|command/ },
5162
5197
  { cap: "fs.read", tool: /(^|[_.])(read|cat|glob|grep|search|ls|view|list_files|open)/ },
@@ -5168,7 +5203,11 @@ var init_receipt_enrichment = __esm({
5168
5203
  { 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
5204
  { 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
5205
  { 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/ }
5206
+ { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
5207
+ // Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
5208
+ // only makes a `claim --no payment` harder to assert (conservative); a false
5209
+ // negative would let a real payment escape the record's payment claims.
5210
+ { 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/ }
5172
5211
  ];
5173
5212
  }
5174
5213
  });
@@ -5177,16 +5216,22 @@ var init_receipt_enrichment = __esm({
5177
5216
  var claim_exports = {};
5178
5217
  __export(claim_exports, {
5179
5218
  ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
5219
+ CHECKPOINT_SCHEMA: () => CHECKPOINT_SCHEMA,
5180
5220
  CLAIM_TYPE: () => CLAIM_TYPE,
5181
5221
  DEFAULT_LOG: () => DEFAULT_LOG,
5182
5222
  anchorClaim: () => anchorClaim,
5223
+ anchorRecordCheckpoint: () => anchorRecordCheckpoint,
5183
5224
  buildAnchorEnvelope: () => buildAnchorEnvelope,
5184
5225
  buildClaim: () => buildClaim,
5226
+ buildRecordCheckpoint: () => buildRecordCheckpoint,
5227
+ checkClaimAnchor: () => checkClaimAnchor,
5185
5228
  claimDigest: () => claimDigest,
5186
5229
  evaluate: () => evaluate,
5187
5230
  leafHash: () => leafHash,
5231
+ lookupPinnedIdentity: () => lookupPinnedIdentity,
5188
5232
  merkleRoot: () => merkleRoot,
5189
5233
  receiptToLeaf: () => receiptToLeaf,
5234
+ verifyAnchorEnvelope: () => verifyAnchorEnvelope,
5190
5235
  verifyClaim: () => verifyClaim
5191
5236
  });
5192
5237
  function sha256Hex3(input) {
@@ -5203,7 +5248,13 @@ function receiptToLeaf(e) {
5203
5248
  const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
5204
5249
  const t = isFinite(ms) ? new Date(ms).toISOString() : "";
5205
5250
  const d = sha256Hex3(canonicalJson(e));
5206
- return { d, v, c, t };
5251
+ const leaf = { d, v, c, t };
5252
+ const pay = enr && enr.payment;
5253
+ if (pay && typeof pay === "object") {
5254
+ const amt = pay.amount;
5255
+ leaf.p = typeof amt === "number" && Number.isFinite(amt) ? amt : null;
5256
+ }
5257
+ return leaf;
5207
5258
  }
5208
5259
  function leafHash(leaf) {
5209
5260
  return sha256Hex3(canonicalJson(leaf));
@@ -5236,6 +5287,10 @@ function evaluate(pred, leaves) {
5236
5287
  const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
5237
5288
  return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
5238
5289
  }
5290
+ if (pred.kind === "payment_under") {
5291
+ const matched2 = leaves.filter((l) => "p" in l && (l.p === null || l.p >= pred.cap)).length;
5292
+ return { statement: `Every payment stayed under ${pred.cap} (unknown amounts count as over)`, holds: matched2 === 0, matched: matched2 };
5293
+ }
5239
5294
  const matched = leaves.filter((l) => l.v === pred.verdict).length;
5240
5295
  return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
5241
5296
  }
@@ -5327,11 +5382,82 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
5327
5382
  const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5328
5383
  return { ...signed, signature, digest };
5329
5384
  }
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 };
5385
+ function verifyAnchorEnvelope(pack, envelope) {
5386
+ const reasons = [];
5387
+ if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
5388
+ return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
5389
+ }
5390
+ const expected = claimDigest(pack);
5391
+ if (envelope.claim_digest !== expected) {
5392
+ reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
5393
+ }
5394
+ if (envelope.record_root !== pack.record.root) {
5395
+ reasons.push("anchored envelope commits to a different record root");
5396
+ }
5397
+ if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
5398
+ reasons.push("anchor was signed by a different key than the claim issuer");
5399
+ }
5400
+ try {
5401
+ const { signature, digest, ...signed } = envelope;
5402
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5403
+ if (bytesToHex(hash) !== String(digest).toLowerCase()) {
5404
+ reasons.push("envelope digest does not match its contents");
5405
+ } else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
5406
+ reasons.push("envelope signature does not verify");
5407
+ }
5408
+ } catch {
5409
+ reasons.push("envelope signature does not verify");
5410
+ }
5411
+ return { ok: reasons.length === 0, reasons };
5412
+ }
5413
+ async function checkClaimAnchor(pack, sidecar, opts) {
5414
+ const reasons = [];
5415
+ const envelope = sidecar && sidecar.envelope;
5416
+ if (!envelope) {
5417
+ return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
5418
+ }
5419
+ const local = verifyAnchorEnvelope(pack, envelope);
5420
+ reasons.push(...local.reasons);
5421
+ const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
5422
+ const out = {
5423
+ local_ok: local.ok,
5424
+ log_ok: null,
5425
+ seq: sidecar.seq,
5426
+ anchored_at: sidecar.anchored_at,
5427
+ entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
5428
+ reasons
5429
+ };
5430
+ if (opts?.offline) return out;
5431
+ const doFetch = opts?.fetchImpl || globalThis.fetch;
5432
+ if (!doFetch) return out;
5433
+ try {
5434
+ const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
5435
+ const data = await resp.json().catch(() => null);
5436
+ if (!resp.ok || !data) {
5437
+ out.log_ok = null;
5438
+ return out;
5439
+ }
5440
+ if (data.anchored !== true) {
5441
+ out.log_ok = false;
5442
+ out.reasons.push("the public log does not contain this anchor digest");
5443
+ return out;
5444
+ }
5445
+ if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
5446
+ out.log_ok = false;
5447
+ out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
5448
+ return out;
5449
+ }
5450
+ out.log_ok = true;
5451
+ if (typeof data.seq === "number") out.seq = data.seq;
5452
+ return out;
5453
+ } catch {
5454
+ out.log_ok = null;
5455
+ return out;
5456
+ }
5457
+ }
5458
+ async function submitEnvelope(envelope, base, fetchImpl) {
5459
+ const doFetch = fetchImpl || globalThis.fetch;
5460
+ if (!doFetch) return { ok: false, error: "fetch_unavailable" };
5335
5461
  const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
5336
5462
  try {
5337
5463
  const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
@@ -5341,22 +5467,86 @@ async function anchorClaim(pack, key, opts) {
5341
5467
  });
5342
5468
  const data = await resp.json().catch(() => null);
5343
5469
  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 };
5470
+ return { ok: false, error: data && data.error || `http_${resp.status}` };
5345
5471
  }
5472
+ return { ok: true, seq: data.seq, anchored_at: data.anchored_at, already_anchored: !!data.already_anchored };
5473
+ } catch {
5474
+ return { ok: false, error: "network_error" };
5475
+ }
5476
+ }
5477
+ async function anchorClaim(pack, key, opts) {
5478
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
5479
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5480
+ const out = await submitEnvelope(envelope, base, opts.fetchImpl);
5481
+ if (!out.ok) return { ok: false, claim_digest: envelope.claim_digest, error: out.error, envelope };
5482
+ return {
5483
+ ok: true,
5484
+ claim_digest: envelope.claim_digest,
5485
+ seq: out.seq,
5486
+ entry_url: `${base}/fn/log/${out.seq}`,
5487
+ anchored_at: out.anchored_at,
5488
+ already_anchored: out.already_anchored,
5489
+ envelope
5490
+ };
5491
+ }
5492
+ function buildRecordCheckpoint(receipts, key, issuedAt) {
5493
+ const leaves = receipts.map(receiptToLeaf);
5494
+ const times = leaves.map((l) => l.t).filter(Boolean).sort();
5495
+ const signed = {
5496
+ type: "evidence_pack",
5497
+ schema: CHECKPOINT_SCHEMA,
5498
+ anchors: "protect-mcp-record",
5499
+ record_root: merkleRoot(leaves.map(leafHash)),
5500
+ total: leaves.length,
5501
+ from: times[0] || "",
5502
+ to: times[times.length - 1] || "",
5503
+ issued_at: issuedAt,
5504
+ verification_key: key.publicKey,
5505
+ disclosure: "internal"
5506
+ };
5507
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5508
+ const digest = bytesToHex(hash);
5509
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5510
+ return { ...signed, signature, digest };
5511
+ }
5512
+ async function anchorRecordCheckpoint(receipts, key, opts) {
5513
+ const checkpoint = buildRecordCheckpoint(receipts, key, opts.issuedAt);
5514
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5515
+ const out = await submitEnvelope(checkpoint, base, opts.fetchImpl);
5516
+ if (!out.ok) return { ok: false, record_root: checkpoint.record_root, total: checkpoint.total, checkpoint, error: out.error };
5517
+ return {
5518
+ ok: true,
5519
+ record_root: checkpoint.record_root,
5520
+ total: checkpoint.total,
5521
+ seq: out.seq,
5522
+ entry_url: `${base}/fn/log/${out.seq}`,
5523
+ anchored_at: out.anchored_at,
5524
+ already_anchored: out.already_anchored,
5525
+ checkpoint
5526
+ };
5527
+ }
5528
+ async function lookupPinnedIdentity(publicKey, opts) {
5529
+ const base = (opts && opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5530
+ const doFetch = opts && opts.fetchImpl || globalThis.fetch;
5531
+ if (!doFetch || !/^[0-9a-f]{64}$/i.test(publicKey)) return null;
5532
+ try {
5533
+ const resp = await doFetch(`${base}/fn/log/keys/lookup/${publicKey.toLowerCase()}`, { headers: { accept: "application/json" } });
5534
+ const data = await resp.json().catch(() => null);
5535
+ if (!data || data.ok !== true) return null;
5536
+ if (!data.found) return { found: false };
5346
5537
  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
5538
+ found: true,
5539
+ name: data.name,
5540
+ slug: data.slug,
5541
+ kid: data.kid,
5542
+ enrolled_at: data.enrolled_at,
5543
+ revoked: !!(data.revoked || data.revoked_at)
5354
5544
  };
5355
5545
  } catch {
5356
- return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
5546
+ return null;
5357
5547
  }
5358
5548
  }
5359
- var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
5549
+ var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG, CHECKPOINT_SCHEMA;
5360
5550
  var init_claim = __esm({
5361
5551
  "src/claim.ts"() {
5362
5552
  "use strict";
@@ -5367,6 +5557,7 @@ var init_claim = __esm({
5367
5557
  CLAIM_TYPE = "scopeblind.claim.v1";
5368
5558
  ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
5369
5559
  DEFAULT_LOG = "https://scopeblind.com";
5560
+ CHECKPOINT_SCHEMA = "scopeblind.protect-mcp.record-checkpoint.v1";
5370
5561
  }
5371
5562
  });
5372
5563
 
@@ -8630,8 +8821,12 @@ Commands:
8630
8821
  digest Generate a human-readable summary of agent activity
8631
8822
  receipts Show recent persisted signed receipts
8632
8823
  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)
8824
+ claim Attest a signed, position-blind claim over your record (e.g. no egress,
8825
+ no payment, every payment under a cap)
8826
+ verify-claim Verify a claim attestation offline (signature + predicate + commitment
8827
+ + the anchor sidecar and issuer identity when present)
8828
+ anchor-record Checkpoint the record's Merkle root + count into the public log
8829
+ (heartbeat-friendly: skips when unchanged; only hashes leave)
8635
8830
  bundle Export an offline-verifiable audit bundle
8636
8831
 
8637
8832
  Examples:
@@ -10563,6 +10758,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10563
10758
  return null;
10564
10759
  }
10565
10760
  }).filter((x) => x !== null).map(mapRecordEntry);
10761
+ let pinnedKey = "";
10762
+ let pinnedKid = "";
10763
+ try {
10764
+ const kd = JSON.parse(readFileSync11(join8(dir, "keys", "gateway.json"), "utf-8"));
10765
+ if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
10766
+ pinnedKey = kd.publicKey;
10767
+ pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
10768
+ }
10769
+ } catch {
10770
+ }
10566
10771
  const openTarget = (target) => {
10567
10772
  if (argv.includes("--no-open")) return;
10568
10773
  const platform = process.platform;
@@ -10590,7 +10795,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10590
10795
  res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
10591
10796
  return;
10592
10797
  }
10593
- const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
10798
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
10594
10799
  const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
10595
10800
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
10596
10801
  res.end(page);
@@ -10615,7 +10820,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
10615
10820
  return;
10616
10821
  }
10617
10822
  const recs = readRecs(chosen);
10618
- const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
10823
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
10619
10824
  const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10620
10825
  const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10621
10826
  writeFileSync4(out, html);
@@ -10681,6 +10886,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10681
10886
  .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
10682
10887
  .badge.sgn{background:var(--gb);color:var(--g)}
10683
10888
  .badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
10889
+ .badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
10890
+ .badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
10891
+ .stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
10684
10892
  .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10685
10893
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10686
10894
  .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}
@@ -10727,6 +10935,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10727
10935
  </div>
10728
10936
  <script>
10729
10937
  var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
10938
+ // In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
10939
+ // preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
10940
+ // signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
10941
+ // public half, injected by the CLI) = authenticity; key embedded in the receipt
10942
+ // payload (0.9.3+) = self-consistency. Everything runs locally.
10943
+ var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
10944
+ function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
10945
+ 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}
10946
+ 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})}
10947
+ 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)}
10948
+ async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
10949
+ var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
10950
+ var msg=new TextEncoder().encode(canon(rest));
10951
+ var pin=String(META.pinned_key||"").toLowerCase();
10952
+ var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
10953
+ if(!/^[0-9a-f]{64}$/.test(emb))emb="";
10954
+ 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"}
10955
+ if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
10956
+ return"nokey"}
10957
+ 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}
10958
+ async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
10959
+ 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;
10960
+ try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
10961
+ finally{VDONE=true;VBUSY=false;render()}}
10730
10962
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
10731
10963
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
10732
10964
  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"})}
@@ -10740,8 +10972,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
10740
10972
  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
10973
  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
10974
  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>';}
10975
+ 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>'}
10976
+ p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
10977
+ 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>';}
10745
10978
  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
10979
  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
10980
  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;}
@@ -10759,8 +10992,8 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
10759
10992
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
10760
10993
  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()});
10761
10994
  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");}});
10762
- render();
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);}
10995
+ render();kickVerify();
10996
+ 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);}
10764
10997
  </script></body></html>`;
10765
10998
  async function handleClaim(argv) {
10766
10999
  const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
@@ -10770,20 +11003,23 @@ async function handleClaim(argv) {
10770
11003
  const di = argv.indexOf("--dir");
10771
11004
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
10772
11005
  let predicate = null;
10773
- const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count");
11006
+ const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count"), puIdx = argv.indexOf("--payment-under");
10774
11007
  if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
10775
11008
  else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
10776
11009
  else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
10777
11010
  else if (cvIdx !== -1 && argv[cvIdx + 1]) predicate = { kind: "count_verdict", verdict: argv[cvIdx + 1] };
11011
+ else if (puIdx !== -1 && argv[puIdx + 1] && isFinite(parseFloat(argv[puIdx + 1]))) predicate = { kind: "payment_under", cap: parseFloat(argv[puIdx + 1]) };
10778
11012
  if (!predicate) {
10779
11013
  process.stderr.write(`
10780
11014
  ${bold("protect-mcp claim")}
10781
11015
 
10782
11016
  Attest a signed, position-blind claim over your record:
10783
- --no <capability> no action used it, e.g. ${dim("--no net.egress")}
11017
+ --no <capability> no action used it, e.g. ${dim("--no net.egress")} or ${dim("--no payment")}
10784
11018
  --only <c1,c2,...> all actions confined to these capabilities
10785
11019
  --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
10786
11020
  --count <verdict> how many, e.g. ${dim("--count blocked")}
11021
+ --payment-under <cap> every agent payment stayed under the cap (amounts the
11022
+ gate could not read count as OVER, so this cannot lie)
10787
11023
  --anchor also record the claim digest in the public append-only
10788
11024
  log so a counterparty can trust it is complete (only the
10789
11025
  hash is sent; your record stays local)
@@ -10886,8 +11122,18 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
10886
11122
  `);
10887
11123
  process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
10888
11124
  `);
10889
- process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
11125
+ const { lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
11126
+ const who = await lookupPinnedIdentity2(key.publicKey, { log: logBase });
11127
+ if (who && who.found && !who.revoked) {
11128
+ 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) : "") + ")")}
10890
11129
  `);
11130
+ } else if (who && who.found && who.revoked) {
11131
+ process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
11132
+ `);
11133
+ } else {
11134
+ process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). To anchor as a named org a counterparty can pin, see")} ${bold("scopeblind.com/enroll")}
11135
+ `);
11136
+ }
10891
11137
  } else {
10892
11138
  process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
10893
11139
  `);
@@ -10897,6 +11143,132 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
10897
11143
  `);
10898
11144
  process.exit(0);
10899
11145
  }
11146
+ async function handleAnchorRecord(argv) {
11147
+ const { readFileSync: readFileSync11, existsSync: existsSync9, appendFileSync: appendFileSync3 } = await import("fs");
11148
+ const { join: join8 } = await import("path");
11149
+ const { anchorRecordCheckpoint: anchorRecordCheckpoint2, buildRecordCheckpoint: buildRecordCheckpoint2, lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
11150
+ let dir = process.cwd();
11151
+ const di = argv.indexOf("--dir");
11152
+ if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
11153
+ const li = argv.indexOf("--log");
11154
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
11155
+ const keyPath = join8(dir, "keys", "gateway.json");
11156
+ if (!existsSync9(keyPath)) {
11157
+ process.stderr.write(`
11158
+ ${bold("protect-mcp anchor-record")}
11159
+
11160
+ No signing key at ${keyPath}. A checkpoint must be signed. Run ${bold("npx protect-mcp init")} first.
11161
+
11162
+ `);
11163
+ process.exit(1);
11164
+ return;
11165
+ }
11166
+ let key;
11167
+ try {
11168
+ key = JSON.parse(readFileSync11(keyPath, "utf-8"));
11169
+ } catch {
11170
+ process.stderr.write(`
11171
+ protect-mcp anchor-record: ${keyPath} is not valid JSON.
11172
+
11173
+ `);
11174
+ process.exit(1);
11175
+ return;
11176
+ }
11177
+ if (!key.privateKey || !key.publicKey) {
11178
+ process.stderr.write(`
11179
+ protect-mcp anchor-record: ${keyPath} is missing privateKey/publicKey.
11180
+
11181
+ `);
11182
+ process.exit(1);
11183
+ return;
11184
+ }
11185
+ const recPath = join8(dir, ".protect-mcp-receipts.jsonl");
11186
+ if (!existsSync9(recPath)) {
11187
+ process.stderr.write(`
11188
+ ${bold("protect-mcp anchor-record")}
11189
+
11190
+ No signed receipts in ${dir}. Run the gate with signing on, then try again.
11191
+
11192
+ `);
11193
+ process.exit(0);
11194
+ return;
11195
+ }
11196
+ const receipts = readFileSync11(recPath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
11197
+ try {
11198
+ return JSON.parse(l);
11199
+ } catch {
11200
+ return null;
11201
+ }
11202
+ }).filter((x) => x !== null);
11203
+ if (!receipts.length) {
11204
+ process.stderr.write(`
11205
+ protect-mcp anchor-record: no readable receipts in ${recPath}.
11206
+
11207
+ `);
11208
+ process.exit(0);
11209
+ return;
11210
+ }
11211
+ const claimKey = { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" };
11212
+ const historyPath = join8(dir, ".protect-mcp-anchors.jsonl");
11213
+ const preview = buildRecordCheckpoint2(receipts, claimKey, "preview");
11214
+ if (!argv.includes("--force") && existsSync9(historyPath)) {
11215
+ const lines = readFileSync11(historyPath, "utf-8").split(/\r?\n/).filter(Boolean);
11216
+ const last = lines.length ? (() => {
11217
+ try {
11218
+ return JSON.parse(lines[lines.length - 1]);
11219
+ } catch {
11220
+ return null;
11221
+ }
11222
+ })() : null;
11223
+ if (last && last.record_root === preview.record_root && last.total === preview.total) {
11224
+ process.stdout.write(`
11225
+ ${bold("\u{1F6E1}\uFE0F Record checkpoint")}
11226
+ `);
11227
+ process.stdout.write(` Unchanged since entry ${bold("#" + last.seq)} ${dim("(" + last.total + " receipts, anchored " + (last.anchored_at || "") + ")")}. Nothing new to anchor.
11228
+ `);
11229
+ process.stdout.write(` ${dim("Use --force to re-anchor anyway.")}
11230
+
11231
+ `);
11232
+ process.exit(0);
11233
+ return;
11234
+ }
11235
+ }
11236
+ const res = await anchorRecordCheckpoint2(receipts, claimKey, { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() });
11237
+ process.stdout.write(`
11238
+ ${bold("\u{1F6E1}\uFE0F Record checkpoint")}
11239
+ `);
11240
+ 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) + ")")}
11241
+ `);
11242
+ if (!res.ok) {
11243
+ process.stdout.write(` ${yellow("Anchor failed")} ${dim("(" + (res.error || "unavailable") + "). Nothing was recorded; try again.")}
11244
+
11245
+ `);
11246
+ process.exit(1);
11247
+ return;
11248
+ }
11249
+ appendFileSync3(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");
11250
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)} ${dim(res.entry_url || "")}
11251
+ `);
11252
+ process.stdout.write(` ${dim("Only the root, count, and time range were sent. History: " + historyPath)}
11253
+ `);
11254
+ const who = await lookupPinnedIdentity2(claimKey.publicKey, { log: logBase });
11255
+ if (who && who.found && !who.revoked) {
11256
+ process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory)")}
11257
+ `);
11258
+ } else if (who && who.found && who.revoked) {
11259
+ process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
11260
+ `);
11261
+ } else {
11262
+ process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). Named identity: scopeblind.com/enroll")}
11263
+ `);
11264
+ }
11265
+ process.stdout.write(` ${dim("A claim whose commitment matches this root is provably over the complete record as of")}
11266
+ `);
11267
+ process.stdout.write(` ${dim("this checkpoint. Run this on a heartbeat (e.g. cron) to keep the anchored history growing.")}
11268
+
11269
+ `);
11270
+ process.exit(0);
11271
+ }
10900
11272
  async function handleVerifyClaim(argv) {
10901
11273
  const { readFileSync: readFileSync11, existsSync: existsSync9 } = await import("fs");
10902
11274
  const { verifyClaim: verifyClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
@@ -10946,17 +11318,79 @@ ${bold("protect-mcp verify-claim")}
10946
11318
  `);
10947
11319
  process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
10948
11320
  `);
11321
+ const ai = argv.indexOf("--anchor-file");
11322
+ const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
11323
+ const requireAnchor = argv.includes("--check-anchor");
11324
+ let anchorOk = true;
11325
+ if (existsSync9(sidecarPath)) {
11326
+ const { checkClaimAnchor: checkClaimAnchor2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
11327
+ let sidecar = null;
11328
+ try {
11329
+ sidecar = JSON.parse(readFileSync11(sidecarPath, "utf-8"));
11330
+ } catch {
11331
+ }
11332
+ if (!sidecar) {
11333
+ anchorOk = false;
11334
+ process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
11335
+ `);
11336
+ } else {
11337
+ const a = await checkClaimAnchor2(pack, sidecar, { offline: argv.includes("--offline") });
11338
+ anchorOk = a.local_ok && a.log_ok !== false;
11339
+ if (a.local_ok) {
11340
+ process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
11341
+ `);
11342
+ process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
11343
+ `);
11344
+ } else {
11345
+ for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
11346
+ `);
11347
+ }
11348
+ if (a.log_ok === true) {
11349
+ 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) : ""}
11350
+ `);
11351
+ } else if (a.log_ok === false) {
11352
+ process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
11353
+ `);
11354
+ } else if (a.local_ok) {
11355
+ process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
11356
+ `);
11357
+ }
11358
+ if (!argv.includes("--offline") && sidecar.envelope) {
11359
+ const { lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
11360
+ const who = await lookupPinnedIdentity2(sidecar.envelope.verification_key, {});
11361
+ if (who && who.found && !who.revoked) {
11362
+ process.stdout.write(` ${green("\u2713")} issuer key pinned to ${bold(who.name || who.slug || "an enrolled org")} ${dim("(ScopeBlind key directory)")}
11363
+ `);
11364
+ } else if (who && who.found && who.revoked) {
11365
+ anchorOk = false;
11366
+ process.stdout.write(` ${red("\u2717")} issuer key is REVOKED in the ScopeBlind key directory
11367
+ `);
11368
+ } else if (who && !who.found) {
11369
+ process.stdout.write(` ${dim("issuer key not enrolled (anonymous issuer); named identities pin via scopeblind.com/enroll")}
11370
+ `);
11371
+ }
11372
+ }
11373
+ }
11374
+ } else if (requireAnchor) {
11375
+ anchorOk = false;
11376
+ process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
11377
+ `);
11378
+ } else {
11379
+ process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
11380
+ `);
11381
+ }
11382
+ const finalValid = v.valid && anchorOk;
10949
11383
  process.stdout.write(`
10950
- ${v.valid ? green("VALID") : red("INVALID")} attestation.
11384
+ ${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
10951
11385
  `);
10952
11386
  process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
10953
11387
  `);
10954
11388
  process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
10955
11389
  `);
10956
- process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; pin the key or anchor to the log for more.")}
11390
+ process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
10957
11391
 
10958
11392
  `);
10959
- process.exit(v.valid ? 0 : 1);
11393
+ process.exit(finalValid ? 0 : 1);
10960
11394
  }
10961
11395
  async function handleBundle(argv) {
10962
11396
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
@@ -12463,6 +12897,10 @@ async function main() {
12463
12897
  await handleVerifyClaim(args.slice(1));
12464
12898
  return;
12465
12899
  }
12900
+ if (args[0] === "anchor-record") {
12901
+ await handleAnchorRecord(args.slice(1));
12902
+ return;
12903
+ }
12466
12904
  if (args[0] === "init-hooks") {
12467
12905
  await handleInitHooks(args.slice(1));
12468
12906
  process.exit(0);