protect-mcp 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -11,11 +11,11 @@ import {
11
11
  readInstalledConnectorPilots,
12
12
  simulate,
13
13
  writeConnectorPilots
14
- } from "./chunk-36UID5WY.mjs";
14
+ } from "./chunk-JCMDLN5I.mjs";
15
15
  import {
16
16
  ProtectGateway,
17
17
  validateCredentials
18
- } from "./chunk-UBZJ3VI2.mjs";
18
+ } from "./chunk-VTPZ4G5I.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-D2RDY2JR.mjs";
29
+ } from "./chunk-WIPWNWMJ.mjs";
30
30
  import "./chunk-PQJP2ZCI.mjs";
31
31
 
32
32
  // src/cli.ts
@@ -36,7 +36,7 @@ import { basename as basenameCli, dirname as dirnameCli, join as joinCli, resolv
36
36
  import { homedir as homedirCli } from "os";
37
37
  function printHelp() {
38
38
  process.stderr.write(`
39
- protect-mcp \u2014 Enterprise security gateway for MCP servers & Claude Code hooks
39
+ protect-mcp: Enterprise security gateway for MCP servers & Claude Code hooks
40
40
 
41
41
  Usage:
42
42
  protect-mcp [options] -- <command> [args...]
@@ -59,6 +59,7 @@ Usage:
59
59
  protect-mcp status [--dir <path>]
60
60
  protect-mcp digest [--today] [--dir <path>]
61
61
  protect-mcp receipts [--last <n>] [--dir <path>]
62
+ protect-mcp record [--dir <path>] [--no-open]
62
63
  protect-mcp bundle [--output <path>] [--dir <path>]
63
64
  protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
64
65
  protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
@@ -96,6 +97,7 @@ Commands:
96
97
  status Show tool call statistics from the local decision log
97
98
  digest Generate a human-readable summary of agent activity
98
99
  receipts Show recent persisted signed receipts
100
+ record Open a local, searchable view of your record in the browser
99
101
  bundle Export an offline-verifiable audit bundle
100
102
 
101
103
  Examples:
@@ -194,7 +196,7 @@ async function handleInit(argv) {
194
196
  let keypair;
195
197
  {
196
198
  const { randomBytes } = await import("crypto");
197
- const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
199
+ const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
198
200
  const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
199
201
  const privateKey = randomBytes(32);
200
202
  const publicKey = ed25519.getPublicKey(privateKey);
@@ -1942,6 +1944,162 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
1942
1944
  process.stdout.write(`
1943
1945
  `);
1944
1946
  }
1947
+ var _pkgV = null;
1948
+ async function pkgVersion() {
1949
+ if (_pkgV) return _pkgV;
1950
+ let v = "0.0.0";
1951
+ try {
1952
+ const { readFileSync, existsSync, realpathSync } = await import("fs");
1953
+ const { dirname, join, resolve } = await import("path");
1954
+ let base = "";
1955
+ try {
1956
+ base = dirname(realpathSync(resolve(process.argv[1] || "")));
1957
+ } catch {
1958
+ }
1959
+ const candidates = [
1960
+ base ? join(base, "..", "package.json") : "",
1961
+ base ? join(base, "package.json") : ""
1962
+ ].filter(Boolean);
1963
+ for (const p of candidates) {
1964
+ if (existsSync(p)) {
1965
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
1966
+ if (parsed && parsed.name === "protect-mcp" && parsed.version) {
1967
+ v = parsed.version;
1968
+ break;
1969
+ }
1970
+ }
1971
+ }
1972
+ } catch {
1973
+ }
1974
+ _pkgV = v;
1975
+ return v;
1976
+ }
1977
+ function mapRecordEntry(e) {
1978
+ const p = e && e.payload && typeof e.payload === "object" ? e.payload : e;
1979
+ const dec = String(p.decision || e.decision || "").toLowerCase();
1980
+ const verdict = /den|block|reject|refus/.test(dec) ? "blocked" : /ask|approv|hold|escal|review|pending/.test(dec) ? "held" : "allowed";
1981
+ const tsRaw = e.issued_at || e.timestamp || p.timestamp || p.issued_at;
1982
+ const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
1983
+ const ts = isFinite(ms) ? new Date(ms).toISOString() : "";
1984
+ const tool = String(p.tool || e.tool || "action");
1985
+ const reason = String(p.reason_code || e.reason_code || p.policy_engine || "signed");
1986
+ const hook = String(p.hook_event || e.hook_event || "");
1987
+ const signed = !!(e.signature || e.sig || e.receipt_hash || typeof e.type === "string" && e.type.indexOf("receipt") >= 0);
1988
+ let digest = "";
1989
+ if (e.receipt_hash) digest = String(e.receipt_hash);
1990
+ else if (e.digest) digest = String(e.digest);
1991
+ else if (p.payload_digest && p.payload_digest.output_hash) digest = String(p.payload_digest.output_hash);
1992
+ return { ts, tool, verdict, reason, hook, signed, id: String(e.request_id || p.request_id || ""), digest };
1993
+ }
1994
+ async function handleRecord(argv) {
1995
+ const { readFileSync, existsSync, writeFileSync } = await import("fs");
1996
+ const { join } = await import("path");
1997
+ const osMod = await import("os");
1998
+ const cp = await import("child_process");
1999
+ let dir = process.cwd();
2000
+ const di = argv.indexOf("--dir");
2001
+ if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
2002
+ const recPath = join(dir, ".protect-mcp-receipts.jsonl");
2003
+ const logPath = join(dir, ".protect-mcp-log.jsonl");
2004
+ const chosen = existsSync(recPath) ? recPath : existsSync(logPath) ? logPath : null;
2005
+ if (!chosen) {
2006
+ process.stderr.write(`
2007
+ ${bold("protect-mcp record")}
2008
+
2009
+ No record found in ${dir}.
2010
+ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
2011
+
2012
+ `);
2013
+ process.exit(0);
2014
+ return;
2015
+ }
2016
+ const raw = readFileSync(chosen, "utf-8");
2017
+ const recs = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
2018
+ try {
2019
+ return JSON.parse(l);
2020
+ } catch {
2021
+ return null;
2022
+ }
2023
+ }).filter((x) => x !== null).map(mapRecordEntry);
2024
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length };
2025
+ const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
2026
+ const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
2027
+ writeFileSync(out, html);
2028
+ if (!argv.includes("--no-open")) {
2029
+ const platform = process.platform;
2030
+ const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2031
+ const openArgs = platform === "win32" ? ["/c", "start", "", out] : [out];
2032
+ try {
2033
+ const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
2034
+ child.unref();
2035
+ } catch {
2036
+ }
2037
+ }
2038
+ process.stdout.write(`
2039
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${recs.length} decision${recs.length === 1 ? "" : "s"}
2040
+ `);
2041
+ process.stdout.write(` Opened a searchable view in your browser. Everything stayed on this machine.
2042
+ `);
2043
+ if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
2044
+ `);
2045
+ process.stdout.write(` ${dim("If it did not open, open this file: " + out)}
2046
+
2047
+ `);
2048
+ process.exit(0);
2049
+ }
2050
+ var RECORD_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>protect-mcp record</title>
2051
+ <style>
2052
+ :root{--paper:#f6f4ef;--ink:#1b1815;--soft:#524d46;--faint:#8a837a;--line:#ddd7c9;--g:#3f6146;--gb:#e7eee3;--a:#8f6216;--ab:#f2e8d3;--r:#7d3535;--rb:#f2e0dc}
2053
+ *{box-sizing:border-box}
2054
+ body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
2055
+ .wrap{max-width:1000px;margin:0 auto;padding:26px 22px 60px}
2056
+ h1{font-size:24px;margin:0 0 4px;letter-spacing:-.012em}
2057
+ .meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace}
2058
+ .bar{margin:18px 0 12px}
2059
+ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit}
2060
+ .chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
2061
+ .chip{cursor:pointer;font-size:12px;padding:3px 10px;border-radius:100px;border:1px solid var(--line);background:#fff;color:var(--soft)}
2062
+ .chip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
2063
+ .count{color:var(--faint);font-size:12px;font-family:ui-monospace,Menlo,monospace;margin-bottom:8px}
2064
+ .row{border:1px solid var(--line);border-radius:9px;background:#fcfbf7;padding:11px 13px;margin-bottom:8px;cursor:pointer}
2065
+ .top{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
2066
+ .pill{font-size:11px;font-weight:600;padding:2px 9px;border-radius:100px}
2067
+ .pill.allowed{background:var(--gb);color:var(--g)}.pill.held{background:var(--ab);color:var(--a)}.pill.blocked{background:var(--rb);color:var(--r)}
2068
+ .tag{font-size:11px;padding:1px 7px;border-radius:100px;background:var(--paper);border:1px solid var(--line);color:var(--faint)}
2069
+ .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
2070
+ .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}
2071
+ .row.open .det{display:block}
2072
+ .foot{margin-top:22px;color:var(--faint);font-size:12px;line-height:1.6;border-top:1px solid var(--line);padding-top:14px}
2073
+ .foot b{color:var(--soft)}
2074
+ </style></head><body><div class="wrap">
2075
+ <h1>Your record</h1>
2076
+ <div class="meta" id="meta"></div>
2077
+ <div class="bar"><input id="q" placeholder="Search your record: tool, reason, verdict"></div>
2078
+ <div class="chips" id="chips"></div>
2079
+ <div class="count" id="count"></div>
2080
+ <div id="list"></div>
2081
+ <div class="foot">Signed decisions from your own gate. This page is local and nothing was uploaded. Verify authoritatively with <b>npx protect-mcp receipts</b> or the open verifier. protect-mcp governs proposed actions before they run.</div>
2082
+ </div>
2083
+ <script>
2084
+ var RECORDS=__DATA__;var META=__META__;var Q="",ACT={};
2085
+ function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
2086
+ function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
2087
+ 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"})}
2088
+ function fvals(key){var m={};RECORDS.forEach(function(r){var v=key==="Decision"?vlabel(r.verdict):r[key.toLowerCase()];if(v){m[v]=(m[v]||0)+1}});return Object.keys(m).sort(function(a,b){return m[b]-m[a]}).slice(0,8).map(function(v){return[v,m[v]]})}
2089
+ function match(r){if(ACT.Decision&&vlabel(r.verdict)!==ACT.Decision)return false;if(ACT.Tool&&r.tool!==ACT.Tool)return false;if(ACT.Reason&&r.reason!==ACT.Reason)return false;if(Q){var h=(r.tool+" "+r.reason+" "+vlabel(r.verdict)+" "+r.hook).toLowerCase();if(h.indexOf(Q)<0)return false}return true}
2090
+ function render(){
2091
+ document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local";
2092
+ var chips="";["Decision","Tool","Reason"].forEach(function(key){fvals(key).forEach(function(p){var on=ACT[key]===p[0];chips+='<span class="chip'+(on?" on":"")+'" data-k="'+key+'" data-v="'+esc(p[0])+'">'+esc(p[0])+" "+p[1]+"</span>"})});
2093
+ document.getElementById("chips").innerHTML=chips;
2094
+ var rows=RECORDS.filter(match);
2095
+ document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records";
2096
+ var html="";rows.slice(0,800).forEach(function(r){html+='<div class="row"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+'<span class="tag">'+(r.signed?"signed":"log")+'</span><span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r,null,2))+"</div></div>"});
2097
+ document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>'}
2098
+ document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
2099
+ 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()});
2100
+ document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
2101
+ render();
2102
+ </script></body></html>`;
1945
2103
  async function handleBundle(argv) {
1946
2104
  const { readFileSync, writeFileSync, existsSync } = await import("fs");
1947
2105
  const { join } = await import("path");
@@ -2063,7 +2221,7 @@ ${bold("protect-mcp connect")}
2063
2221
  `));
2064
2222
  process.stderr.write(` Receipts will be uploaded automatically.
2065
2223
  `);
2066
- process.stderr.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
2224
+ process.stderr.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
2067
2225
  `));
2068
2226
  process.stderr.write(`
2069
2227
  ${"\u2500".repeat(50)}
@@ -2106,7 +2264,7 @@ ${bold("protect-mcp quickstart")}
2106
2264
  const { randomBytes } = await import("crypto");
2107
2265
  let keypair;
2108
2266
  try {
2109
- const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
2267
+ const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
2110
2268
  const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
2111
2269
  const privateKey = randomBytes(32);
2112
2270
  const publicKey = ed25519.getPublicKey(privateKey);
@@ -2161,7 +2319,7 @@ ${bold("protect-mcp quickstart")}
2161
2319
  `));
2162
2320
  process.stdout.write(` Receipts will be uploaded automatically.
2163
2321
  `);
2164
- process.stdout.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
2322
+ process.stdout.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
2165
2323
  `));
2166
2324
  process.stdout.write(`
2167
2325
  `);
@@ -2321,7 +2479,7 @@ ${bold("protect-mcp registry status")}
2321
2479
  async function handleKillerDemo(argv) {
2322
2480
  const { mkdtempSync } = await import("fs");
2323
2481
  const { tmpdir } = await import("os");
2324
- const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
2482
+ const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
2325
2483
  const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
2326
2484
  const { randomBytes } = await import("crypto");
2327
2485
  const artifacts = await import("@veritasacta/artifacts");
@@ -2329,7 +2487,7 @@ async function handleKillerDemo(argv) {
2329
2487
  createSelectiveDisclosurePackage,
2330
2488
  signCommittedDecision,
2331
2489
  verifySelectiveDisclosurePackage
2332
- } = await import("./signing-committed-MMLJ6OGG.mjs");
2490
+ } = await import("./signing-committed-QXCW24RF.mjs");
2333
2491
  const registryMod = await import("./receipt-registry-6CAOY6RP.mjs");
2334
2492
  const dir = resolveCli(flagValue(argv, "--dir") || mkdtempSync(joinCli(tmpdir(), "scopeblind-killer-demo-")));
2335
2493
  mkdirSyncCli(dir, { recursive: true });
@@ -2598,7 +2756,7 @@ async function handleVerifyDisclosure(argv) {
2598
2756
  process.stderr.write("Usage: protect-mcp verify-disclosure --receipt <committed-receipt.json> --disclosure <selective-disclosure.json>\\n");
2599
2757
  process.exit(1);
2600
2758
  }
2601
- const { verifySelectiveDisclosurePackage } = await import("./signing-committed-MMLJ6OGG.mjs");
2759
+ const { verifySelectiveDisclosurePackage } = await import("./signing-committed-QXCW24RF.mjs");
2602
2760
  const receipt = JSON.parse(readFileSyncCli(resolveCli(receiptPath), "utf-8"));
2603
2761
  const disclosure = JSON.parse(readFileSyncCli(resolveCli(disclosurePath), "utf-8"));
2604
2762
  const result = verifySelectiveDisclosurePackage(receipt, disclosure);
@@ -2910,7 +3068,7 @@ ${bold("protect-mcp trace")}
2910
3068
  process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
2911
3069
  `);
2912
3070
  if (rendered.has(id)) {
2913
- process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
3071
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle: already rendered)")}
2914
3072
  `);
2915
3073
  return;
2916
3074
  }
@@ -3094,7 +3252,7 @@ ${bold("protect-mcp init-hooks")}
3094
3252
  if (!existsSync(keysDir)) mkdirSync(keysDir, { recursive: true });
3095
3253
  const { randomBytes: rb } = await import("crypto");
3096
3254
  try {
3097
- const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
3255
+ const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
3098
3256
  const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
3099
3257
  const privateKey = rb(32);
3100
3258
  const publicKey = ed25519.getPublicKey(privateKey);
@@ -3113,7 +3271,7 @@ ${bold("protect-mcp init-hooks")}
3113
3271
 
3114
3272
  `);
3115
3273
  } catch {
3116
- process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys \u2014 signing disabled
3274
+ process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys, signing disabled
3117
3275
 
3118
3276
  `);
3119
3277
  }
@@ -3184,13 +3342,14 @@ ${bold("protect-mcp init-hooks")}
3184
3342
  process.stdout.write(` Every tool call will be receipted automatically.
3185
3343
 
3186
3344
  `);
3187
- process.stdout.write(` 3. Check receipts:
3345
+ process.stdout.write(` 3. See your record: a searchable view of every decision.
3188
3346
  `);
3189
- process.stdout.write(` ${dim(`curl http://127.0.0.1:${port}/receipts/latest`)}
3347
+ process.stdout.write(` ${dim(`npx protect-mcp record`)}
3190
3348
  `);
3191
- process.stdout.write(` ${dim(`npx protect-mcp receipts`)}
3349
+ process.stdout.write(` ${dim(`Everything stays on this machine. Nothing is uploaded.`)}
3350
+
3192
3351
  `);
3193
- process.stdout.write(` Or use ${dim("/verify-receipt")} inside Claude Code.
3352
+ process.stdout.write(` Prefer the terminal? ${dim(`npx protect-mcp receipts`)}, or ${dim("/verify-receipt")} in Claude Code.
3194
3353
 
3195
3354
  `);
3196
3355
  process.stdout.write(` 4. View policy suggestions:
@@ -3200,9 +3359,9 @@ ${bold("protect-mcp init-hooks")}
3200
3359
  `);
3201
3360
  process.stdout.write(`${bold("Key facts:")}
3202
3361
  `);
3203
- process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")} \u2014 cannot be overridden
3362
+ process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}: they cannot be overridden
3204
3363
  `);
3205
- process.stdout.write(` \u2022 PostToolUse runs ${bold("async")} \u2014 zero latency impact on tool execution
3364
+ process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}, so there is zero latency impact on tool execution
3206
3365
  `);
3207
3366
  process.stdout.write(` \u2022 Receipts are Ed25519-signed and append-only
3208
3367
  `);
@@ -3221,15 +3380,7 @@ async function sendInstallTelemetry() {
3221
3380
  if (existsSync(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
3222
3381
  return;
3223
3382
  }
3224
- let version = "unknown";
3225
- try {
3226
- const thisDir = dirname(fileURLToPath(import.meta.url));
3227
- const pkgPath = join(thisDir, "..", "package.json");
3228
- if (existsSync(pkgPath)) {
3229
- version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
3230
- }
3231
- } catch {
3232
- }
3383
+ const version = await pkgVersion();
3233
3384
  const controller = new AbortController();
3234
3385
  const timeout = setTimeout(() => controller.abort(), 3e3);
3235
3386
  fetch("https://api.scopeblind.com/telemetry/install", {
@@ -3403,6 +3554,7 @@ async function main() {
3403
3554
  sendInstallTelemetry().catch(() => {
3404
3555
  });
3405
3556
  const args = process.argv.slice(2);
3557
+ process.env.PROTECT_MCP_VERSION = process.env.PROTECT_MCP_VERSION || await pkgVersion();
3406
3558
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
3407
3559
  printHelp();
3408
3560
  process.exit(0);
@@ -3441,6 +3593,10 @@ async function main() {
3441
3593
  await startHookServer({ port, policyPath: policyPath2, cedarDir: cedarDir2, enforce: enforce2, verbose: verbose2 });
3442
3594
  return;
3443
3595
  }
3596
+ if (args[0] === "record") {
3597
+ await handleRecord(args.slice(1));
3598
+ process.exit(0);
3599
+ }
3444
3600
  if (args[0] === "init-hooks") {
3445
3601
  await handleInitHooks(args.slice(1));
3446
3602
  process.exit(0);
@@ -3629,7 +3785,7 @@ async function main() {
3629
3785
  if (useHttp) {
3630
3786
  const portIdx = args.indexOf("--port");
3631
3787
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
3632
- const { startHttpTransport } = await import("./http-transport-4GMMRPW7.mjs");
3788
+ const { startHttpTransport } = await import("./http-transport-JBORN27G.mjs");
3633
3789
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
3634
3790
  return;
3635
3791
  }
@@ -3698,7 +3854,7 @@ async function handleDoctor() {
3698
3854
  process.stdout.write(green2(`Node.js ${nodeVersion}
3699
3855
  `));
3700
3856
  } else {
3701
- process.stdout.write(red2(`Node.js ${nodeVersion} \u2014 requires >= 18
3857
+ process.stdout.write(red2(`Node.js ${nodeVersion}, requires >= 18
3702
3858
  `));
3703
3859
  issues++;
3704
3860
  }
@@ -3709,7 +3865,7 @@ async function handleDoctor() {
3709
3865
  if (config.signing?.private_key || config.signing?.key_file) {
3710
3866
  process.stdout.write(green2("Signing keys configured\n"));
3711
3867
  } else {
3712
- process.stdout.write(yellow2("Config found but no signing keys \u2014 run: protect-mcp init\n"));
3868
+ process.stdout.write(yellow2("Config found but no signing keys. Run: protect-mcp init\n"));
3713
3869
  issues++;
3714
3870
  }
3715
3871
  } catch {
@@ -3717,7 +3873,7 @@ async function handleDoctor() {
3717
3873
  issues++;
3718
3874
  }
3719
3875
  } else {
3720
- process.stdout.write(yellow2("No scopeblind.config.json \u2014 run: protect-mcp init\n"));
3876
+ process.stdout.write(yellow2("No scopeblind.config.json. Run: protect-mcp init\n"));
3721
3877
  }
3722
3878
  let policyFound = false;
3723
3879
  for (const dir of ["cedar", "policies", "."]) {
@@ -3742,14 +3898,14 @@ async function handleDoctor() {
3742
3898
  }
3743
3899
  }
3744
3900
  if (!policyFound) {
3745
- process.stdout.write(yellow2("No policy files found \u2014 running in shadow mode (allow all)\n"));
3901
+ process.stdout.write(yellow2("No policy files found, running in shadow mode (allow all)\n"));
3746
3902
  }
3747
3903
  try {
3748
3904
  const cedarAvailable = await isCedarAvailable();
3749
3905
  if (cedarAvailable) {
3750
3906
  process.stdout.write(green2("Cedar WASM engine available\n"));
3751
3907
  } else {
3752
- process.stdout.write(dim2(" Cedar WASM not installed \u2014 install: npm install @cedar-policy/cedar-wasm\n"));
3908
+ process.stdout.write(dim2(" Cedar WASM not installed. Install: npm install @cedar-policy/cedar-wasm\n"));
3753
3909
  }
3754
3910
  } catch {
3755
3911
  process.stdout.write(dim2(" Cedar WASM not installed\n"));
@@ -3765,7 +3921,7 @@ async function handleDoctor() {
3765
3921
  process.stdout.write(green2("Decision log exists\n"));
3766
3922
  }
3767
3923
  } else {
3768
- process.stdout.write(dim2(" No decision log yet \u2014 will be created on first tool call\n"));
3924
+ process.stdout.write(dim2(" No decision log yet, will be created on first tool call\n"));
3769
3925
  }
3770
3926
  if (existsSync(receiptFile)) {
3771
3927
  try {
@@ -3780,17 +3936,17 @@ async function handleDoctor() {
3780
3936
  execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
3781
3937
  process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
3782
3938
  } catch {
3783
- process.stdout.write(dim2(" Verifier not cached \u2014 install: npm install -g @veritasacta/verify\n"));
3939
+ process.stdout.write(dim2(" Verifier not cached. Install: npm install -g @veritasacta/verify\n"));
3784
3940
  }
3785
3941
  try {
3786
3942
  const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
3787
3943
  if (res.ok) {
3788
3944
  process.stdout.write(green2("ScopeBlind API reachable\n"));
3789
3945
  } else {
3790
- process.stdout.write(yellow2("ScopeBlind API returned non-200 \u2014 receipts will be stored locally\n"));
3946
+ process.stdout.write(yellow2("ScopeBlind API returned non-200, receipts will be stored locally\n"));
3791
3947
  }
3792
3948
  } catch {
3793
- process.stdout.write(dim2(" ScopeBlind API not reachable \u2014 offline mode (receipts stored locally)\n"));
3949
+ process.stdout.write(dim2(" ScopeBlind API not reachable, offline mode (receipts stored locally)\n"));
3794
3950
  }
3795
3951
  process.stdout.write("\nRestraint self-test:\n");
3796
3952
  try {
@@ -35115,7 +35115,7 @@ function handleRequest(request) {
35115
35115
  id: request.id,
35116
35116
  result: {
35117
35117
  protocolVersion: "2024-11-05",
35118
- serverInfo: { name: "protect-mcp-demo", version: "0.5.3" },
35118
+ serverInfo: { name: "protect-mcp-demo", version: process.env.PROTECT_MCP_VERSION || "0.5.3" },
35119
35119
  capabilities: { tools: {} }
35120
35120
  }
35121
35121
  });
@@ -35201,7 +35201,7 @@ function createSandboxServer() {
35201
35201
  const { z } = require_zod();
35202
35202
  const server = new McpServer({
35203
35203
  name: "protect-mcp",
35204
- version: "0.4.5",
35204
+ version: process.env.PROTECT_MCP_VERSION || "0.4.5",
35205
35205
  description: "Security gateway for MCP servers. Per-tool policies, Ed25519-signed receipts, human approval gates, trust tiers."
35206
35206
  });
35207
35207
  server.tool(
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSandboxServer
4
- } from "./chunk-KPSICBAJ.mjs";
4
+ } from "./chunk-SETXVE2K.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
  export {
7
7
  createSandboxServer
@@ -15,7 +15,7 @@ import {
15
15
  ristretto255,
16
16
  ristretto255_hasher,
17
17
  x25519
18
- } from "./chunk-LYKNULYU.mjs";
18
+ } from "./chunk-LJQOALYR.mjs";
19
19
  import "./chunk-D733KAPG.mjs";
20
20
  import "./chunk-PQJP2ZCI.mjs";
21
21
  export {
@@ -1361,7 +1361,7 @@ async function startHookServer(options = {}) {
1361
1361
  res.end(JSON.stringify({
1362
1362
  status: "ok",
1363
1363
  server: "protect-mcp-hooks",
1364
- version: "0.5.0",
1364
+ version: process.env.PROTECT_MCP_VERSION || "unknown",
1365
1365
  uptime_ms: Date.now() - state.startTime,
1366
1366
  mode: enforce ? "enforce" : "shadow",
1367
1367
  policy_digest: policyDigest,
@@ -1448,9 +1448,10 @@ async function startHookServer(options = {}) {
1448
1448
  const pad = (s, n = 46) => s.padEnd(n);
1449
1449
  w(`
1450
1450
  `);
1451
- w(` protect-mcp v0.5.4
1451
+ w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
1452
+ ` : ` protect-mcp
1452
1453
  `);
1453
- w(` ScopeBlind \u2014 https://scopeblind.com
1454
+ w(` ScopeBlind \xB7 https://scopeblind.com
1454
1455
  `);
1455
1456
  w(`
1456
1457
  `);
@@ -1478,7 +1479,13 @@ async function startHookServer(options = {}) {
1478
1479
  `);
1479
1480
  w(`
1480
1481
  `);
1481
- w(` deny is authoritative \u2014 cannot be overridden.
1482
+ w(` deny is authoritative: it cannot be overridden.
1483
+ `);
1484
+ w(`
1485
+ `);
1486
+ w(` See your record npx protect-mcp record
1487
+ `);
1488
+ w(` a searchable view of every decision, all on this machine
1482
1489
  `);
1483
1490
  w(`
1484
1491
  `);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-NVJHGXXG.mjs";
4
- import "./chunk-D2RDY2JR.mjs";
3
+ } from "./chunk-6E2DHBAR.mjs";
4
+ import "./chunk-WIPWNWMJ.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
  export {
7
7
  startHookServer
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ProtectGateway
3
- } from "./chunk-UBZJ3VI2.mjs";
4
- import "./chunk-D2RDY2JR.mjs";
3
+ } from "./chunk-VTPZ4G5I.mjs";
4
+ import "./chunk-WIPWNWMJ.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
 
7
7
  // src/http-transport.ts
@@ -34,7 +34,7 @@ async function startHttpTransport(options) {
34
34
  res.end(JSON.stringify({
35
35
  status: "ok",
36
36
  server: "protect-mcp",
37
- version: "0.4.0",
37
+ version: process.env.PROTECT_MCP_VERSION || "unknown",
38
38
  transport: "streamable-http",
39
39
  mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow",
40
40
  wrapping: serverCommand.join(" ")
package/dist/index.d.mts CHANGED
@@ -1683,7 +1683,7 @@ declare const POLICY_PACKS: PolicyPack[];
1683
1683
  declare function getPolicyPack(id: string): PolicyPack | undefined;
1684
1684
  declare function policyPackIds(): string[];
1685
1685
 
1686
- type ConnectorPilotId = 'github' | 'email-gmail' | 'filesystem-git' | 'slack-teams' | 'finance-pms';
1686
+ type ConnectorPilotId = 'github' | 'email-gmail' | 'filesystem-git' | 'slack-teams' | 'finance-pms' | 'nautilus-trader';
1687
1687
  interface ConnectorEnvVar {
1688
1688
  name: string;
1689
1689
  required: boolean;
@@ -1696,6 +1696,11 @@ interface ConnectorAction {
1696
1696
  mode: 'observe' | 'require_approval' | 'deny';
1697
1697
  description: string;
1698
1698
  }
1699
+ interface ConnectorArtifact {
1700
+ path: string;
1701
+ contents: string;
1702
+ executable?: boolean;
1703
+ }
1699
1704
  interface ConnectorPilot {
1700
1705
  id: ConnectorPilotId;
1701
1706
  category: string;
@@ -1709,6 +1714,7 @@ interface ConnectorPilot {
1709
1714
  setup: string[];
1710
1715
  config: Record<string, unknown>;
1711
1716
  cedar: string;
1717
+ artifacts?: ConnectorArtifact[];
1712
1718
  }
1713
1719
  interface InstalledConnectorPilot {
1714
1720
  id: string;
@@ -2166,6 +2172,29 @@ interface ApprovalResult {
2166
2172
  contextHash: string;
2167
2173
  /** Timestamp of approval */
2168
2174
  approvedAt: string;
2175
+ /** On failure, a machine-readable reason (e.g. 'invalid_signature'). */
2176
+ reason?: string;
2177
+ }
2178
+ /**
2179
+ * The registered credential public key, extracted from the COSE_Key at
2180
+ * registration. ES256 keys are an uncompressed P-256 point; EdDSA keys are a
2181
+ * 32-byte Ed25519 public key.
2182
+ */
2183
+ interface CredentialPublicKey {
2184
+ /** COSE algorithm: -7 = ES256 (P-256 / ECDSA), -8 = EdDSA (Ed25519). */
2185
+ alg: -7 | -8;
2186
+ /** Public key, hex. ES256: 65-byte uncompressed point (0x04 || x || y). EdDSA: 32-byte key. */
2187
+ publicKeyHex: string;
2188
+ }
2189
+ interface VerifyAssertionOptions {
2190
+ /** Allowed origin(s) the assertion must come from, e.g. 'https://app.scopeblind.com'. Defaults to https://<rpId>. */
2191
+ expectedOrigin?: string | string[];
2192
+ /** Require the UV (user-verified / biometric or PIN) flag. Default true. */
2193
+ requireUserVerification?: boolean;
2194
+ /** The signCount stored from the previous assertion; a non-increasing counter signals a cloned authenticator. */
2195
+ prevSignCount?: number;
2196
+ /** Override 'now' (ms) for testing. */
2197
+ now?: number;
2169
2198
  }
2170
2199
  /**
2171
2200
  * Create a WebAuthn challenge for approving a tool call.
@@ -2202,17 +2231,22 @@ declare function toCredentialRequestOptions(challenge: ApprovalChallenge, allowC
2202
2231
  };
2203
2232
  };
2204
2233
  /**
2205
- * Verify a WebAuthn assertion from the client.
2206
- *
2207
- * This is a simplified verification that checks the structure
2208
- * and extracts the authenticator data. For production use with
2209
- * full signature verification, use the @simplewebauthn/server package.
2210
- *
2211
- * @param challenge - The original challenge
2212
- * @param assertion - The assertion from navigator.credentials.get()
2213
- * @returns ApprovalResult with verification details
2214
- */
2215
- declare function verifyApprovalAssertion(challenge: ApprovalChallenge, assertion: ApprovalAssertion): ApprovalResult;
2234
+ * Verify a WebAuthn assertion: full, fail-closed verification of a passkey or
2235
+ * security-key co-sign. This proves a SPECIFIC human authorized a SPECIFIC
2236
+ * action with a hardware-held key the host operator cannot exfiltrate. It
2237
+ * checks, in order: challenge freshness; clientDataJSON type, challenge, and
2238
+ * origin; the rpIdHash; the UP (and, by default, UV) flags; the authenticator
2239
+ * signature over authenticatorData || SHA-256(clientDataJSON) using the
2240
+ * registered credential public key (ES256 or EdDSA); and signCount monotonicity
2241
+ * (clone detection) when a previous count is supplied. Any failure returns
2242
+ * valid:false with a reason; nothing is trusted on a partial check.
2243
+ *
2244
+ * @param challenge - the original challenge
2245
+ * @param assertion - the assertion from navigator.credentials.get()
2246
+ * @param credentialPublicKey - the registered public key for assertion.credentialId
2247
+ * @param opts - origin / UV / signCount / clock options
2248
+ */
2249
+ declare function verifyApprovalAssertion(challenge: ApprovalChallenge, assertion: ApprovalAssertion, credentialPublicKey?: CredentialPublicKey, opts?: VerifyAssertionOptions): ApprovalResult;
2216
2250
  /**
2217
2251
  * Create the approval receipt payload for embedding in an Acta receipt.
2218
2252
  *