protect-mcp 0.7.5 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8294,7 +8294,7 @@ Usage:
8294
8294
  protect-mcp status [--dir <path>]
8295
8295
  protect-mcp digest [--today] [--dir <path>]
8296
8296
  protect-mcp receipts [--last <n>] [--dir <path>]
8297
- protect-mcp record [--dir <path>] [--no-open]
8297
+ protect-mcp record [--dir <path>] [--live] [--no-open]
8298
8298
  protect-mcp bundle [--output <path>] [--dir <path>]
8299
8299
  protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
8300
8300
  protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
@@ -10236,48 +10236,99 @@ async function handleRecord(argv) {
10236
10236
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
10237
10237
  const recPath = join8(dir, ".protect-mcp-receipts.jsonl");
10238
10238
  const logPath = join8(dir, ".protect-mcp-log.jsonl");
10239
- const chosen = existsSync9(recPath) ? recPath : existsSync9(logPath) ? logPath : null;
10239
+ const pick = () => existsSync9(recPath) ? recPath : existsSync9(logPath) ? logPath : null;
10240
+ const chosen = pick();
10240
10241
  if (!chosen) {
10241
10242
  process.stderr.write(`
10242
10243
  ${bold("protect-mcp record")}
10243
10244
 
10244
10245
  No record found in ${dir}.
10245
10246
  Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
10247
+ `);
10248
+ process.stderr.write(`Tip: run this in the directory where your gate is signing (where .protect-mcp-receipts.jsonl lives), or pass ${bold("--dir <path>")}.
10246
10249
 
10247
10250
  `);
10248
10251
  process.exit(0);
10249
10252
  return;
10250
10253
  }
10251
- const raw = readFileSync11(chosen, "utf-8");
10252
- const recs = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
10254
+ const readRecs = (file) => readFileSync11(file, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
10253
10255
  try {
10254
10256
  return JSON.parse(l);
10255
10257
  } catch {
10256
10258
  return null;
10257
10259
  }
10258
10260
  }).filter((x) => x !== null).map(mapRecordEntry);
10259
- const meta = { file: chosen, signed: chosen === recPath, count: recs.length };
10260
- const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10261
- const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10262
- writeFileSync4(out, html);
10263
- if (!argv.includes("--no-open")) {
10261
+ const openTarget = (target) => {
10262
+ if (argv.includes("--no-open")) return;
10264
10263
  const platform = process.platform;
10265
10264
  const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
10266
- const openArgs = platform === "win32" ? ["/c", "start", "", out] : [out];
10265
+ const openArgs = platform === "win32" ? ["/c", "start", "", target] : [target];
10267
10266
  try {
10268
10267
  const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
10269
10268
  child.unref();
10270
10269
  } catch {
10271
10270
  }
10271
+ };
10272
+ if (argv.includes("--live") || argv.includes("--watch")) {
10273
+ const http = await import("http");
10274
+ const pi = argv.indexOf("--port");
10275
+ const port = pi !== -1 && argv[pi + 1] ? parseInt(argv[pi + 1], 10) : 9378;
10276
+ const server = http.createServer((req, res) => {
10277
+ if (req.url && req.url.indexOf("/data") === 0) {
10278
+ let recs2 = [];
10279
+ const f = pick();
10280
+ try {
10281
+ if (f) recs2 = readRecs(f);
10282
+ } catch {
10283
+ }
10284
+ res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
10285
+ res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
10286
+ return;
10287
+ }
10288
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
10289
+ const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
10290
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
10291
+ res.end(page);
10292
+ });
10293
+ server.on("error", (e) => {
10294
+ process.stderr.write(`
10295
+ protect-mcp record --live: could not start on port ${port}${e && e.code ? ` (${e.code})` : ""}. Try ${bold("--port <n>")}.
10296
+
10297
+ `);
10298
+ process.exit(1);
10299
+ });
10300
+ server.listen(port, "127.0.0.1", () => {
10301
+ const url = `http://127.0.0.1:${port}`;
10302
+ openTarget(url);
10303
+ process.stdout.write(`
10304
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
10305
+ `);
10306
+ process.stdout.write(` Updates as your agent runs. All local, nothing uploaded. ${dim("Ctrl-C to stop.")}
10307
+
10308
+ `);
10309
+ });
10310
+ return;
10272
10311
  }
10312
+ const recs = readRecs(chosen);
10313
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
10314
+ const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10315
+ const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10316
+ writeFileSync4(out, html);
10317
+ openTarget(out);
10273
10318
  process.stdout.write(`
10274
- ${bold("\u{1F6E1}\uFE0F Your record")} ${recs.length} decision${recs.length === 1 ? "" : "s"}
10275
- `);
10276
- process.stdout.write(` Opened a searchable view in your browser. Everything stayed on this machine.
10319
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} ${recs.length} decision${recs.length === 1 ? "" : "s"}, all on this machine
10277
10320
  `);
10278
10321
  if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
10279
10322
  `);
10280
- process.stdout.write(` ${dim("If it did not open, open this file: " + out)}
10323
+ const fileUrl = "file://" + encodeURI(out);
10324
+ if (process.stdout.isTTY) {
10325
+ process.stdout.write(` Opened in your browser. If it did not open, click: \x1B]8;;${fileUrl}\x1B\\${bold("your record")}\x1B]8;;\x1B\\
10326
+ `);
10327
+ } else {
10328
+ process.stdout.write(` Opened in your browser. If it did not open, open: ${out}
10329
+ `);
10330
+ }
10331
+ process.stdout.write(` ${dim("Want it to update live as your agent runs? npx protect-mcp record --live")}
10281
10332
 
10282
10333
  `);
10283
10334
  process.exit(0);
@@ -10323,7 +10374,7 @@ function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString
10323
10374
  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]]})}
10324
10375
  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}
10325
10376
  function render(){
10326
- document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local";
10377
+ document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local"+(META.live?" \xB7 live, updating":"");
10327
10378
  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>"})});
10328
10379
  document.getElementById("chips").innerHTML=chips;
10329
10380
  var rows=RECORDS.filter(match);
@@ -10334,6 +10385,7 @@ document.getElementById("q").addEventListener("input",function(e){Q=e.target.val
10334
10385
  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()});
10335
10386
  document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
10336
10387
  render();
10388
+ if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){RECORDS=d.recs||[];META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;render()}).catch(function(){})};poll();setInterval(poll,2000);}
10337
10389
  </script></body></html>`;
10338
10390
  async function handleBundle(argv) {
10339
10391
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
@@ -11830,7 +11882,7 @@ async function main() {
11830
11882
  }
11831
11883
  if (args[0] === "record") {
11832
11884
  await handleRecord(args.slice(1));
11833
- process.exit(0);
11885
+ return;
11834
11886
  }
11835
11887
  if (args[0] === "init-hooks") {
11836
11888
  await handleInitHooks(args.slice(1));
package/dist/cli.mjs CHANGED
@@ -59,7 +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
+ protect-mcp record [--dir <path>] [--live] [--no-open]
63
63
  protect-mcp bundle [--output <path>] [--dir <path>]
64
64
  protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
65
65
  protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
@@ -2001,48 +2001,99 @@ async function handleRecord(argv) {
2001
2001
  if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
2002
2002
  const recPath = join(dir, ".protect-mcp-receipts.jsonl");
2003
2003
  const logPath = join(dir, ".protect-mcp-log.jsonl");
2004
- const chosen = existsSync(recPath) ? recPath : existsSync(logPath) ? logPath : null;
2004
+ const pick = () => existsSync(recPath) ? recPath : existsSync(logPath) ? logPath : null;
2005
+ const chosen = pick();
2005
2006
  if (!chosen) {
2006
2007
  process.stderr.write(`
2007
2008
  ${bold("protect-mcp record")}
2008
2009
 
2009
2010
  No record found in ${dir}.
2010
2011
  Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
2012
+ `);
2013
+ process.stderr.write(`Tip: run this in the directory where your gate is signing (where .protect-mcp-receipts.jsonl lives), or pass ${bold("--dir <path>")}.
2011
2014
 
2012
2015
  `);
2013
2016
  process.exit(0);
2014
2017
  return;
2015
2018
  }
2016
- const raw = readFileSync(chosen, "utf-8");
2017
- const recs = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
2019
+ const readRecs = (file) => readFileSync(file, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
2018
2020
  try {
2019
2021
  return JSON.parse(l);
2020
2022
  } catch {
2021
2023
  return null;
2022
2024
  }
2023
2025
  }).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")) {
2026
+ const openTarget = (target) => {
2027
+ if (argv.includes("--no-open")) return;
2029
2028
  const platform = process.platform;
2030
2029
  const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
2031
- const openArgs = platform === "win32" ? ["/c", "start", "", out] : [out];
2030
+ const openArgs = platform === "win32" ? ["/c", "start", "", target] : [target];
2032
2031
  try {
2033
2032
  const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
2034
2033
  child.unref();
2035
2034
  } catch {
2036
2035
  }
2036
+ };
2037
+ if (argv.includes("--live") || argv.includes("--watch")) {
2038
+ const http = await import("http");
2039
+ const pi = argv.indexOf("--port");
2040
+ const port = pi !== -1 && argv[pi + 1] ? parseInt(argv[pi + 1], 10) : 9378;
2041
+ const server = http.createServer((req, res) => {
2042
+ if (req.url && req.url.indexOf("/data") === 0) {
2043
+ let recs2 = [];
2044
+ const f = pick();
2045
+ try {
2046
+ if (f) recs2 = readRecs(f);
2047
+ } catch {
2048
+ }
2049
+ res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
2050
+ res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
2051
+ return;
2052
+ }
2053
+ const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
2054
+ const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
2055
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2056
+ res.end(page);
2057
+ });
2058
+ server.on("error", (e) => {
2059
+ process.stderr.write(`
2060
+ protect-mcp record --live: could not start on port ${port}${e && e.code ? ` (${e.code})` : ""}. Try ${bold("--port <n>")}.
2061
+
2062
+ `);
2063
+ process.exit(1);
2064
+ });
2065
+ server.listen(port, "127.0.0.1", () => {
2066
+ const url = `http://127.0.0.1:${port}`;
2067
+ openTarget(url);
2068
+ process.stdout.write(`
2069
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
2070
+ `);
2071
+ process.stdout.write(` Updates as your agent runs. All local, nothing uploaded. ${dim("Ctrl-C to stop.")}
2072
+
2073
+ `);
2074
+ });
2075
+ return;
2037
2076
  }
2077
+ const recs = readRecs(chosen);
2078
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
2079
+ const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
2080
+ const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
2081
+ writeFileSync(out, html);
2082
+ openTarget(out);
2038
2083
  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.
2084
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} ${recs.length} decision${recs.length === 1 ? "" : "s"}, all on this machine
2042
2085
  `);
2043
2086
  if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
2044
2087
  `);
2045
- process.stdout.write(` ${dim("If it did not open, open this file: " + out)}
2088
+ const fileUrl = "file://" + encodeURI(out);
2089
+ if (process.stdout.isTTY) {
2090
+ process.stdout.write(` Opened in your browser. If it did not open, click: \x1B]8;;${fileUrl}\x1B\\${bold("your record")}\x1B]8;;\x1B\\
2091
+ `);
2092
+ } else {
2093
+ process.stdout.write(` Opened in your browser. If it did not open, open: ${out}
2094
+ `);
2095
+ }
2096
+ process.stdout.write(` ${dim("Want it to update live as your agent runs? npx protect-mcp record --live")}
2046
2097
 
2047
2098
  `);
2048
2099
  process.exit(0);
@@ -2088,7 +2139,7 @@ function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString
2088
2139
  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
2140
  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
2141
  function render(){
2091
- document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local";
2142
+ document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local"+(META.live?" \xB7 live, updating":"");
2092
2143
  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
2144
  document.getElementById("chips").innerHTML=chips;
2094
2145
  var rows=RECORDS.filter(match);
@@ -2099,6 +2150,7 @@ document.getElementById("q").addEventListener("input",function(e){Q=e.target.val
2099
2150
  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
2151
  document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
2101
2152
  render();
2153
+ if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){RECORDS=d.recs||[];META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;render()}).catch(function(){})};poll();setInterval(poll,2000);}
2102
2154
  </script></body></html>`;
2103
2155
  async function handleBundle(argv) {
2104
2156
  const { readFileSync, writeFileSync, existsSync } = await import("fs");
@@ -3595,7 +3647,7 @@ async function main() {
3595
3647
  }
3596
3648
  if (args[0] === "record") {
3597
3649
  await handleRecord(args.slice(1));
3598
- process.exit(0);
3650
+ return;
3599
3651
  }
3600
3652
  if (args[0] === "init-hooks") {
3601
3653
  await handleInitHooks(args.slice(1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
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",
@@ -0,0 +1,50 @@
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
+ // );