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/LICENSE +68 -0
- package/README.md +42 -2
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/{chunk-U7TMVD3E.mjs → chunk-WDCPUM2O.mjs} +202 -3
- package/dist/cli.js +804 -31
- package/dist/cli.mjs +414 -6
- package/dist/index.d.mts +151 -4
- package/dist/index.d.ts +151 -4
- package/dist/index.js +369 -12
- package/dist/index.mjs +13 -1
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +13 -3
- package/policies/claude-code-hooks.json +18 -0
- package/policies/clinejection.json +45 -0
- package/policies/data-exfiltration.json +52 -0
- package/policies/financial-safe.json +49 -0
- package/policies/github-mcp-hijack.json +54 -0
- package/policies/terraform-destroy.json +50 -0
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-
|
|
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
|
-
|
|
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 };
|