protect-mcp 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2: anchor a claim to the public log
4
+
5
+ Closes the one honest gap in a bare claim: that the disclosed set is complete.
6
+
7
+ - Added `protect-mcp claim --anchor`: records the claim's digest in the public,
8
+ append-only ScopeBlind transparency log, so a counterparty who does not trust
9
+ you can confirm the exact claim existed at a time and was not quietly re-cut.
10
+ Only the hash is sent; the claim, its leaves, and every receipt stay on your
11
+ machine. Writes a `<claim>.anchor.json` sidecar with the log entry and a
12
+ verify URL. Anonymous and free; enrolling an org identity a counterparty can
13
+ pin is the paid upgrade (scopeblind.com/enroll).
14
+
15
+ ## 0.9.1: prove a claim without revealing the record
16
+
17
+ The record viewer closes the loop from a decision to a portable proof.
18
+
19
+ - The `record` viewer surfaces the exact `claim` command when you filter by a
20
+ capability, so a decision becomes a signed, position-blind attestation in one
21
+ step, with copy-to-clipboard and an offline `verify-claim` hint.
22
+ - The browser drop-viewer (legate.scopeblind.com/record) reached parity with
23
+ the CLI: signed capability tags, a capability facet, and a provenance tree.
24
+
25
+ ## 0.9.0: signed, position-blind claim attestations
26
+
27
+ Prove a predicate over the record without revealing it.
28
+
29
+ - Added `protect-mcp claim`: mints a signed attestation of a predicate over the
30
+ record (`--no <cap>` for "no action carried this capability", `--only`,
31
+ `--count`/`--no-verdict <verdict>`), disclosing only per-decision categories
32
+ (receipt digest, verdict, capability tags), never inputs, outputs, or data.
33
+ - Added `protect-mcp verify-claim`: checks it offline via Ed25519 over the pack,
34
+ a recomputed Merkle root over the disclosed set, and the predicate recomputed
35
+ independently. Honest trust model: an accountable position-blind attestation,
36
+ not zero-knowledge; set-completeness is issuer-attested.
37
+
38
+ ## 0.8.0: deterministic receipt enrichment
39
+
40
+ Each signed decision now carries minimum-disclosure, rule-derived metadata.
41
+
42
+ - `input_digest`: SHA-256 of the canonicalized tool input, so you can prove what
43
+ was requested without storing it.
44
+ - Signed `capabilities`: deterministic, rule-derived tags (fs.read, fs.write,
45
+ net.egress, secret.adjacent, destructive, financial, exec.shell, ...) so the
46
+ record is queryable by what an action touched.
47
+ - `resource`: the hashed target (path, host, or command). The gate sees tool
48
+ calls, not the model's prompt or reasoning, and does not claim to.
49
+
50
+ ## 0.7.6: the live record viewer
51
+
52
+ - Added `protect-mcp record --live`: a local, streaming view of the receipt
53
+ file that turns decisions into a searchable, auto-labelled record as the agent
54
+ runs (stat strip, per-row signed badge and digest, a List/Tree provenance
55
+ view), all local, nothing uploaded.
56
+ - One-click export: `.jsonl` carrying the real signatures (so a recipient
57
+ verifies offline with `npx @veritasacta/verify`) plus a Markdown report.
58
+
59
+ ## 0.7.5: honest version strings and the record viewer
60
+
61
+ - Added `protect-mcp record`, a local browser viewer over the receipts file
62
+ (the answer to "the receipts are a hidden dotfile I cannot find").
63
+ - Fixed hard-coded version strings in the banner and health/serverInfo: they
64
+ read the resolved package version now.
65
+ - WebAuthn co-sign hardening: verify the rpIdHash and enforce challenge expiry.
66
+
3
67
  ## 0.7.4: the self-serve trial path
4
68
 
5
69
  Everything the scopeblind.com trial and docs pages describe now ships in the
package/README.md CHANGED
@@ -189,7 +189,7 @@ session always runs the gate you tested:
189
189
  "hooks": [
190
190
  {
191
191
  "type": "command",
192
- "command": "npx protect-mcp@0.7.3 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
192
+ "command": "npx protect-mcp@0.9.1 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
193
193
  }
194
194
  ]
195
195
  }
@@ -200,7 +200,7 @@ session always runs the gate you tested:
200
200
  "hooks": [
201
201
  {
202
202
  "type": "command",
203
- "command": "npx protect-mcp@0.7.3 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
203
+ "command": "npx protect-mcp@0.9.1 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
204
204
  }
205
205
  ]
206
206
  }
@@ -118,9 +118,81 @@ function verifyClaim(pack, overridePublicKey) {
118
118
  reasons
119
119
  };
120
120
  }
121
+ var ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
122
+ var DEFAULT_LOG = "https://scopeblind.com";
123
+ function anchorDeepSort(o) {
124
+ if (o === null || typeof o !== "object") return o;
125
+ if (Array.isArray(o)) return o.map(anchorDeepSort);
126
+ const src = o;
127
+ const out = {};
128
+ for (const k of Object.keys(src).sort()) out[k] = anchorDeepSort(src[k]);
129
+ return out;
130
+ }
131
+ function toBase64(bytes) {
132
+ let bin = "";
133
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
134
+ return btoa(bin);
135
+ }
136
+ function claimDigest(pack) {
137
+ return sha256Hex(canonicalJson(pack));
138
+ }
139
+ function buildAnchorEnvelope(pack, key, issuedAt) {
140
+ const signed = {
141
+ type: "evidence_pack",
142
+ schema: ANCHOR_SCHEMA,
143
+ anchors: "protect-mcp-claim",
144
+ claim_digest: claimDigest(pack),
145
+ record_root: pack.record.root,
146
+ statement: pack.claim.statement,
147
+ holds: pack.claim.holds,
148
+ matched: pack.claim.matched,
149
+ total: pack.scope.total,
150
+ issued_at: issuedAt,
151
+ verification_key: key.publicKey,
152
+ disclosure: "internal"
153
+ };
154
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
155
+ const digest = bytesToHex(hash);
156
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
157
+ return { ...signed, signature, digest };
158
+ }
159
+ async function anchorClaim(pack, key, opts) {
160
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
161
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
162
+ const doFetch = opts.fetchImpl || globalThis.fetch;
163
+ if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
164
+ const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
165
+ try {
166
+ const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
167
+ method: "POST",
168
+ headers: { "content-type": "application/json" },
169
+ body: JSON.stringify({ encoded })
170
+ });
171
+ const data = await resp.json().catch(() => null);
172
+ if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
173
+ return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
174
+ }
175
+ return {
176
+ ok: true,
177
+ claim_digest: envelope.claim_digest,
178
+ seq: data.seq,
179
+ entry_url: `${base}/fn/log/${data.seq}`,
180
+ anchored_at: data.anchored_at,
181
+ already_anchored: !!data.already_anchored,
182
+ envelope
183
+ };
184
+ } catch {
185
+ return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
186
+ }
187
+ }
121
188
  export {
189
+ ANCHOR_SCHEMA,
122
190
  CLAIM_TYPE,
191
+ DEFAULT_LOG,
192
+ anchorClaim,
193
+ buildAnchorEnvelope,
123
194
  buildClaim,
195
+ claimDigest,
124
196
  evaluate,
125
197
  leafHash,
126
198
  merkleRoot,
package/dist/cli.js CHANGED
@@ -5176,8 +5176,13 @@ var init_receipt_enrichment = __esm({
5176
5176
  // src/claim.ts
5177
5177
  var claim_exports = {};
5178
5178
  __export(claim_exports, {
5179
+ ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
5179
5180
  CLAIM_TYPE: () => CLAIM_TYPE,
5181
+ DEFAULT_LOG: () => DEFAULT_LOG,
5182
+ anchorClaim: () => anchorClaim,
5183
+ buildAnchorEnvelope: () => buildAnchorEnvelope,
5180
5184
  buildClaim: () => buildClaim,
5185
+ claimDigest: () => claimDigest,
5181
5186
  evaluate: () => evaluate,
5182
5187
  leafHash: () => leafHash,
5183
5188
  merkleRoot: () => merkleRoot,
@@ -5286,7 +5291,72 @@ function verifyClaim(pack, overridePublicKey) {
5286
5291
  reasons
5287
5292
  };
5288
5293
  }
5289
- var CLAIM_TYPE;
5294
+ function anchorDeepSort(o) {
5295
+ if (o === null || typeof o !== "object") return o;
5296
+ if (Array.isArray(o)) return o.map(anchorDeepSort);
5297
+ const src = o;
5298
+ const out = {};
5299
+ for (const k of Object.keys(src).sort()) out[k] = anchorDeepSort(src[k]);
5300
+ return out;
5301
+ }
5302
+ function toBase64(bytes) {
5303
+ let bin = "";
5304
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
5305
+ return btoa(bin);
5306
+ }
5307
+ function claimDigest(pack) {
5308
+ return sha256Hex3(canonicalJson(pack));
5309
+ }
5310
+ function buildAnchorEnvelope(pack, key, issuedAt) {
5311
+ const signed = {
5312
+ type: "evidence_pack",
5313
+ schema: ANCHOR_SCHEMA,
5314
+ anchors: "protect-mcp-claim",
5315
+ claim_digest: claimDigest(pack),
5316
+ record_root: pack.record.root,
5317
+ statement: pack.claim.statement,
5318
+ holds: pack.claim.holds,
5319
+ matched: pack.claim.matched,
5320
+ total: pack.scope.total,
5321
+ issued_at: issuedAt,
5322
+ verification_key: key.publicKey,
5323
+ disclosure: "internal"
5324
+ };
5325
+ const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
5326
+ const digest = bytesToHex(hash);
5327
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
5328
+ return { ...signed, signature, digest };
5329
+ }
5330
+ async function anchorClaim(pack, key, opts) {
5331
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
5332
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
5333
+ const doFetch = opts.fetchImpl || globalThis.fetch;
5334
+ if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
5335
+ const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
5336
+ try {
5337
+ const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
5338
+ method: "POST",
5339
+ headers: { "content-type": "application/json" },
5340
+ body: JSON.stringify({ encoded })
5341
+ });
5342
+ const data = await resp.json().catch(() => null);
5343
+ if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
5344
+ return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
5345
+ }
5346
+ return {
5347
+ ok: true,
5348
+ claim_digest: envelope.claim_digest,
5349
+ seq: data.seq,
5350
+ entry_url: `${base}/fn/log/${data.seq}`,
5351
+ anchored_at: data.anchored_at,
5352
+ already_anchored: !!data.already_anchored,
5353
+ envelope
5354
+ };
5355
+ } catch {
5356
+ return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
5357
+ }
5358
+ }
5359
+ var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
5290
5360
  var init_claim = __esm({
5291
5361
  "src/claim.ts"() {
5292
5362
  "use strict";
@@ -5295,6 +5365,8 @@ var init_claim = __esm({
5295
5365
  init_ed25519();
5296
5366
  init_receipt_enrichment();
5297
5367
  CLAIM_TYPE = "scopeblind.claim.v1";
5368
+ ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
5369
+ DEFAULT_LOG = "https://scopeblind.com";
5298
5370
  }
5299
5371
  });
5300
5372
 
@@ -10712,8 +10784,11 @@ Attest a signed, position-blind claim over your record:
10712
10784
  --only <c1,c2,...> all actions confined to these capabilities
10713
10785
  --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
10714
10786
  --count <verdict> how many, e.g. ${dim("--count blocked")}
10787
+ --anchor also record the claim digest in the public append-only
10788
+ log so a counterparty can trust it is complete (only the
10789
+ hash is sent; your record stays local)
10715
10790
 
10716
- Example: ${bold("npx protect-mcp claim --no net.egress")}
10791
+ Example: ${bold("npx protect-mcp claim --no net.egress --anchor")}
10717
10792
 
10718
10793
  `);
10719
10794
  process.exit(0);
@@ -10789,7 +10864,36 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
10789
10864
  process.stdout.write(` Written to ${out}
10790
10865
  `);
10791
10866
  process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
10792
-
10867
+ `);
10868
+ if (argv.indexOf("--anchor") !== -1) {
10869
+ const { anchorClaim: anchorClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
10870
+ const li = argv.indexOf("--log");
10871
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
10872
+ process.stdout.write(`
10873
+ ${dim("Anchoring the claim digest to the public append-only log (only the hash leaves your machine)...")}
10874
+ `);
10875
+ const res = await anchorClaim2(
10876
+ pack,
10877
+ { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" },
10878
+ { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() }
10879
+ );
10880
+ if (res.ok) {
10881
+ const sidecar = out.replace(/\.json$/, "") + ".anchor.json";
10882
+ writeFileSync4(sidecar, JSON.stringify({ log: logBase || "https://scopeblind.com", seq: res.seq, entry_url: res.entry_url, anchored_at: res.anchored_at, claim_digest: res.claim_digest, envelope: res.envelope }, null, 2) + "\n");
10883
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)}${res.already_anchored ? dim(" (already present)") : ""} ${dim(res.entry_url || "")}
10884
+ `);
10885
+ process.stdout.write(` ${dim("A counterparty can now confirm this exact claim existed at " + (res.anchored_at || "this time") + " and cannot be quietly re-cut.")}
10886
+ `);
10887
+ process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
10888
+ `);
10889
+ process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
10890
+ `);
10891
+ } else {
10892
+ process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
10893
+ `);
10894
+ }
10895
+ }
10896
+ process.stdout.write(`
10793
10897
  `);
10794
10898
  process.exit(0);
10795
10899
  }
package/dist/cli.mjs CHANGED
@@ -2235,7 +2235,7 @@ if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(functio
2235
2235
  async function handleClaim(argv) {
2236
2236
  const { readFileSync, existsSync, writeFileSync } = await import("fs");
2237
2237
  const { join } = await import("path");
2238
- const { buildClaim } = await import("./claim-U7Q3A4WW.mjs");
2238
+ const { buildClaim } = await import("./claim-TUDH2WPB.mjs");
2239
2239
  let dir = process.cwd();
2240
2240
  const di = argv.indexOf("--dir");
2241
2241
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
@@ -2254,8 +2254,11 @@ Attest a signed, position-blind claim over your record:
2254
2254
  --only <c1,c2,...> all actions confined to these capabilities
2255
2255
  --no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
2256
2256
  --count <verdict> how many, e.g. ${dim("--count blocked")}
2257
+ --anchor also record the claim digest in the public append-only
2258
+ log so a counterparty can trust it is complete (only the
2259
+ hash is sent; your record stays local)
2257
2260
 
2258
- Example: ${bold("npx protect-mcp claim --no net.egress")}
2261
+ Example: ${bold("npx protect-mcp claim --no net.egress --anchor")}
2259
2262
 
2260
2263
  `);
2261
2264
  process.exit(0);
@@ -2331,13 +2334,42 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
2331
2334
  process.stdout.write(` Written to ${out}
2332
2335
  `);
2333
2336
  process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
2334
-
2337
+ `);
2338
+ if (argv.indexOf("--anchor") !== -1) {
2339
+ const { anchorClaim } = await import("./claim-TUDH2WPB.mjs");
2340
+ const li = argv.indexOf("--log");
2341
+ const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
2342
+ process.stdout.write(`
2343
+ ${dim("Anchoring the claim digest to the public append-only log (only the hash leaves your machine)...")}
2344
+ `);
2345
+ const res = await anchorClaim(
2346
+ pack,
2347
+ { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" },
2348
+ { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() }
2349
+ );
2350
+ if (res.ok) {
2351
+ const sidecar = out.replace(/\.json$/, "") + ".anchor.json";
2352
+ writeFileSync(sidecar, JSON.stringify({ log: logBase || "https://scopeblind.com", seq: res.seq, entry_url: res.entry_url, anchored_at: res.anchored_at, claim_digest: res.claim_digest, envelope: res.envelope }, null, 2) + "\n");
2353
+ process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)}${res.already_anchored ? dim(" (already present)") : ""} ${dim(res.entry_url || "")}
2354
+ `);
2355
+ process.stdout.write(` ${dim("A counterparty can now confirm this exact claim existed at " + (res.anchored_at || "this time") + " and cannot be quietly re-cut.")}
2356
+ `);
2357
+ process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
2358
+ `);
2359
+ process.stdout.write(` ${dim("To anchor as your enrolled org identity (a key a counterparty can pin), see")} ${bold("scopeblind.com/enroll")}
2360
+ `);
2361
+ } else {
2362
+ process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
2363
+ `);
2364
+ }
2365
+ }
2366
+ process.stdout.write(`
2335
2367
  `);
2336
2368
  process.exit(0);
2337
2369
  }
2338
2370
  async function handleVerifyClaim(argv) {
2339
2371
  const { readFileSync, existsSync } = await import("fs");
2340
- const { verifyClaim } = await import("./claim-U7Q3A4WW.mjs");
2372
+ const { verifyClaim } = await import("./claim-TUDH2WPB.mjs");
2341
2373
  const file = argv.find((a) => !a.startsWith("--"));
2342
2374
  if (!file || !existsSync(file)) {
2343
2375
  process.stderr.write(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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",
@@ -1,50 +0,0 @@
1
- // Generated by protect-mcp init-hooks
2
- // Customize these policies to match your security requirements.
3
- // Cedar deny decisions are AUTHORITATIVE — they cannot be overridden.
4
-
5
- // Allow all read-only tools by default
6
- permit(
7
- principal,
8
- action == Action::"MCP::Tool::call",
9
- resource == Tool::"Read"
10
- );
11
-
12
- permit(
13
- principal,
14
- action == Action::"MCP::Tool::call",
15
- resource == Tool::"Glob"
16
- );
17
-
18
- permit(
19
- principal,
20
- action == Action::"MCP::Tool::call",
21
- resource == Tool::"Grep"
22
- );
23
-
24
- // Allow write/edit tools (remove these to require explicit approval)
25
- permit(
26
- principal,
27
- action == Action::"MCP::Tool::call",
28
- resource == Tool::"Write"
29
- );
30
-
31
- permit(
32
- principal,
33
- action == Action::"MCP::Tool::call",
34
- resource == Tool::"Edit"
35
- );
36
-
37
- // Allow Bash with caution (Cedar evaluates before hook patterns)
38
- permit(
39
- principal,
40
- action == Action::"MCP::Tool::call",
41
- resource == Tool::"Bash"
42
- );
43
-
44
- // Block dangerous tools entirely
45
- // Uncomment any of these to block specific tools:
46
- // forbid(
47
- // principal,
48
- // action == Action::"MCP::Tool::call",
49
- // resource == Tool::"delete_file"
50
- // );