protect-mcp 0.3.1 → 0.3.3

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.d.ts CHANGED
@@ -36,8 +36,8 @@ type PolicyEngineMode = 'built-in' | 'external' | 'hybrid';
36
36
  interface ExternalPDPConfig {
37
37
  /** HTTP endpoint for the external policy decision point */
38
38
  endpoint: string;
39
- /** Response format: 'opa' | 'cerbos' | 'generic' */
40
- format?: 'opa' | 'cerbos' | 'generic';
39
+ /** Response format: 'opa' | 'cerbos' | 'cedar' | 'generic' */
40
+ format?: 'opa' | 'cerbos' | 'cedar' | 'generic';
41
41
  /** Timeout in milliseconds (default: 500) */
42
42
  timeout_ms?: number;
43
43
  /** Fallback decision when external PDP is unreachable */
@@ -154,6 +154,27 @@ interface ProtectConfig {
154
154
  signing?: SigningConfig;
155
155
  /** Credential vault: maps credential labels to injection config */
156
156
  credentials?: Record<string, CredentialConfig>;
157
+ /** Multi-agent mode: identify calling agents and apply per-agent policy */
158
+ multiAgent?: MultiAgentConfig;
159
+ }
160
+ /**
161
+ * Multi-agent mode configuration.
162
+ *
163
+ * When enabled, protect-mcp resolves the calling agent's passport kid
164
+ * from request metadata (x-passport-kid header or _passport_kid param)
165
+ * and applies agent-specific policy overrides.
166
+ */
167
+ interface MultiAgentConfig {
168
+ /** Enable multi-agent mode */
169
+ enabled: boolean;
170
+ /** Registry endpoint for agent manifest lookup */
171
+ registryUrl?: string;
172
+ /** Per-agent policy overrides: maps kid → tool policy overrides */
173
+ agentPolicies?: Record<string, Record<string, ToolPolicy>>;
174
+ /** Default policy for unrecognized agents (default: use base policy) */
175
+ unknownAgentPolicy?: 'base' | 'deny' | 'shadow-only';
176
+ /** Cache TTL for agent manifests in ms (default: 300000 = 5 min) */
177
+ cacheTtlMs?: number;
157
178
  }
158
179
 
159
180
  /**
@@ -459,7 +480,7 @@ declare function isSigningEnabled(): boolean;
459
480
  * BYOPE (Bring Your Own Policy Engine) — sends decision context
460
481
  * to an external Policy Decision Point via HTTP webhook.
461
482
  *
462
- * Supports OPA, Cerbos, and generic JSON formats.
483
+ * Supports OPA, Cerbos, Cedar (AWS), and generic JSON formats.
463
484
  * ScopeBlind always signs the receipt regardless of who made the decision.
464
485
  *
465
486
  * Sprint 2: One HTTP webhook adapter. More adapters later.
@@ -570,6 +591,132 @@ declare function createAuditBundle(opts: AuditBundleOptions): AuditBundle;
570
591
  */
571
592
  declare function collectSignedReceipts(logs: DecisionLog[]): Record<string, unknown>[];
572
593
 
594
+ /**
595
+ * protect-mcp simulate — dry-run policy evaluation
596
+ *
597
+ * Reads a recorded log file (.protect-mcp-log.jsonl) and evaluates
598
+ * each tool call against a policy file. Shows what would have been
599
+ * blocked, rate-limited, or approved — without wrapping a live server.
600
+ *
601
+ * Usage:
602
+ * npx protect-mcp simulate --policy strict.json [--log .protect-mcp-log.jsonl] [--json]
603
+ */
604
+
605
+ interface LogEntry {
606
+ v: number;
607
+ tool: string;
608
+ decision: string;
609
+ reason_code: string;
610
+ mode: string;
611
+ timestamp: number;
612
+ tier?: string;
613
+ rate_limit_remaining?: number;
614
+ [key: string]: unknown;
615
+ }
616
+ interface SimulationResult {
617
+ tool: string;
618
+ calls: number;
619
+ results: {
620
+ allow: number;
621
+ block: number;
622
+ rate_limited: number;
623
+ require_approval: number;
624
+ tier_insufficient: number;
625
+ };
626
+ original: {
627
+ allow: number;
628
+ deny: number;
629
+ };
630
+ }
631
+ interface SimulationSummary {
632
+ policy_file: string;
633
+ log_file: string;
634
+ total_calls: number;
635
+ results: {
636
+ allow: number;
637
+ block: number;
638
+ rate_limited: number;
639
+ require_approval: number;
640
+ tier_insufficient: number;
641
+ };
642
+ original: {
643
+ allow: number;
644
+ deny: number;
645
+ };
646
+ tool_breakdown: SimulationResult[];
647
+ changes: string[];
648
+ }
649
+ /**
650
+ * Parse a JSONL log file into log entries.
651
+ */
652
+ declare function parseLogFile(path: string): LogEntry[];
653
+ /**
654
+ * Simulate a policy against a set of log entries.
655
+ * Evaluates each entry against the policy's per-tool rules,
656
+ * including block, rate_limit, min_tier, and require_approval.
657
+ */
658
+ declare function simulate(entries: LogEntry[], policy: ProtectPolicy, tier?: TrustTier): SimulationSummary;
659
+ /**
660
+ * Format simulation results for terminal output.
661
+ */
662
+ declare function formatSimulation(summary: SimulationSummary): string;
663
+
664
+ /**
665
+ * protect-mcp report — compliance report generation
666
+ *
667
+ * Generates structured compliance reports from local log and receipt files.
668
+ * Output as JSON (machine-readable) or Markdown (human-readable, PDF-convertible).
669
+ *
670
+ * Usage:
671
+ * npx protect-mcp report --period 30d --output report.json
672
+ * npx protect-mcp report --period 30d --format md --output report.md
673
+ */
674
+ interface ComplianceReport {
675
+ generated_at: string;
676
+ period: {
677
+ from: string;
678
+ to: string;
679
+ };
680
+ signing_identity: {
681
+ kid: string;
682
+ issuer: string;
683
+ } | null;
684
+ summary: {
685
+ total_decisions: number;
686
+ allowed: number;
687
+ blocked: number;
688
+ rate_limited: number;
689
+ approval_required: number;
690
+ unique_tools: number;
691
+ unique_tiers: number;
692
+ };
693
+ tool_breakdown: Array<{
694
+ tool: string;
695
+ total: number;
696
+ allowed: number;
697
+ blocked: number;
698
+ rate_limited: number;
699
+ approval_required: number;
700
+ }>;
701
+ policy_changes: Array<{
702
+ at: string;
703
+ policy_digest: string;
704
+ }>;
705
+ verification: {
706
+ receipts_signed: number;
707
+ receipts_unsigned: number;
708
+ verify_command: string;
709
+ };
710
+ }
711
+ /**
712
+ * Generate a compliance report from local log and receipt files.
713
+ */
714
+ declare function generateReport(logPath: string, receiptPath: string, periodDays: number): ComplianceReport;
715
+ /**
716
+ * Format a compliance report as Markdown.
717
+ */
718
+ declare function formatReportMarkdown(report: ComplianceReport): string;
719
+
573
720
  /**
574
721
  * Agent identity format: sb:agent:{first 32 hex chars of SHA-256(public key bytes)}
575
722
  * Example: "sb:agent:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
@@ -851,4 +998,4 @@ declare function validateManifest(manifest: unknown): string[];
851
998
  */
852
999
  declare function validateEvidenceReceipt(receipt: unknown): string[];
853
1000
 
854
- export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, validateCredentials, validateEvidenceReceipt, validateManifest };
1001
+ export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type ComplianceReport, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type SimulationResult, type SimulationSummary, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, formatReportMarkdown, formatSimulation, generateReport, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseLogFile, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, simulate, validateCredentials, validateEvidenceReceipt, validateManifest };
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;
@@ -522,6 +521,28 @@ function formatRequest(context, format) {
522
521
  },
523
522
  actions: [context.action.operation || "call"]
524
523
  };
524
+ case "cedar":
525
+ return {
526
+ principal: {
527
+ type: "Agent",
528
+ id: context.actor.id || "unknown"
529
+ },
530
+ action: {
531
+ type: "Action",
532
+ id: `MCP::Tool::${context.action.operation || "call"}`
533
+ },
534
+ resource: {
535
+ type: "Tool",
536
+ id: context.action.tool
537
+ },
538
+ context: {
539
+ tier: context.actor.tier,
540
+ manifest_hash: context.actor.manifest_hash || null,
541
+ service: context.target.service || "default",
542
+ mode: context.mode,
543
+ credential_ref: context.credential_ref || null
544
+ }
545
+ };
525
546
  case "generic":
526
547
  default:
527
548
  return context;
@@ -551,6 +572,22 @@ function parseResponse(result, format) {
551
572
  }
552
573
  }
553
574
  return { allowed: false, reason: "unrecognized Cerbos response" };
575
+ case "cedar":
576
+ if (typeof result.decision === "string") {
577
+ return {
578
+ allowed: result.decision === "Allow",
579
+ reason: result.decision === "Deny" ? `cedar_deny${result.diagnostics ? ": " + JSON.stringify(result.diagnostics) : ""}` : void 0,
580
+ metadata: result.diagnostics
581
+ };
582
+ }
583
+ if (Array.isArray(result.results) && result.results.length > 0) {
584
+ const first = result.results[0];
585
+ return {
586
+ allowed: first.decision === "Allow",
587
+ reason: first.decision === "Deny" ? "cedar_deny" : void 0
588
+ };
589
+ }
590
+ return { allowed: false, reason: "unrecognized Cedar response" };
554
591
  case "generic":
555
592
  default:
556
593
  return {
@@ -968,8 +1005,31 @@ var ProtectGateway = class {
968
1005
  async interceptToolCall(request) {
969
1006
  const toolName = request.params?.name || "unknown";
970
1007
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
971
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
972
1008
  const mode = this.config.enforce ? "enforce" : "shadow";
1009
+ let resolvedAgentKid = this.admissionResult?.agent_id;
1010
+ let effectiveToolPolicy;
1011
+ if (this.config.multiAgent?.enabled) {
1012
+ const paramKid = request.params?._passport_kid;
1013
+ if (paramKid) resolvedAgentKid = paramKid;
1014
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
1015
+ if (agentOverrides && agentOverrides[toolName]) {
1016
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
1017
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
1018
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
1019
+ if (this.config.enforce) {
1020
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
1021
+ }
1022
+ return null;
1023
+ } else {
1024
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
1025
+ }
1026
+ if (this.config.verbose && resolvedAgentKid) {
1027
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
1028
+ }
1029
+ } else {
1030
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
1031
+ }
1032
+ const toolPolicy = effectiveToolPolicy;
973
1033
  let credentialRef;
974
1034
  if (this.config.credentials) {
975
1035
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -1193,6 +1253,298 @@ function collectSignedReceipts(logs) {
1193
1253
  }).filter((r) => typeof r.signature === "string");
1194
1254
  }
1195
1255
 
1256
+ // src/simulate.ts
1257
+ var import_node_fs6 = require("fs");
1258
+ function parseLogFile(path) {
1259
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
1260
+ const entries = [];
1261
+ for (const line of raw.split("\n")) {
1262
+ const trimmed = line.trim();
1263
+ if (!trimmed) continue;
1264
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1265
+ try {
1266
+ const parsed = JSON.parse(jsonStr);
1267
+ if (parsed.tool && parsed.decision) {
1268
+ entries.push(parsed);
1269
+ }
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ return entries;
1274
+ }
1275
+ function simulate(entries, policy, tier = "unknown") {
1276
+ const rateLimitStore = /* @__PURE__ */ new Map();
1277
+ const toolResults = /* @__PURE__ */ new Map();
1278
+ const totals = {
1279
+ allow: 0,
1280
+ block: 0,
1281
+ rate_limited: 0,
1282
+ require_approval: 0,
1283
+ tier_insufficient: 0
1284
+ };
1285
+ const originalTotals = { allow: 0, deny: 0 };
1286
+ const changes = [];
1287
+ for (const entry of entries) {
1288
+ const toolName = entry.tool;
1289
+ const toolPolicy = getToolPolicy(toolName, policy);
1290
+ if (entry.decision === "allow") {
1291
+ originalTotals.allow++;
1292
+ } else {
1293
+ originalTotals.deny++;
1294
+ }
1295
+ let newDecision;
1296
+ if (toolPolicy.block) {
1297
+ newDecision = "block";
1298
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
1299
+ newDecision = "tier_insufficient";
1300
+ } else if (toolPolicy.require_approval) {
1301
+ newDecision = "require_approval";
1302
+ } else if (toolPolicy.rate_limit) {
1303
+ const limit = parseRateLimit(toolPolicy.rate_limit);
1304
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
1305
+ newDecision = result.allowed ? "allow" : "rate_limited";
1306
+ } else {
1307
+ newDecision = "allow";
1308
+ }
1309
+ totals[newDecision]++;
1310
+ if (!toolResults.has(toolName)) {
1311
+ toolResults.set(toolName, {
1312
+ tool: toolName,
1313
+ calls: 0,
1314
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
1315
+ original: { allow: 0, deny: 0 }
1316
+ });
1317
+ }
1318
+ const tr = toolResults.get(toolName);
1319
+ tr.calls++;
1320
+ tr.results[newDecision]++;
1321
+ if (entry.decision === "allow") {
1322
+ tr.original.allow++;
1323
+ } else {
1324
+ tr.original.deny++;
1325
+ }
1326
+ }
1327
+ for (const [tool, result] of toolResults) {
1328
+ const wasAllBlocked = result.original.allow === 0;
1329
+ const nowAllBlocked = result.results.allow === 0;
1330
+ const wasAllAllowed = result.original.deny === 0;
1331
+ if (wasAllAllowed && result.results.block > 0) {
1332
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
1333
+ }
1334
+ if (wasAllAllowed && result.results.rate_limited > 0) {
1335
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
1336
+ }
1337
+ if (wasAllAllowed && result.results.require_approval > 0) {
1338
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
1339
+ }
1340
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
1341
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
1342
+ }
1343
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
1344
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
1345
+ }
1346
+ }
1347
+ return {
1348
+ policy_file: "",
1349
+ log_file: "",
1350
+ total_calls: entries.length,
1351
+ results: totals,
1352
+ original: originalTotals,
1353
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
1354
+ changes
1355
+ };
1356
+ }
1357
+ function formatSimulation(summary) {
1358
+ const lines = [];
1359
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
1360
+ `);
1361
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
1362
+ for (const tr of summary.tool_breakdown) {
1363
+ const parts = [];
1364
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
1365
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
1366
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
1367
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
1368
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
1369
+ const originalParts = [];
1370
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
1371
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
1372
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
1373
+ }
1374
+ lines.push("");
1375
+ 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`);
1376
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
1377
+ if (summary.changes.length > 0) {
1378
+ lines.push("");
1379
+ lines.push("Changes:");
1380
+ for (const change of summary.changes) {
1381
+ lines.push(` \u2022 ${change}`);
1382
+ }
1383
+ }
1384
+ return lines.join("\n");
1385
+ }
1386
+
1387
+ // src/report.ts
1388
+ var import_node_fs7 = require("fs");
1389
+ function generateReport(logPath, receiptPath, periodDays) {
1390
+ const now = /* @__PURE__ */ new Date();
1391
+ const from = new Date(now.getTime() - periodDays * 864e5);
1392
+ const entries = [];
1393
+ if ((0, import_node_fs7.existsSync)(logPath)) {
1394
+ const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
1395
+ for (const line of raw.split("\n")) {
1396
+ const trimmed = line.trim();
1397
+ if (!trimmed) continue;
1398
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1399
+ try {
1400
+ const parsed = JSON.parse(jsonStr);
1401
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
1402
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
1403
+ if (entryTime >= from.getTime()) {
1404
+ entries.push(parsed);
1405
+ }
1406
+ }
1407
+ } catch {
1408
+ }
1409
+ }
1410
+ }
1411
+ let receiptsSigned = 0;
1412
+ let signerKid = "";
1413
+ let signerIssuer = "";
1414
+ if ((0, import_node_fs7.existsSync)(receiptPath)) {
1415
+ const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
1416
+ for (const line of raw.split("\n")) {
1417
+ const trimmed = line.trim();
1418
+ if (!trimmed) continue;
1419
+ try {
1420
+ const parsed = JSON.parse(trimmed);
1421
+ if (parsed.signature) {
1422
+ receiptsSigned++;
1423
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
1424
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
1425
+ }
1426
+ } catch {
1427
+ }
1428
+ }
1429
+ }
1430
+ const toolMap = /* @__PURE__ */ new Map();
1431
+ const tiers = /* @__PURE__ */ new Set();
1432
+ const policyDigests = /* @__PURE__ */ new Map();
1433
+ let allowed = 0;
1434
+ let blocked = 0;
1435
+ let rateLimited = 0;
1436
+ let approvalRequired = 0;
1437
+ for (const entry of entries) {
1438
+ const tool = entry.tool;
1439
+ if (!toolMap.has(tool)) {
1440
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
1441
+ }
1442
+ const tm = toolMap.get(tool);
1443
+ tm.total++;
1444
+ if (entry.decision === "allow") {
1445
+ allowed++;
1446
+ tm.allowed++;
1447
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
1448
+ rateLimited++;
1449
+ tm.rate_limited++;
1450
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
1451
+ approvalRequired++;
1452
+ tm.approval_required++;
1453
+ } else {
1454
+ blocked++;
1455
+ tm.blocked++;
1456
+ }
1457
+ if (entry.tier) tiers.add(entry.tier);
1458
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
1459
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
1460
+ }
1461
+ }
1462
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
1463
+ at,
1464
+ policy_digest: digest
1465
+ })).sort((a, b) => a.at.localeCompare(b.at));
1466
+ return {
1467
+ generated_at: now.toISOString(),
1468
+ period: { from: from.toISOString(), to: now.toISOString() },
1469
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
1470
+ summary: {
1471
+ total_decisions: entries.length,
1472
+ allowed,
1473
+ blocked,
1474
+ rate_limited: rateLimited,
1475
+ approval_required: approvalRequired,
1476
+ unique_tools: toolMap.size,
1477
+ unique_tiers: tiers.size
1478
+ },
1479
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
1480
+ policy_changes: policyChanges,
1481
+ verification: {
1482
+ receipts_signed: receiptsSigned,
1483
+ receipts_unsigned: entries.length - receiptsSigned,
1484
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
1485
+ }
1486
+ };
1487
+ }
1488
+ function formatReportMarkdown(report) {
1489
+ const lines = [];
1490
+ lines.push("# ScopeBlind Compliance Report");
1491
+ lines.push("");
1492
+ lines.push(`**Generated:** ${report.generated_at}`);
1493
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
1494
+ if (report.signing_identity) {
1495
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
1496
+ }
1497
+ lines.push("");
1498
+ lines.push("## Summary");
1499
+ lines.push("");
1500
+ lines.push(`| Metric | Value |`);
1501
+ lines.push(`|--------|-------|`);
1502
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
1503
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
1504
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
1505
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
1506
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
1507
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
1508
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
1509
+ lines.push("");
1510
+ if (report.tool_breakdown.length > 0) {
1511
+ lines.push("## Tool Breakdown");
1512
+ lines.push("");
1513
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
1514
+ lines.push("|------|-------|---------|---------|--------------|----------|");
1515
+ for (const t of report.tool_breakdown) {
1516
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
1517
+ }
1518
+ lines.push("");
1519
+ }
1520
+ if (report.policy_changes.length > 0) {
1521
+ lines.push("## Policy History");
1522
+ lines.push("");
1523
+ lines.push("| Timestamp | Policy Digest |");
1524
+ lines.push("|-----------|--------------|");
1525
+ for (const pc of report.policy_changes) {
1526
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
1527
+ }
1528
+ lines.push("");
1529
+ }
1530
+ lines.push("## Verification");
1531
+ lines.push("");
1532
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
1533
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
1534
+ lines.push("");
1535
+ lines.push("Verify the audit bundle:");
1536
+ lines.push("");
1537
+ lines.push("```bash");
1538
+ lines.push(report.verification.verify_command);
1539
+ lines.push("```");
1540
+ lines.push("");
1541
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
1542
+ lines.push("");
1543
+ lines.push("---");
1544
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
1545
+ return lines.join("\n");
1546
+ }
1547
+
1196
1548
  // src/manifest.ts
1197
1549
  function isAgentId(s) {
1198
1550
  return /^sb:agent:[a-f0-9]{32}$/.test(s);
@@ -1355,6 +1707,9 @@ function validateEvidenceReceipt(receipt) {
1355
1707
  collectSignedReceipts,
1356
1708
  createAuditBundle,
1357
1709
  evaluateTier,
1710
+ formatReportMarkdown,
1711
+ formatSimulation,
1712
+ generateReport,
1358
1713
  getSignerInfo,
1359
1714
  getToolPolicy,
1360
1715
  initSigning,
@@ -1366,10 +1721,12 @@ function validateEvidenceReceipt(receipt) {
1366
1721
  listCredentialLabels,
1367
1722
  loadPolicy,
1368
1723
  meetsMinTier,
1724
+ parseLogFile,
1369
1725
  parseRateLimit,
1370
1726
  queryExternalPDP,
1371
1727
  resolveCredential,
1372
1728
  signDecision,
1729
+ simulate,
1373
1730
  validateCredentials,
1374
1731
  validateEvidenceReceipt,
1375
1732
  validateManifest