protect-mcp 0.9.2 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3: the skeptic's tools sharpen
4
+
5
+ Two verification upgrades: the anchor check moves into the verifier, and the
6
+ record viewer proves signatures instead of just labelling them.
7
+
8
+ - `verify-claim` now verifies the anchor automatically: it finds the
9
+ `<claim>.anchor.json` sidecar, checks offline that the anchored envelope binds
10
+ this exact claim (digest, record root, and the same issuer key), and confirms
11
+ against the public log that the digest sits at the recorded entry. A failed
12
+ binding makes the attestation INVALID; an unreachable log does not (the local
13
+ checks stand alone). `--check-anchor` makes a missing anchor fatal,
14
+ `--anchor-file <p>` overrides the path, `--offline` skips the network hop.
15
+ - The `record` viewer now verifies Ed25519 signatures in the browser, locally:
16
+ each row shows `✓ verified`, `✗ invalid signature`, or `signed · unpinned
17
+ key`, and the stat strip counts how many verified. The CLI injects your
18
+ gateway's PUBLIC key so rows verify against your own key (the private key
19
+ never reaches the page).
20
+ - Receipts now embed the signer's public key inside the signed payload
21
+ (`payload.public_key`), so a receipt is self-contained: any verifier can check
22
+ it without a side channel, and the key cannot be swapped without breaking the
23
+ signature. Older receipts still verify against a pinned or pasted key.
24
+
3
25
  ## 0.9.2: anchor a claim to the public log
4
26
 
5
27
  Closes the one honest gap in a bare claim: that the disclosed set is complete.
package/README.md CHANGED
@@ -148,6 +148,43 @@ disclosure, not full zero-knowledge, but it makes the privacy claim concrete:
148
148
  auditors can verify selected facts without receiving the full tool payload or
149
149
  sensitive desk context.
150
150
 
151
+ ### Prove a claim over the record (position-blind attestations)
152
+
153
+ You can prove a CLAIM over your record without revealing it. Mint a signed,
154
+ position-blind attestation over the whole record that discloses only per-decision
155
+ categories (a receipt digest, the verdict, capability tags), never your tool
156
+ inputs, outputs, or data:
157
+
158
+ ```bash
159
+ # "No action reached the network across the record":
160
+ npx protect-mcp claim --no net.egress
161
+
162
+ # other predicates:
163
+ # --only fs.read,fs.write all actions were confined to these capabilities
164
+ # --no-verdict blocked no action was blocked
165
+ # --count blocked how many were blocked
166
+ ```
167
+
168
+ Anyone verifies it offline, seeing only the categories, never the content:
169
+
170
+ ```bash
171
+ npx protect-mcp verify-claim claim-<id>.json
172
+ ```
173
+
174
+ The verifier recomputes a Merkle root over the disclosed set and recomputes the
175
+ predicate independently, so the issuer cannot lie about the claim given the
176
+ disclosure. Add `--anchor` to record the claim's digest in the public,
177
+ append-only ScopeBlind transparency log, so a counterparty who does not trust you
178
+ can confirm the disclosed set is complete and was not quietly re-cut (only the
179
+ hash is sent; the record stays local):
180
+
181
+ ```bash
182
+ npx protect-mcp claim --no net.egress --anchor
183
+ ```
184
+
185
+ This is an accountable, position-blind attestation, not full zero-knowledge: it
186
+ reveals the shape, not the content.
187
+
151
188
  ## Claude Code hook quickstart
152
189
 
153
190
  ```bash
@@ -335,6 +372,9 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
335
372
  | `dashboard` | Start a local-only dashboard on `127.0.0.1` showing tool inventory, risk, policy coverage, exact-action approvals, receipt chains, and audit export. |
336
373
  | `recommend` | Draft a reviewable JSON policy from observed local calls. Dry-run by default; use `--write` to create `protect-mcp.recommended.json`. |
337
374
  | `registry` | Create an org identity, anchor receipt digests, and write a static verifier page. Hosted mode uploads digests only. |
375
+ | `record` | Open a local, searchable viewer over your receipts (`--live` streams as the agent runs): Ed25519 signatures verified in your browser against your gateway key, capability tags, a provenance tree, and one-click signed export. All local, nothing uploaded. |
376
+ | `claim` | Mint a signed, position-blind attestation of a predicate over the record (`--no <cap>`, `--only <c1,c2>`, `--no-verdict <verdict>`, `--count <verdict>`), disclosing only decision categories. Add `--anchor` to record the claim digest in the public transparency log. |
377
+ | `verify-claim` | Verify a claim pack offline: signature, recomputed Merkle root, independently recomputed predicate, and the anchor sidecar when present (binds the anchored envelope to this exact claim, then confirms the public log holds it). `--check-anchor` requires the anchor; `--offline` skips the log hop. |
338
378
  | `killer-demo` | Generate a complete shadow-mode to policy to approval to signed-receipt demo pack. |
339
379
  | `verify-disclosure` | Verify a `scopeblind.selective_disclosure.v0` package and explain disclosed versus hidden fields. |
340
380
  | `policy-packs` | List, inspect, and install starter Cedar policy packs. |
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-GHR65WVD.mjs";
3
+ } from "./chunk-PB3TC7E3.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-IDUH2O4Q.mjs";
8
+ } from "./chunk-XLJUZ4WO.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildEnrichment
3
- } from "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-HPKHMVZX.mjs";
4
4
  import {
5
5
  ReceiptBuffer,
6
6
  buildActionReadback,
@@ -15,7 +15,7 @@ import {
15
15
  loadPolicy,
16
16
  parseRateLimit,
17
17
  signDecision
18
- } from "./chunk-IDUH2O4Q.mjs";
18
+ } from "./chunk-XLJUZ4WO.mjs";
19
19
 
20
20
  // src/hook-server.ts
21
21
  import { createServer } from "http";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-D733KAPG.mjs";
7
7
 
8
8
  // src/receipt-enrichment.ts
9
- var ENRICHMENT_VERSION = 1;
9
+ var ENRICHMENT_VERSION = 2;
10
10
  function canonicalJson(value) {
11
11
  const seen = /* @__PURE__ */ new WeakSet();
12
12
  const enc = (v) => {
@@ -8,7 +8,7 @@ import {
8
8
  parseRateLimit,
9
9
  signDecision,
10
10
  startStatusServer
11
- } from "./chunk-IDUH2O4Q.mjs";
11
+ } from "./chunk-XLJUZ4WO.mjs";
12
12
 
13
13
  // src/evidence-store.ts
14
14
  import { readFileSync, writeFileSync, existsSync } from "fs";
@@ -177,7 +177,13 @@ function signDecision(entry) {
177
177
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
178
178
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
179
179
  // - uncertified = unsigned receipt (shadow mode, no signing configured)
180
- issuer_certification: signerState ? "self-signed" : "uncertified"
180
+ issuer_certification: signerState ? "self-signed" : "uncertified",
181
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
182
+ // self-contained: any verifier (including the record viewer, in-browser)
183
+ // can check the signature without a side channel. Binding the key inside
184
+ // the signature means it cannot be swapped without breaking the signature;
185
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
186
+ public_key: signerState.publicKey
181
187
  };
182
188
  if (entry.tier) payload.tier = entry.tier;
183
189
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalJson
3
- } from "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-HPKHMVZX.mjs";
4
4
  import {
5
5
  sha256
6
6
  } from "./chunk-AYNQIEN7.mjs";
@@ -156,6 +156,79 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
156
156
  const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
157
157
  return { ...signed, signature, digest };
158
158
  }
159
+ function verifyAnchorEnvelope(pack, envelope) {
160
+ const reasons = [];
161
+ if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
162
+ return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
163
+ }
164
+ const expected = claimDigest(pack);
165
+ if (envelope.claim_digest !== expected) {
166
+ reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
167
+ }
168
+ if (envelope.record_root !== pack.record.root) {
169
+ reasons.push("anchored envelope commits to a different record root");
170
+ }
171
+ if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
172
+ reasons.push("anchor was signed by a different key than the claim issuer");
173
+ }
174
+ try {
175
+ const { signature, digest, ...signed } = envelope;
176
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
177
+ if (bytesToHex(hash) !== String(digest).toLowerCase()) {
178
+ reasons.push("envelope digest does not match its contents");
179
+ } else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
180
+ reasons.push("envelope signature does not verify");
181
+ }
182
+ } catch {
183
+ reasons.push("envelope signature does not verify");
184
+ }
185
+ return { ok: reasons.length === 0, reasons };
186
+ }
187
+ async function checkClaimAnchor(pack, sidecar, opts) {
188
+ const reasons = [];
189
+ const envelope = sidecar && sidecar.envelope;
190
+ if (!envelope) {
191
+ return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
192
+ }
193
+ const local = verifyAnchorEnvelope(pack, envelope);
194
+ reasons.push(...local.reasons);
195
+ const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
196
+ const out = {
197
+ local_ok: local.ok,
198
+ log_ok: null,
199
+ seq: sidecar.seq,
200
+ anchored_at: sidecar.anchored_at,
201
+ entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
202
+ reasons
203
+ };
204
+ if (opts?.offline) return out;
205
+ const doFetch = opts?.fetchImpl || globalThis.fetch;
206
+ if (!doFetch) return out;
207
+ try {
208
+ const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
209
+ const data = await resp.json().catch(() => null);
210
+ if (!resp.ok || !data) {
211
+ out.log_ok = null;
212
+ return out;
213
+ }
214
+ if (data.anchored !== true) {
215
+ out.log_ok = false;
216
+ out.reasons.push("the public log does not contain this anchor digest");
217
+ return out;
218
+ }
219
+ if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
220
+ out.log_ok = false;
221
+ out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
222
+ return out;
223
+ }
224
+ out.log_ok = true;
225
+ if (typeof data.seq === "number") out.seq = data.seq;
226
+ return out;
227
+ } catch {
228
+ out.log_ok = null;
229
+ return out;
230
+ }
231
+ }
159
232
  async function anchorClaim(pack, key, opts) {
160
233
  const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
161
234
  const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
@@ -192,10 +265,12 @@ export {
192
265
  anchorClaim,
193
266
  buildAnchorEnvelope,
194
267
  buildClaim,
268
+ checkClaimAnchor,
195
269
  claimDigest,
196
270
  evaluate,
197
271
  leafHash,
198
272
  merkleRoot,
199
273
  receiptToLeaf,
274
+ verifyAnchorEnvelope,
200
275
  verifyClaim
201
276
  };
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)/ },
@@ -5182,11 +5188,13 @@ __export(claim_exports, {
5182
5188
  anchorClaim: () => anchorClaim,
5183
5189
  buildAnchorEnvelope: () => buildAnchorEnvelope,
5184
5190
  buildClaim: () => buildClaim,
5191
+ checkClaimAnchor: () => checkClaimAnchor,
5185
5192
  claimDigest: () => claimDigest,
5186
5193
  evaluate: () => evaluate,
5187
5194
  leafHash: () => leafHash,
5188
5195
  merkleRoot: () => merkleRoot,
5189
5196
  receiptToLeaf: () => receiptToLeaf,
5197
+ verifyAnchorEnvelope: () => verifyAnchorEnvelope,
5190
5198
  verifyClaim: () => verifyClaim
5191
5199
  });
5192
5200
  function sha256Hex3(input) {
@@ -5327,6 +5335,79 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
5327
5335
  const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5328
5336
  return { ...signed, signature, digest };
5329
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
+ }
5330
5411
  async function anchorClaim(pack, key, opts) {
5331
5412
  const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
5332
5413
  const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
@@ -10563,6 +10644,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10563
10644
  return null;
10564
10645
  }
10565
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
+ }
10566
10657
  const openTarget = (target) => {
10567
10658
  if (argv.includes("--no-open")) return;
10568
10659
  const platform = process.platform;
@@ -10590,7 +10681,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
10590
10681
  res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
10591
10682
  return;
10592
10683
  }
10593
- 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 };
10594
10685
  const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
10595
10686
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
10596
10687
  res.end(page);
@@ -10615,7 +10706,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
10615
10706
  return;
10616
10707
  }
10617
10708
  const recs = readRecs(chosen);
10618
- 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 };
10619
10710
  const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10620
10711
  const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10621
10712
  writeFileSync4(out, html);
@@ -10681,6 +10772,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10681
10772
  .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
10682
10773
  .badge.sgn{background:var(--gb);color:var(--g)}
10683
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}
10684
10778
  .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10685
10779
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10686
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}
@@ -10727,6 +10821,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
10727
10821
  </div>
10728
10822
  <script>
10729
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()}}
10730
10848
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
10731
10849
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
10732
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"})}
@@ -10740,8 +10858,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
10740
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")}
10741
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)}}
10742
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)}}
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>';}
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>';}
10745
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";}
10746
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];});}
10747
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;}
@@ -10759,8 +10878,8 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
10759
10878
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
10760
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()});
10761
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");}});
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);}
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);}
10764
10883
  </script></body></html>`;
10765
10884
  async function handleClaim(argv) {
10766
10885
  const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
@@ -10946,17 +11065,64 @@ ${bold("protect-mcp verify-claim")}
10946
11065
  `);
10947
11066
  process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
10948
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;
10949
11115
  process.stdout.write(`
10950
- ${v.valid ? green("VALID") : red("INVALID")} attestation.
11116
+ ${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
10951
11117
  `);
10952
11118
  process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
10953
11119
  `);
10954
11120
  process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
10955
11121
  `);
10956
- 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.")}
10957
11123
 
10958
11124
  `);
10959
- process.exit(v.valid ? 0 : 1);
11125
+ process.exit(finalValid ? 0 : 1);
10960
11126
  }
10961
11127
  async function handleBundle(argv) {
10962
11128
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
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
@@ -2033,6 +2033,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
2033
2033
  return null;
2034
2034
  }
2035
2035
  }).filter((x) => x !== null).map(mapRecordEntry);
2036
+ let pinnedKey = "";
2037
+ let pinnedKid = "";
2038
+ try {
2039
+ const kd = JSON.parse(readFileSync(join(dir, "keys", "gateway.json"), "utf-8"));
2040
+ if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
2041
+ pinnedKey = kd.publicKey;
2042
+ pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
2043
+ }
2044
+ } catch {
2045
+ }
2036
2046
  const openTarget = (target) => {
2037
2047
  if (argv.includes("--no-open")) return;
2038
2048
  const platform = process.platform;
@@ -2060,7 +2070,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
2060
2070
  res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
2061
2071
  return;
2062
2072
  }
2063
- const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
2073
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
2064
2074
  const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
2065
2075
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2066
2076
  res.end(page);
@@ -2085,7 +2095,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
2085
2095
  return;
2086
2096
  }
2087
2097
  const recs = readRecs(chosen);
2088
- const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
2098
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
2089
2099
  const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
2090
2100
  const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
2091
2101
  writeFileSync(out, html);
@@ -2151,6 +2161,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
2151
2161
  .badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
2152
2162
  .badge.sgn{background:var(--gb);color:var(--g)}
2153
2163
  .badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
2164
+ .badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
2165
+ .badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
2166
+ .stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
2154
2167
  .dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
2155
2168
  .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
2156
2169
  .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 +2210,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
2197
2210
  </div>
2198
2211
  <script>
2199
2212
  var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
2213
+ // In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
2214
+ // preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
2215
+ // signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
2216
+ // public half, injected by the CLI) = authenticity; key embedded in the receipt
2217
+ // payload (0.9.3+) = self-consistency. Everything runs locally.
2218
+ var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
2219
+ function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
2220
+ 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}
2221
+ 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})}
2222
+ 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)}
2223
+ async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
2224
+ var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
2225
+ var msg=new TextEncoder().encode(canon(rest));
2226
+ var pin=String(META.pinned_key||"").toLowerCase();
2227
+ var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
2228
+ if(!/^[0-9a-f]{64}$/.test(emb))emb="";
2229
+ 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"}
2230
+ if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
2231
+ return"nokey"}
2232
+ 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}
2233
+ async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
2234
+ 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;
2235
+ try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
2236
+ finally{VDONE=true;VBUSY=false;render()}}
2200
2237
  function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
2201
2238
  function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
2202
2239
  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 +2247,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
2210
2247
  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
2248
  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
2249
  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>';}
2250
+ 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>'}
2251
+ p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
2252
+ 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
2253
  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
2254
  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
2255
  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,13 +2267,13 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
2229
2267
  document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
2230
2268
  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
2269
  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);}
2270
+ render();kickVerify();
2271
+ 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
2272
  </script></body></html>`;
2235
2273
  async function handleClaim(argv) {
2236
2274
  const { readFileSync, existsSync, writeFileSync } = await import("fs");
2237
2275
  const { join } = await import("path");
2238
- const { buildClaim } = await import("./claim-TUDH2WPB.mjs");
2276
+ const { buildClaim } = await import("./claim-3EBRPP5U.mjs");
2239
2277
  let dir = process.cwd();
2240
2278
  const di = argv.indexOf("--dir");
2241
2279
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
@@ -2336,7 +2374,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2336
2374
  process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
2337
2375
  `);
2338
2376
  if (argv.indexOf("--anchor") !== -1) {
2339
- const { anchorClaim } = await import("./claim-TUDH2WPB.mjs");
2377
+ const { anchorClaim } = await import("./claim-3EBRPP5U.mjs");
2340
2378
  const li = argv.indexOf("--log");
2341
2379
  const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
2342
2380
  process.stdout.write(`
@@ -2369,7 +2407,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2369
2407
  }
2370
2408
  async function handleVerifyClaim(argv) {
2371
2409
  const { readFileSync, existsSync } = await import("fs");
2372
- const { verifyClaim } = await import("./claim-TUDH2WPB.mjs");
2410
+ const { verifyClaim } = await import("./claim-3EBRPP5U.mjs");
2373
2411
  const file = argv.find((a) => !a.startsWith("--"));
2374
2412
  if (!file || !existsSync(file)) {
2375
2413
  process.stderr.write(`
@@ -2416,17 +2454,64 @@ ${bold("protect-mcp verify-claim")}
2416
2454
  `);
2417
2455
  process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
2418
2456
  `);
2457
+ const ai = argv.indexOf("--anchor-file");
2458
+ const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
2459
+ const requireAnchor = argv.includes("--check-anchor");
2460
+ let anchorOk = true;
2461
+ if (existsSync(sidecarPath)) {
2462
+ const { checkClaimAnchor } = await import("./claim-3EBRPP5U.mjs");
2463
+ let sidecar = null;
2464
+ try {
2465
+ sidecar = JSON.parse(readFileSync(sidecarPath, "utf-8"));
2466
+ } catch {
2467
+ }
2468
+ if (!sidecar) {
2469
+ anchorOk = false;
2470
+ process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
2471
+ `);
2472
+ } else {
2473
+ const a = await checkClaimAnchor(pack, sidecar, { offline: argv.includes("--offline") });
2474
+ anchorOk = a.local_ok && a.log_ok !== false;
2475
+ if (a.local_ok) {
2476
+ process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
2477
+ `);
2478
+ process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
2479
+ `);
2480
+ } else {
2481
+ for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
2482
+ `);
2483
+ }
2484
+ if (a.log_ok === true) {
2485
+ 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) : ""}
2486
+ `);
2487
+ } else if (a.log_ok === false) {
2488
+ process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
2489
+ `);
2490
+ } else if (a.local_ok) {
2491
+ process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
2492
+ `);
2493
+ }
2494
+ }
2495
+ } else if (requireAnchor) {
2496
+ anchorOk = false;
2497
+ process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
2498
+ `);
2499
+ } else {
2500
+ process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
2501
+ `);
2502
+ }
2503
+ const finalValid = v.valid && anchorOk;
2419
2504
  process.stdout.write(`
2420
- ${v.valid ? green("VALID") : red("INVALID")} attestation.
2505
+ ${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
2421
2506
  `);
2422
2507
  process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
2423
2508
  `);
2424
2509
  process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
2425
2510
  `);
2426
- process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; pin the key or anchor to the log for more.")}
2511
+ process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
2427
2512
 
2428
2513
  `);
2429
- process.exit(v.valid ? 0 : 1);
2514
+ process.exit(finalValid ? 0 : 1);
2430
2515
  }
2431
2516
  async function handleBundle(argv) {
2432
2517
  const { readFileSync, writeFileSync, existsSync } = await import("fs");
@@ -4121,7 +4206,7 @@ async function main() {
4121
4206
  if (useHttp) {
4122
4207
  const portIdx = args.indexOf("--port");
4123
4208
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
4124
- const { startHttpTransport } = await import("./http-transport-D7C64PIA.mjs");
4209
+ const { startHttpTransport } = await import("./http-transport-HLSMVBI6.mjs");
4125
4210
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
4126
4211
  return;
4127
4212
  }
@@ -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) => {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-744JMCY4.mjs";
4
- import "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-DX7QOA3E.mjs";
4
+ import "./chunk-HPKHMVZX.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 {
package/dist/index.js CHANGED
@@ -35540,7 +35540,13 @@ function signDecision(entry) {
35540
35540
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
35541
35541
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
35542
35542
  // - uncertified = unsigned receipt (shadow mode, no signing configured)
35543
- issuer_certification: signerState ? "self-signed" : "uncertified"
35543
+ issuer_certification: signerState ? "self-signed" : "uncertified",
35544
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
35545
+ // self-contained: any verifier (including the record viewer, in-browser)
35546
+ // can check the signature without a side channel. Binding the key inside
35547
+ // the signature means it cannot be swapped without breaking the signature;
35548
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
35549
+ public_key: signerState.publicKey
35544
35550
  };
35545
35551
  if (entry.tier) payload.tier = entry.tier;
35546
35552
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -40362,7 +40368,7 @@ function forwardReceipt(signedReceipt) {
40362
40368
  }
40363
40369
 
40364
40370
  // src/receipt-enrichment.ts
40365
- var ENRICHMENT_VERSION = 1;
40371
+ var ENRICHMENT_VERSION = 2;
40366
40372
  function canonicalJson(value) {
40367
40373
  const seen = /* @__PURE__ */ new WeakSet();
40368
40374
  const enc = (v) => {
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  readInstalledConnectorPilots,
28
28
  simulate,
29
29
  writeConnectorPilots
30
- } from "./chunk-CXW2EIRM.mjs";
30
+ } from "./chunk-7MHK5RF4.mjs";
31
31
  import {
32
32
  ProtectGateway,
33
33
  buildDecisionContext,
@@ -39,7 +39,7 @@ import {
39
39
  resolveCredential,
40
40
  sendApprovalNotification,
41
41
  validateCredentials
42
- } from "./chunk-GHR65WVD.mjs";
42
+ } from "./chunk-PB3TC7E3.mjs";
43
43
  import {
44
44
  createSandboxServer
45
45
  } from "./chunk-SETXVE2K.mjs";
@@ -54,8 +54,8 @@ import {
54
54
  forwardReceipt,
55
55
  getScopeBlindBridge,
56
56
  startHookServer
57
- } from "./chunk-744JMCY4.mjs";
58
- import "./chunk-KMNXHGGT.mjs";
57
+ } from "./chunk-DX7QOA3E.mjs";
58
+ import "./chunk-HPKHMVZX.mjs";
59
59
  import {
60
60
  sha256 as sha2562
61
61
  } from "./chunk-AYNQIEN7.mjs";
@@ -73,7 +73,7 @@ import {
73
73
  policySetFromSource,
74
74
  runEvaluatorSelfTest,
75
75
  signDecision
76
- } from "./chunk-IDUH2O4Q.mjs";
76
+ } from "./chunk-XLJUZ4WO.mjs";
77
77
  import {
78
78
  Field,
79
79
  _abool2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "mcpName": "com.scopeblind/protect-mcp",
5
5
  "description": "Fail-closed Cedar policy gate + Ed25519 signed receipts for AI agent tool calls. Denies on any policy error, proves the gate is live with a startup self-test, and turns every decision into a local searchable record you own. The open gate behind Legate by ScopeBlind. scopeblind.com",
6
6
  "main": "dist/index.js",