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/cli.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ProtectGateway,
4
+ formatSimulation,
4
5
  initSigning,
5
6
  loadPolicy,
7
+ parseLogFile,
8
+ simulate,
6
9
  validateCredentials
7
- } from "./chunk-U7TMVD3E.mjs";
10
+ } from "./chunk-WDCPUM2O.mjs";
8
11
 
9
12
  // src/cli.ts
10
13
  function printHelp() {
@@ -13,12 +16,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
13
16
 
14
17
  Usage:
15
18
  protect-mcp [options] -- <command> [args...]
19
+ protect-mcp quickstart
16
20
  protect-mcp init [--dir <path>]
17
21
  protect-mcp demo
22
+ protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
18
23
  protect-mcp status [--dir <path>]
19
24
  protect-mcp digest [--today] [--dir <path>]
20
25
  protect-mcp receipts [--last <n>] [--dir <path>]
21
26
  protect-mcp bundle [--output <path>] [--dir <path>]
27
+ protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
28
+ protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
22
29
 
23
30
  Options:
24
31
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -28,19 +35,22 @@ Options:
28
35
  --help Show this help
29
36
 
30
37
  Commands:
38
+ quickstart Zero-config onboarding: init + demo + show receipts in one command
31
39
  init Generate config template, Ed25519 keypair, and sample policy
32
40
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
41
+ trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
33
42
  status Show tool call statistics from the local decision log
34
43
  digest Generate a human-readable summary of agent activity
35
44
  receipts Show recent persisted signed receipts
36
45
  bundle Export an offline-verifiable audit bundle
37
46
 
38
47
  Examples:
48
+ protect-mcp quickstart
39
49
  protect-mcp -- node my-server.js
40
50
  protect-mcp --policy protect-mcp.json -- node my-server.js
41
- protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
42
51
  protect-mcp init
43
52
  protect-mcp demo
53
+ protect-mcp trace sha256:abc123 --depth 5
44
54
  protect-mcp status
45
55
  protect-mcp digest --today
46
56
  protect-mcp receipts --last 10
@@ -105,10 +115,7 @@ async function handleInit(argv) {
105
115
  process.exit(1);
106
116
  }
107
117
  let keypair;
108
- try {
109
- const artifacts = await import("@veritasacta/artifacts");
110
- keypair = artifacts.generateKeypair();
111
- } catch {
118
+ {
112
119
  const { randomBytes } = await import("crypto");
113
120
  const { ed25519 } = await import("./ed25519-EDO4K4EP.mjs");
114
121
  const { bytesToHex } = await import("./utils-IDWBSHJU.mjs");
@@ -614,12 +621,322 @@ ${bold("protect-mcp bundle")}
614
621
 
615
622
  `);
616
623
  }
624
+ async function handleQuickstart() {
625
+ const { mkdtempSync, writeFileSync, existsSync, mkdirSync, readFileSync } = await import("fs");
626
+ const { join } = await import("path");
627
+ const { tmpdir } = await import("os");
628
+ const dir = mkdtempSync(join(tmpdir(), "protect-mcp-quickstart-"));
629
+ process.stdout.write(`
630
+ ${bold("protect-mcp quickstart")}
631
+ `);
632
+ process.stdout.write(`${"\u2500".repeat(50)}
633
+
634
+ `);
635
+ process.stdout.write(` This will:
636
+ `);
637
+ process.stdout.write(` 1. Generate an Ed25519 signing keypair
638
+ `);
639
+ process.stdout.write(` 2. Create a shadow-mode policy
640
+ `);
641
+ process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
642
+ `);
643
+ process.stdout.write(` 4. Log signed receipts for every tool call
644
+
645
+ `);
646
+ process.stdout.write(` Working dir: ${dir}
647
+
648
+ `);
649
+ const keysDir = join(dir, "keys");
650
+ mkdirSync(keysDir, { recursive: true });
651
+ const { randomBytes } = await import("crypto");
652
+ let keypair;
653
+ try {
654
+ const { ed25519 } = await import("./ed25519-EDO4K4EP.mjs");
655
+ const { bytesToHex } = await import("./utils-IDWBSHJU.mjs");
656
+ const privateKey = randomBytes(32);
657
+ const publicKey = ed25519.getPublicKey(privateKey);
658
+ keypair = {
659
+ privateKey: bytesToHex(privateKey),
660
+ publicKey: bytesToHex(publicKey),
661
+ kid: `quickstart-${Date.now()}`
662
+ };
663
+ } catch {
664
+ keypair = {
665
+ privateKey: randomBytes(32).toString("hex"),
666
+ publicKey: randomBytes(32).toString("hex"),
667
+ kid: `quickstart-${Date.now()}`
668
+ };
669
+ }
670
+ writeFileSync(join(keysDir, "gateway.json"), JSON.stringify({
671
+ privateKey: keypair.privateKey,
672
+ publicKey: keypair.publicKey,
673
+ kid: keypair.kid,
674
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
675
+ }, null, 2) + "\n");
676
+ const configPath = join(dir, "protect-mcp.json");
677
+ const config = {
678
+ tools: {
679
+ "*": { rate_limit: "100/hour" },
680
+ "delete_file": { block: true }
681
+ },
682
+ default_tier: "unknown",
683
+ signing: {
684
+ key_path: join(keysDir, "gateway.json"),
685
+ issuer: "protect-mcp-quickstart",
686
+ enabled: true
687
+ }
688
+ };
689
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
690
+ process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
691
+ `);
692
+ process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
693
+ `);
694
+ process.stdout.write(` \u2713 Signing enabled (Ed25519)
695
+
696
+ `);
697
+ process.stdout.write(`${bold("Starting demo server...")}
698
+
699
+ `);
700
+ process.stdout.write(` Every tool call will produce a signed receipt.
701
+ `);
702
+ process.stdout.write(` Try it with Claude Desktop or any MCP client.
703
+
704
+ `);
705
+ process.stdout.write(` ${bold("To use in production:")}
706
+ `);
707
+ process.stdout.write(` 1. Copy ${configPath} to your project
708
+ `);
709
+ process.stdout.write(` 2. Edit tool policies to match your server
710
+ `);
711
+ process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
712
+
713
+ `);
714
+ process.stdout.write(`${"\u2500".repeat(50)}
715
+
716
+ `);
717
+ process.env.PROTECT_MCP_CONFIG = configPath;
718
+ await handleDemo();
719
+ }
720
+ async function handleTrace(argv) {
721
+ const receiptId = argv[0];
722
+ if (!receiptId) {
723
+ process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
724
+ process.exit(1);
725
+ }
726
+ let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
727
+ let depth = 3;
728
+ for (let i = 1; i < argv.length; i++) {
729
+ if (argv[i] === "--endpoint" && argv[i + 1]) {
730
+ endpoint = argv[++i];
731
+ } else if (argv[i] === "--depth" && argv[i + 1]) {
732
+ depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
733
+ }
734
+ }
735
+ process.stdout.write(`
736
+ ${bold("protect-mcp trace")}
737
+ `);
738
+ process.stdout.write(`${"\u2500".repeat(60)}
739
+
740
+ `);
741
+ process.stdout.write(` Root: ${receiptId}
742
+ `);
743
+ process.stdout.write(` Endpoint: ${endpoint}
744
+ `);
745
+ process.stdout.write(` Depth: ${depth}
746
+
747
+ `);
748
+ const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
749
+ let graphData;
750
+ try {
751
+ const resp = await fetch(url);
752
+ if (!resp.ok) {
753
+ const body = await resp.text();
754
+ process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
755
+ `);
756
+ process.exit(1);
757
+ }
758
+ graphData = await resp.json();
759
+ } catch (err) {
760
+ process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
761
+ `);
762
+ process.stderr.write(`[PROTECT_MCP] Trying local receipts...
763
+
764
+ `);
765
+ await traceLocal(receiptId);
766
+ return;
767
+ }
768
+ if (!graphData.nodes || graphData.nodes.length === 0) {
769
+ process.stdout.write(` No receipts found for ${receiptId}
770
+
771
+ `);
772
+ return;
773
+ }
774
+ process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
775
+
776
+ `);
777
+ const nodeMap = /* @__PURE__ */ new Map();
778
+ for (const node of graphData.nodes) {
779
+ nodeMap.set(node.receipt_id, node);
780
+ }
781
+ const childMap = /* @__PURE__ */ new Map();
782
+ for (const edge of graphData.edges) {
783
+ if (!childMap.has(edge.from)) childMap.set(edge.from, []);
784
+ childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
785
+ }
786
+ const rendered = /* @__PURE__ */ new Set();
787
+ function renderNode(id, prefix, isLast) {
788
+ const node = nodeMap.get(id);
789
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
790
+ const childPrefix = isLast ? " " : "\u2502 ";
791
+ const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
792
+ const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
793
+ const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
794
+ const type = node?.receipt_type?.replace("acta:", "") || "unknown";
795
+ process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
796
+ `);
797
+ if (rendered.has(id)) {
798
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
799
+ `);
800
+ return;
801
+ }
802
+ rendered.add(id);
803
+ const children = childMap.get(id) || [];
804
+ for (let i = 0; i < children.length; i++) {
805
+ const child = children[i];
806
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
807
+ process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
808
+ `);
809
+ renderNode(child.to, prefix + childPrefix, i === children.length - 1);
810
+ }
811
+ }
812
+ const rootNode = nodeMap.get(receiptId);
813
+ if (rootNode) {
814
+ const typeEmoji = getTypeEmoji(rootNode.receipt_type);
815
+ const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
816
+ const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
817
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
818
+ `);
819
+ rendered.add(receiptId);
820
+ const children = childMap.get(receiptId) || [];
821
+ for (let i = 0; i < children.length; i++) {
822
+ const child = children[i];
823
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
824
+ process.stdout.write(` ${edgeLabel}
825
+ `);
826
+ renderNode(child.to, " ", i === children.length - 1);
827
+ }
828
+ const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
829
+ if (incomingEdges.length > 0) {
830
+ process.stdout.write(`
831
+ ${bold("Incoming edges:")}
832
+ `);
833
+ for (const edge of incomingEdges) {
834
+ const fromNode = nodeMap.get(edge.from);
835
+ const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
836
+ process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
837
+ `);
838
+ }
839
+ }
840
+ } else {
841
+ for (const node of graphData.nodes) {
842
+ const typeEmoji = getTypeEmoji(node.receipt_type);
843
+ const type = node.receipt_type?.replace("acta:", "") || "unknown";
844
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
845
+ `);
846
+ }
847
+ }
848
+ process.stdout.write(`
849
+ ${"\u2500".repeat(60)}
850
+ `);
851
+ process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
852
+
853
+ `);
854
+ }
855
+ async function traceLocal(receiptId) {
856
+ const { readFileSync, existsSync } = await import("fs");
857
+ const { join } = await import("path");
858
+ const dir = process.cwd();
859
+ const receiptsDir = join(dir, ".protect-mcp", "receipts");
860
+ if (!existsSync(receiptsDir)) {
861
+ process.stdout.write(` No local receipts found in ${receiptsDir}
862
+
863
+ `);
864
+ return;
865
+ }
866
+ const { readdirSync } = await import("fs");
867
+ const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
868
+ process.stdout.write(` Scanning ${files.length} local receipts...
869
+
870
+ `);
871
+ const receipts = [];
872
+ for (const file of files) {
873
+ try {
874
+ const content = readFileSync(join(receiptsDir, file), "utf-8");
875
+ const receipt = JSON.parse(content);
876
+ receipts.push(receipt);
877
+ } catch {
878
+ }
879
+ }
880
+ const match = receipts.find(
881
+ (r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
882
+ );
883
+ if (match) {
884
+ const claims = match.signed_claims?.claims || match;
885
+ process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
886
+ `);
887
+ process.stdout.write(` Event: ${claims.event_id || "?"}
888
+ `);
889
+ process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
890
+ `);
891
+ process.stdout.write(` Time: ${claims.event_time || "?"}
892
+ `);
893
+ if (claims.edges && claims.edges.length > 0) {
894
+ process.stdout.write(`
895
+ ${bold("Edges:")}
896
+ `);
897
+ for (const edge of claims.edges) {
898
+ process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
899
+ `);
900
+ }
901
+ }
902
+ } else {
903
+ process.stdout.write(` Receipt ${receiptId} not found locally.
904
+ `);
905
+ }
906
+ process.stdout.write("\n");
907
+ }
908
+ function getTypeEmoji(type) {
909
+ switch (type) {
910
+ case "acta:observation":
911
+ return "\u{1F441} ";
912
+ case "acta:policy-load":
913
+ return "\u{1F4CB}";
914
+ case "acta:approval":
915
+ return "\u2705";
916
+ case "acta:decision":
917
+ return "\u2696\uFE0F ";
918
+ case "acta:execution":
919
+ return "\u26A1";
920
+ case "acta:outcome":
921
+ return "\u{1F4E6}";
922
+ case "acta:delegation":
923
+ return "\u{1F91D}";
924
+ case "acta:capability-attestation":
925
+ return "\u{1F3C5}";
926
+ default:
927
+ return "\u{1F4C4}";
928
+ }
929
+ }
617
930
  async function main() {
618
931
  const args = process.argv.slice(2);
619
932
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
620
933
  printHelp();
621
934
  process.exit(0);
622
935
  }
936
+ if (args[0] === "quickstart") {
937
+ await handleQuickstart();
938
+ return;
939
+ }
623
940
  if (args[0] === "init") {
624
941
  await handleInit(args.slice(1));
625
942
  process.exit(0);
@@ -644,6 +961,18 @@ async function main() {
644
961
  await handleBundle(args.slice(1));
645
962
  process.exit(0);
646
963
  }
964
+ if (args[0] === "trace") {
965
+ await handleTrace(args.slice(1));
966
+ process.exit(0);
967
+ }
968
+ if (args[0] === "simulate") {
969
+ await handleSimulate(args.slice(1));
970
+ process.exit(0);
971
+ }
972
+ if (args[0] === "report") {
973
+ await handleReport(args.slice(1));
974
+ process.exit(0);
975
+ }
647
976
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
648
977
  let policy = null;
649
978
  let policyDigest = "none";
@@ -694,6 +1023,85 @@ async function main() {
694
1023
  const gateway = new ProtectGateway(config);
695
1024
  await gateway.start();
696
1025
  }
1026
+ async function handleSimulate(args) {
1027
+ let policyPath = "";
1028
+ let logPath = ".protect-mcp-log.jsonl";
1029
+ let tier = "unknown";
1030
+ let jsonOutput = false;
1031
+ for (let i = 0; i < args.length; i++) {
1032
+ if (args[i] === "--policy" && args[i + 1]) {
1033
+ policyPath = args[++i];
1034
+ } else if (args[i] === "--log" && args[i + 1]) {
1035
+ logPath = args[++i];
1036
+ } else if (args[i] === "--tier" && args[i + 1]) {
1037
+ tier = args[++i];
1038
+ } else if (args[i] === "--json") {
1039
+ jsonOutput = true;
1040
+ }
1041
+ }
1042
+ if (!policyPath) {
1043
+ process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
1044
+ process.exit(1);
1045
+ }
1046
+ const { existsSync } = await import("fs");
1047
+ if (!existsSync(logPath)) {
1048
+ process.stderr.write(`Log file not found: ${logPath}
1049
+ `);
1050
+ process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
1051
+ process.exit(1);
1052
+ }
1053
+ const { policy } = loadPolicy(policyPath);
1054
+ const entries = parseLogFile(logPath);
1055
+ if (entries.length === 0) {
1056
+ process.stderr.write("No tool call entries found in log file.\n");
1057
+ process.exit(1);
1058
+ }
1059
+ const summary = simulate(entries, policy, tier);
1060
+ summary.policy_file = policyPath;
1061
+ summary.log_file = logPath;
1062
+ if (jsonOutput) {
1063
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
1064
+ } else {
1065
+ process.stdout.write(formatSimulation(summary) + "\n");
1066
+ }
1067
+ }
1068
+ async function handleReport(args) {
1069
+ let period = 30;
1070
+ let format = "json";
1071
+ let outputPath = "";
1072
+ let dir = process.cwd();
1073
+ for (let i = 0; i < args.length; i++) {
1074
+ if (args[i] === "--period" && args[i + 1]) {
1075
+ const match = args[++i].match(/^(\d+)d$/);
1076
+ if (match) period = parseInt(match[1], 10);
1077
+ } else if (args[i] === "--format" && args[i + 1]) {
1078
+ format = args[++i];
1079
+ } else if (args[i] === "--output" && args[i + 1]) {
1080
+ outputPath = args[++i];
1081
+ } else if (args[i] === "--dir" && args[i + 1]) {
1082
+ dir = args[++i];
1083
+ }
1084
+ }
1085
+ const { generateReport, formatReportMarkdown } = await import("./report-ENQ3KUI2.mjs");
1086
+ const { join } = await import("path");
1087
+ const logPath = join(dir, ".protect-mcp-log.jsonl");
1088
+ const receiptPath = join(dir, ".protect-mcp-receipts.jsonl");
1089
+ const report = generateReport(logPath, receiptPath, period);
1090
+ let output;
1091
+ if (format === "md") {
1092
+ output = formatReportMarkdown(report);
1093
+ } else {
1094
+ output = JSON.stringify(report, null, 2);
1095
+ }
1096
+ if (outputPath) {
1097
+ const { writeFileSync } = await import("fs");
1098
+ writeFileSync(outputPath, output, "utf-8");
1099
+ process.stderr.write(`Report written to ${outputPath}
1100
+ `);
1101
+ } else {
1102
+ process.stdout.write(output + "\n");
1103
+ }
1104
+ }
697
1105
  main().catch((err) => {
698
1106
  process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
699
1107
  `);
package/dist/index.d.mts 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 };