protect-mcp 0.3.0 → 0.3.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/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
@@ -36,6 +26,9 @@ __export(index_exports, {
36
26
  collectSignedReceipts: () => collectSignedReceipts,
37
27
  createAuditBundle: () => createAuditBundle,
38
28
  evaluateTier: () => evaluateTier,
29
+ formatReportMarkdown: () => formatReportMarkdown,
30
+ formatSimulation: () => formatSimulation,
31
+ generateReport: () => generateReport,
39
32
  getSignerInfo: () => getSignerInfo,
40
33
  getToolPolicy: () => getToolPolicy,
41
34
  initSigning: () => initSigning,
@@ -47,10 +40,12 @@ __export(index_exports, {
47
40
  listCredentialLabels: () => listCredentialLabels,
48
41
  loadPolicy: () => loadPolicy,
49
42
  meetsMinTier: () => meetsMinTier,
43
+ parseLogFile: () => parseLogFile,
50
44
  parseRateLimit: () => parseRateLimit,
51
45
  queryExternalPDP: () => queryExternalPDP,
52
46
  resolveCredential: () => resolveCredential,
53
47
  signDecision: () => signDecision,
48
+ simulate: () => simulate,
54
49
  validateCredentials: () => validateCredentials,
55
50
  validateEvidenceReceipt: () => validateEvidenceReceipt,
56
51
  validateManifest: () => validateManifest
@@ -383,7 +378,11 @@ async function initSigning(config) {
383
378
  return warnings;
384
379
  }
385
380
  try {
386
- artifactsModule = await import("@veritasacta/artifacts");
381
+ const moduleName = "@veritasacta/artifacts";
382
+ artifactsModule = await import(
383
+ /* @vite-ignore */
384
+ moduleName
385
+ );
387
386
  } catch {
388
387
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
389
388
  return warnings;
@@ -402,8 +401,8 @@ async function initSigning(config) {
402
401
  signerState = {
403
402
  privateKey: keyData.privateKey,
404
403
  publicKey: keyData.publicKey,
405
- kid: artifactsModule.computeKid(keyData.publicKey),
406
- issuer: config.issuer || "protect-mcp"
404
+ kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
405
+ issuer: config.issuer || keyData.issuer || "protect-mcp"
407
406
  };
408
407
  } catch (err) {
409
408
  warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
@@ -615,13 +614,16 @@ var ReceiptBuffer = class {
615
614
  count() {
616
615
  return this.receipts.length;
617
616
  }
617
+ getLatest() {
618
+ return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
619
+ }
618
620
  };
619
- function startStatusServer(config, receiptBuffer) {
621
+ function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
620
622
  const startTime = Date.now();
621
623
  const logDir = process.cwd();
622
624
  const server = (0, import_node_http.createServer)((req, res) => {
623
625
  res.setHeader("Access-Control-Allow-Origin", "*");
624
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
626
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
625
627
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
626
628
  res.setHeader("Content-Type", "application/json");
627
629
  if (req.method === "OPTIONS") {
@@ -638,18 +640,30 @@ function startStatusServer(config, receiptBuffer) {
638
640
  handleStatus(res, logDir);
639
641
  } else if (path === "/receipts") {
640
642
  handleReceipts(res, receiptBuffer, url);
643
+ } else if (path === "/receipts/latest") {
644
+ handleReceiptLatest(res, receiptBuffer);
641
645
  } else if (path.startsWith("/receipts/")) {
642
646
  const id = path.slice("/receipts/".length);
643
647
  handleReceiptById(res, receiptBuffer, id);
648
+ } else if (path === "/approve" && req.method === "POST") {
649
+ handleApprove(req, res, approvalStore, approvalNonce);
650
+ } else if (path === "/approvals" && req.method === "GET") {
651
+ handleListApprovals(res, approvalStore);
644
652
  } else {
645
653
  res.writeHead(404);
646
- res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
654
+ res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
647
655
  }
648
656
  } catch (err) {
649
657
  res.writeHead(500);
650
658
  res.end(JSON.stringify({ error: "internal_error" }));
651
659
  }
652
660
  });
661
+ server.on("error", (err) => {
662
+ if (config.verbose) {
663
+ process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
664
+ `);
665
+ }
666
+ });
653
667
  server.listen(config.port, "127.0.0.1", () => {
654
668
  if (config.verbose) {
655
669
  process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
@@ -665,7 +679,7 @@ function handleHealth(res, startTime, config) {
665
679
  status: "ok",
666
680
  uptime_ms: Date.now() - startTime,
667
681
  mode: config.mode,
668
- version: "0.3.0"
682
+ version: "0.3.1"
669
683
  }));
670
684
  }
671
685
  function handleStatus(res, logDir) {
@@ -714,6 +728,16 @@ function handleReceipts(res, buffer, url) {
714
728
  receipts
715
729
  }));
716
730
  }
731
+ function handleReceiptLatest(res, buffer) {
732
+ const latest = buffer.getLatest();
733
+ if (!latest) {
734
+ res.writeHead(404);
735
+ res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
736
+ return;
737
+ }
738
+ res.writeHead(200);
739
+ res.end(JSON.stringify(latest));
740
+ }
717
741
  function handleReceiptById(res, buffer, id) {
718
742
  const receipt = buffer.getById(id);
719
743
  if (!receipt) {
@@ -724,22 +748,92 @@ function handleReceiptById(res, buffer, id) {
724
748
  res.writeHead(200);
725
749
  res.end(JSON.stringify(receipt));
726
750
  }
751
+ function handleApprove(req, res, approvalStore, expectedNonce) {
752
+ if (!approvalStore) {
753
+ res.writeHead(503);
754
+ res.end(JSON.stringify({ error: "approval_store_not_available" }));
755
+ return;
756
+ }
757
+ let body = "";
758
+ req.on("data", (chunk) => {
759
+ body += chunk.toString();
760
+ });
761
+ req.on("end", () => {
762
+ try {
763
+ const { request_id, tool, mode, nonce } = JSON.parse(body);
764
+ if (expectedNonce && nonce !== expectedNonce) {
765
+ res.writeHead(403);
766
+ res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
767
+ return;
768
+ }
769
+ if (!tool || typeof tool !== "string") {
770
+ res.writeHead(400);
771
+ res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
772
+ return;
773
+ }
774
+ const grantMode = mode === "always" ? "always" : "once";
775
+ const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
776
+ const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
777
+ if (grantMode === "always") {
778
+ approvalStore.set(`always:${tool}`, grantEntry);
779
+ } else if (request_id) {
780
+ approvalStore.set(request_id, grantEntry);
781
+ } else {
782
+ approvalStore.set(tool, grantEntry);
783
+ }
784
+ res.writeHead(200);
785
+ res.end(JSON.stringify({
786
+ approved: true,
787
+ request_id: request_id || null,
788
+ tool,
789
+ mode: grantMode,
790
+ expires_in_seconds: ttlMs / 1e3
791
+ }));
792
+ } catch {
793
+ res.writeHead(400);
794
+ res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
795
+ }
796
+ });
797
+ }
798
+ function handleListApprovals(res, approvalStore) {
799
+ if (!approvalStore) {
800
+ res.writeHead(200);
801
+ res.end(JSON.stringify({ grants: [] }));
802
+ return;
803
+ }
804
+ const now = Date.now();
805
+ const grants = [];
806
+ for (const [key, grant] of approvalStore) {
807
+ if (now < grant.expires_at) {
808
+ grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
809
+ }
810
+ }
811
+ res.writeHead(200);
812
+ res.end(JSON.stringify({ grants }));
813
+ }
727
814
 
728
815
  // src/gateway.ts
729
816
  var LOG_FILE2 = ".protect-mcp-log.jsonl";
817
+ var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
730
818
  var ProtectGateway = class {
731
819
  child = null;
732
820
  config;
733
821
  rateLimitStore = /* @__PURE__ */ new Map();
734
822
  clientReader = null;
735
823
  logFilePath;
824
+ receiptFilePath;
736
825
  evidenceStore;
737
826
  receiptBuffer;
827
+ /** Approval grants keyed by request_id (scoped to the specific action that was requested) */
828
+ approvalStore = /* @__PURE__ */ new Map();
829
+ /** Random nonce generated at startup — required for approval endpoint authentication */
830
+ approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
738
831
  currentTier = "unknown";
739
832
  admissionResult = null;
740
833
  constructor(config) {
741
834
  this.config = config;
742
835
  this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
836
+ this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
743
837
  this.evidenceStore = new EvidenceStore();
744
838
  this.receiptBuffer = new ReceiptBuffer();
745
839
  }
@@ -763,12 +857,15 @@ var ProtectGateway = class {
763
857
  this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
764
858
  }
765
859
  }
860
+ this.log(`Approval nonce: ${this.approvalNonce}`);
766
861
  const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
767
862
  if (httpPort > 0) {
768
863
  try {
769
864
  startStatusServer(
770
865
  { port: httpPort, mode, verbose },
771
- this.receiptBuffer
866
+ this.receiptBuffer,
867
+ this.approvalStore,
868
+ this.approvalNonce
772
869
  );
773
870
  } catch {
774
871
  if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
@@ -870,8 +967,31 @@ var ProtectGateway = class {
870
967
  async interceptToolCall(request) {
871
968
  const toolName = request.params?.name || "unknown";
872
969
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
873
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
874
970
  const mode = this.config.enforce ? "enforce" : "shadow";
971
+ let resolvedAgentKid = this.admissionResult?.agent_id;
972
+ let effectiveToolPolicy;
973
+ if (this.config.multiAgent?.enabled) {
974
+ const paramKid = request.params?._passport_kid;
975
+ if (paramKid) resolvedAgentKid = paramKid;
976
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
977
+ if (agentOverrides && agentOverrides[toolName]) {
978
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
979
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
980
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
981
+ if (this.config.enforce) {
982
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
983
+ }
984
+ return null;
985
+ } else {
986
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
987
+ }
988
+ if (this.config.verbose && resolvedAgentKid) {
989
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
990
+ }
991
+ } else {
992
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
993
+ }
994
+ const toolPolicy = effectiveToolPolicy;
875
995
  let credentialRef;
876
996
  if (this.config.credentials) {
877
997
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -922,6 +1042,32 @@ var ProtectGateway = class {
922
1042
  }
923
1043
  return null;
924
1044
  }
1045
+ if (toolPolicy.require_approval) {
1046
+ const grant = this.approvalStore.get(requestId);
1047
+ const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
1048
+ if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
1049
+ if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
1050
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
1051
+ return null;
1052
+ }
1053
+ this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
1054
+ if (this.config.enforce) {
1055
+ return {
1056
+ jsonrpc: "2.0",
1057
+ id: request.id,
1058
+ result: {
1059
+ content: [
1060
+ {
1061
+ type: "text",
1062
+ text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
1063
+ }
1064
+ ],
1065
+ isError: true
1066
+ }
1067
+ };
1068
+ }
1069
+ return null;
1070
+ }
925
1071
  const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
926
1072
  if (rateSpec) {
927
1073
  try {
@@ -979,6 +1125,10 @@ var ProtectGateway = class {
979
1125
  if (signed.signed) {
980
1126
  process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
981
1127
  `);
1128
+ try {
1129
+ (0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
1130
+ } catch {
1131
+ }
982
1132
  this.receiptBuffer.add(log.request_id, signed.signed);
983
1133
  if (this.admissionResult?.agent_id) {
984
1134
  this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
@@ -1065,6 +1215,298 @@ function collectSignedReceipts(logs) {
1065
1215
  }).filter((r) => typeof r.signature === "string");
1066
1216
  }
1067
1217
 
1218
+ // src/simulate.ts
1219
+ var import_node_fs6 = require("fs");
1220
+ function parseLogFile(path) {
1221
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
1222
+ const entries = [];
1223
+ for (const line of raw.split("\n")) {
1224
+ const trimmed = line.trim();
1225
+ if (!trimmed) continue;
1226
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1227
+ try {
1228
+ const parsed = JSON.parse(jsonStr);
1229
+ if (parsed.tool && parsed.decision) {
1230
+ entries.push(parsed);
1231
+ }
1232
+ } catch {
1233
+ }
1234
+ }
1235
+ return entries;
1236
+ }
1237
+ function simulate(entries, policy, tier = "unknown") {
1238
+ const rateLimitStore = /* @__PURE__ */ new Map();
1239
+ const toolResults = /* @__PURE__ */ new Map();
1240
+ const totals = {
1241
+ allow: 0,
1242
+ block: 0,
1243
+ rate_limited: 0,
1244
+ require_approval: 0,
1245
+ tier_insufficient: 0
1246
+ };
1247
+ const originalTotals = { allow: 0, deny: 0 };
1248
+ const changes = [];
1249
+ for (const entry of entries) {
1250
+ const toolName = entry.tool;
1251
+ const toolPolicy = getToolPolicy(toolName, policy);
1252
+ if (entry.decision === "allow") {
1253
+ originalTotals.allow++;
1254
+ } else {
1255
+ originalTotals.deny++;
1256
+ }
1257
+ let newDecision;
1258
+ if (toolPolicy.block) {
1259
+ newDecision = "block";
1260
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
1261
+ newDecision = "tier_insufficient";
1262
+ } else if (toolPolicy.require_approval) {
1263
+ newDecision = "require_approval";
1264
+ } else if (toolPolicy.rate_limit) {
1265
+ const limit = parseRateLimit(toolPolicy.rate_limit);
1266
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
1267
+ newDecision = result.allowed ? "allow" : "rate_limited";
1268
+ } else {
1269
+ newDecision = "allow";
1270
+ }
1271
+ totals[newDecision]++;
1272
+ if (!toolResults.has(toolName)) {
1273
+ toolResults.set(toolName, {
1274
+ tool: toolName,
1275
+ calls: 0,
1276
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
1277
+ original: { allow: 0, deny: 0 }
1278
+ });
1279
+ }
1280
+ const tr = toolResults.get(toolName);
1281
+ tr.calls++;
1282
+ tr.results[newDecision]++;
1283
+ if (entry.decision === "allow") {
1284
+ tr.original.allow++;
1285
+ } else {
1286
+ tr.original.deny++;
1287
+ }
1288
+ }
1289
+ for (const [tool, result] of toolResults) {
1290
+ const wasAllBlocked = result.original.allow === 0;
1291
+ const nowAllBlocked = result.results.allow === 0;
1292
+ const wasAllAllowed = result.original.deny === 0;
1293
+ if (wasAllAllowed && result.results.block > 0) {
1294
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
1295
+ }
1296
+ if (wasAllAllowed && result.results.rate_limited > 0) {
1297
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
1298
+ }
1299
+ if (wasAllAllowed && result.results.require_approval > 0) {
1300
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
1301
+ }
1302
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
1303
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
1304
+ }
1305
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
1306
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
1307
+ }
1308
+ }
1309
+ return {
1310
+ policy_file: "",
1311
+ log_file: "",
1312
+ total_calls: entries.length,
1313
+ results: totals,
1314
+ original: originalTotals,
1315
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
1316
+ changes
1317
+ };
1318
+ }
1319
+ function formatSimulation(summary) {
1320
+ const lines = [];
1321
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
1322
+ `);
1323
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
1324
+ for (const tr of summary.tool_breakdown) {
1325
+ const parts = [];
1326
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
1327
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
1328
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
1329
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
1330
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
1331
+ const originalParts = [];
1332
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
1333
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
1334
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
1335
+ }
1336
+ lines.push("");
1337
+ lines.push(`Summary: ${summary.results.allow} allow, ${summary.results.block} blocked, ${summary.results.rate_limited} rate_limited, ${summary.results.require_approval} require_approval, ${summary.results.tier_insufficient} tier_insufficient`);
1338
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
1339
+ if (summary.changes.length > 0) {
1340
+ lines.push("");
1341
+ lines.push("Changes:");
1342
+ for (const change of summary.changes) {
1343
+ lines.push(` \u2022 ${change}`);
1344
+ }
1345
+ }
1346
+ return lines.join("\n");
1347
+ }
1348
+
1349
+ // src/report.ts
1350
+ var import_node_fs7 = require("fs");
1351
+ function generateReport(logPath, receiptPath, periodDays) {
1352
+ const now = /* @__PURE__ */ new Date();
1353
+ const from = new Date(now.getTime() - periodDays * 864e5);
1354
+ const entries = [];
1355
+ if ((0, import_node_fs7.existsSync)(logPath)) {
1356
+ const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
1357
+ for (const line of raw.split("\n")) {
1358
+ const trimmed = line.trim();
1359
+ if (!trimmed) continue;
1360
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1361
+ try {
1362
+ const parsed = JSON.parse(jsonStr);
1363
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
1364
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
1365
+ if (entryTime >= from.getTime()) {
1366
+ entries.push(parsed);
1367
+ }
1368
+ }
1369
+ } catch {
1370
+ }
1371
+ }
1372
+ }
1373
+ let receiptsSigned = 0;
1374
+ let signerKid = "";
1375
+ let signerIssuer = "";
1376
+ if ((0, import_node_fs7.existsSync)(receiptPath)) {
1377
+ const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
1378
+ for (const line of raw.split("\n")) {
1379
+ const trimmed = line.trim();
1380
+ if (!trimmed) continue;
1381
+ try {
1382
+ const parsed = JSON.parse(trimmed);
1383
+ if (parsed.signature) {
1384
+ receiptsSigned++;
1385
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
1386
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
1387
+ }
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ }
1392
+ const toolMap = /* @__PURE__ */ new Map();
1393
+ const tiers = /* @__PURE__ */ new Set();
1394
+ const policyDigests = /* @__PURE__ */ new Map();
1395
+ let allowed = 0;
1396
+ let blocked = 0;
1397
+ let rateLimited = 0;
1398
+ let approvalRequired = 0;
1399
+ for (const entry of entries) {
1400
+ const tool = entry.tool;
1401
+ if (!toolMap.has(tool)) {
1402
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
1403
+ }
1404
+ const tm = toolMap.get(tool);
1405
+ tm.total++;
1406
+ if (entry.decision === "allow") {
1407
+ allowed++;
1408
+ tm.allowed++;
1409
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
1410
+ rateLimited++;
1411
+ tm.rate_limited++;
1412
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
1413
+ approvalRequired++;
1414
+ tm.approval_required++;
1415
+ } else {
1416
+ blocked++;
1417
+ tm.blocked++;
1418
+ }
1419
+ if (entry.tier) tiers.add(entry.tier);
1420
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
1421
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
1422
+ }
1423
+ }
1424
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
1425
+ at,
1426
+ policy_digest: digest
1427
+ })).sort((a, b) => a.at.localeCompare(b.at));
1428
+ return {
1429
+ generated_at: now.toISOString(),
1430
+ period: { from: from.toISOString(), to: now.toISOString() },
1431
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
1432
+ summary: {
1433
+ total_decisions: entries.length,
1434
+ allowed,
1435
+ blocked,
1436
+ rate_limited: rateLimited,
1437
+ approval_required: approvalRequired,
1438
+ unique_tools: toolMap.size,
1439
+ unique_tiers: tiers.size
1440
+ },
1441
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
1442
+ policy_changes: policyChanges,
1443
+ verification: {
1444
+ receipts_signed: receiptsSigned,
1445
+ receipts_unsigned: entries.length - receiptsSigned,
1446
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
1447
+ }
1448
+ };
1449
+ }
1450
+ function formatReportMarkdown(report) {
1451
+ const lines = [];
1452
+ lines.push("# ScopeBlind Compliance Report");
1453
+ lines.push("");
1454
+ lines.push(`**Generated:** ${report.generated_at}`);
1455
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
1456
+ if (report.signing_identity) {
1457
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
1458
+ }
1459
+ lines.push("");
1460
+ lines.push("## Summary");
1461
+ lines.push("");
1462
+ lines.push(`| Metric | Value |`);
1463
+ lines.push(`|--------|-------|`);
1464
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
1465
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
1466
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
1467
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
1468
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
1469
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
1470
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
1471
+ lines.push("");
1472
+ if (report.tool_breakdown.length > 0) {
1473
+ lines.push("## Tool Breakdown");
1474
+ lines.push("");
1475
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
1476
+ lines.push("|------|-------|---------|---------|--------------|----------|");
1477
+ for (const t of report.tool_breakdown) {
1478
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
1479
+ }
1480
+ lines.push("");
1481
+ }
1482
+ if (report.policy_changes.length > 0) {
1483
+ lines.push("## Policy History");
1484
+ lines.push("");
1485
+ lines.push("| Timestamp | Policy Digest |");
1486
+ lines.push("|-----------|--------------|");
1487
+ for (const pc of report.policy_changes) {
1488
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
1489
+ }
1490
+ lines.push("");
1491
+ }
1492
+ lines.push("## Verification");
1493
+ lines.push("");
1494
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
1495
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
1496
+ lines.push("");
1497
+ lines.push("Verify the audit bundle:");
1498
+ lines.push("");
1499
+ lines.push("```bash");
1500
+ lines.push(report.verification.verify_command);
1501
+ lines.push("```");
1502
+ lines.push("");
1503
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
1504
+ lines.push("");
1505
+ lines.push("---");
1506
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
1507
+ return lines.join("\n");
1508
+ }
1509
+
1068
1510
  // src/manifest.ts
1069
1511
  function isAgentId(s) {
1070
1512
  return /^sb:agent:[a-f0-9]{32}$/.test(s);
@@ -1227,6 +1669,9 @@ function validateEvidenceReceipt(receipt) {
1227
1669
  collectSignedReceipts,
1228
1670
  createAuditBundle,
1229
1671
  evaluateTier,
1672
+ formatReportMarkdown,
1673
+ formatSimulation,
1674
+ generateReport,
1230
1675
  getSignerInfo,
1231
1676
  getToolPolicy,
1232
1677
  initSigning,
@@ -1238,10 +1683,12 @@ function validateEvidenceReceipt(receipt) {
1238
1683
  listCredentialLabels,
1239
1684
  loadPolicy,
1240
1685
  meetsMinTier,
1686
+ parseLogFile,
1241
1687
  parseRateLimit,
1242
1688
  queryExternalPDP,
1243
1689
  resolveCredential,
1244
1690
  signDecision,
1691
+ simulate,
1245
1692
  validateCredentials,
1246
1693
  validateEvidenceReceipt,
1247
1694
  validateManifest