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 +68 -16
- package/dist/cli.mjs +68 -16
- package/package.json +1 -1
- package/policies/agent.cedar +50 -0
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
|
|
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
|
|
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
|
|
10260
|
-
|
|
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", "",
|
|
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")}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2025
|
-
|
|
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", "",
|
|
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")}
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
// );
|