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/README.md +11 -1
- package/dist/bundle-TXOTFJIJ.mjs +8 -0
- package/dist/chunk-5JXFV37Y.mjs +53 -0
- package/dist/{chunk-3WCA7O4D.mjs → chunk-GV7N53QE.mjs} +300 -11
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/cli.js +1180 -38
- package/dist/cli.mjs +640 -11
- package/dist/index.d.mts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.js +466 -19
- package/dist/index.mjs +17 -50
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +3 -3
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-GV7N53QE.mjs";
|
|
8
11
|
|
|
9
12
|
// src/cli.ts
|
|
10
13
|
function printHelp() {
|
|
@@ -13,9 +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>]
|
|
24
|
+
protect-mcp digest [--today] [--dir <path>]
|
|
25
|
+
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
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>]
|
|
19
29
|
|
|
20
30
|
Options:
|
|
21
31
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -25,17 +35,26 @@ Options:
|
|
|
25
35
|
--help Show this help
|
|
26
36
|
|
|
27
37
|
Commands:
|
|
38
|
+
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
28
39
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
29
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)
|
|
30
42
|
status Show tool call statistics from the local decision log
|
|
43
|
+
digest Generate a human-readable summary of agent activity
|
|
44
|
+
receipts Show recent persisted signed receipts
|
|
45
|
+
bundle Export an offline-verifiable audit bundle
|
|
31
46
|
|
|
32
47
|
Examples:
|
|
48
|
+
protect-mcp quickstart
|
|
33
49
|
protect-mcp -- node my-server.js
|
|
34
50
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
35
|
-
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
36
51
|
protect-mcp init
|
|
37
52
|
protect-mcp demo
|
|
53
|
+
protect-mcp trace sha256:abc123 --depth 5
|
|
38
54
|
protect-mcp status
|
|
55
|
+
protect-mcp digest --today
|
|
56
|
+
protect-mcp receipts --last 10
|
|
57
|
+
protect-mcp bundle --output audit.json
|
|
39
58
|
|
|
40
59
|
`);
|
|
41
60
|
}
|
|
@@ -96,10 +115,7 @@ async function handleInit(argv) {
|
|
|
96
115
|
process.exit(1);
|
|
97
116
|
}
|
|
98
117
|
let keypair;
|
|
99
|
-
|
|
100
|
-
const artifacts = await import("@veritasacta/artifacts");
|
|
101
|
-
keypair = artifacts.generateKeypair();
|
|
102
|
-
} catch {
|
|
118
|
+
{
|
|
103
119
|
const { randomBytes } = await import("crypto");
|
|
104
120
|
const { ed25519 } = await import("./ed25519-EDO4K4EP.mjs");
|
|
105
121
|
const { bytesToHex } = await import("./utils-IDWBSHJU.mjs");
|
|
@@ -196,11 +212,10 @@ Add --enforce when ready to block policy violations.
|
|
|
196
212
|
}
|
|
197
213
|
async function handleDemo() {
|
|
198
214
|
const { existsSync } = await import("fs");
|
|
199
|
-
const { join, dirname } = await import("path");
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const demoServerPath = join(__dirname, "demo-server.js");
|
|
215
|
+
const { join, dirname, resolve } = await import("path");
|
|
216
|
+
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
217
|
+
const cliDir = dirname(cliPath);
|
|
218
|
+
const demoServerPath = join(cliDir, "demo-server.js");
|
|
204
219
|
const configPath = join(process.cwd(), "protect-mcp.json");
|
|
205
220
|
const hasConfig = existsSync(configPath);
|
|
206
221
|
if (!hasConfig) {
|
|
@@ -385,6 +400,27 @@ ${bold("protect-mcp status")}
|
|
|
385
400
|
} catch {
|
|
386
401
|
}
|
|
387
402
|
}
|
|
403
|
+
const keyPath = join(dir, "keys", "gateway.json");
|
|
404
|
+
if (existsSync(keyPath)) {
|
|
405
|
+
try {
|
|
406
|
+
const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
|
|
407
|
+
if (keyData.publicKey) {
|
|
408
|
+
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
409
|
+
process.stdout.write(`
|
|
410
|
+
${bold("\u{1F6E1}\uFE0F Passport identity:")}
|
|
411
|
+
`);
|
|
412
|
+
process.stdout.write(` Public key: ${fingerprint}
|
|
413
|
+
`);
|
|
414
|
+
if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
|
|
415
|
+
`);
|
|
416
|
+
process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
|
|
417
|
+
`);
|
|
418
|
+
process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
|
|
419
|
+
`);
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
388
424
|
process.stdout.write(`
|
|
389
425
|
Log file: ${dim(logPath)}
|
|
390
426
|
|
|
@@ -405,12 +441,502 @@ function red(s) {
|
|
|
405
441
|
function yellow(s) {
|
|
406
442
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
407
443
|
}
|
|
444
|
+
async function handleDigest(argv) {
|
|
445
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
446
|
+
const { join } = await import("path");
|
|
447
|
+
let dir = process.cwd();
|
|
448
|
+
const dirIdx = argv.indexOf("--dir");
|
|
449
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
450
|
+
const today = argv.includes("--today");
|
|
451
|
+
const logPath = join(dir, ".protect-mcp-log.jsonl");
|
|
452
|
+
if (!existsSync(logPath)) {
|
|
453
|
+
process.stderr.write(`${bold("protect-mcp digest")}
|
|
454
|
+
|
|
455
|
+
No log file found. Run protect-mcp first.
|
|
456
|
+
`);
|
|
457
|
+
process.exit(0);
|
|
458
|
+
}
|
|
459
|
+
const raw = readFileSync(logPath, "utf-8");
|
|
460
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
461
|
+
let entries = [];
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
try {
|
|
464
|
+
entries.push(JSON.parse(line));
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (today) {
|
|
469
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
470
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
471
|
+
entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
|
|
472
|
+
}
|
|
473
|
+
if (entries.length === 0) {
|
|
474
|
+
process.stdout.write(`
|
|
475
|
+
${bold("\u{1F6E1}\uFE0F Agent Digest")}
|
|
476
|
+
|
|
477
|
+
No activity${today ? " today" : ""}.
|
|
478
|
+
|
|
479
|
+
`);
|
|
480
|
+
process.exit(0);
|
|
481
|
+
}
|
|
482
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
483
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
484
|
+
const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
|
|
485
|
+
const toolUsage = /* @__PURE__ */ new Map();
|
|
486
|
+
for (const e of entries) {
|
|
487
|
+
toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
|
|
488
|
+
}
|
|
489
|
+
const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
|
|
490
|
+
const currentTier = entries[entries.length - 1]?.tier || "unknown";
|
|
491
|
+
const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
492
|
+
const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
493
|
+
const durationMs = lastTime.getTime() - firstTime.getTime();
|
|
494
|
+
const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
|
|
495
|
+
process.stdout.write(`
|
|
496
|
+
${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
497
|
+
|
|
498
|
+
`);
|
|
499
|
+
process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
|
|
500
|
+
process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
|
|
501
|
+
process.stdout.write(`${red("\u2717 " + denied)} blocked`);
|
|
502
|
+
if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
|
|
503
|
+
process.stdout.write(`
|
|
504
|
+
`);
|
|
505
|
+
process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
|
|
506
|
+
|
|
507
|
+
`);
|
|
508
|
+
process.stdout.write(` ${bold("Tools used:")}
|
|
509
|
+
`);
|
|
510
|
+
for (const [tool, count] of sortedTools.slice(0, 8)) {
|
|
511
|
+
process.stdout.write(` ${tool.padEnd(22)} ${count}x
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
if (denied > 0) {
|
|
515
|
+
const deniedTools = entries.filter((e) => e.decision === "deny");
|
|
516
|
+
const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
|
|
517
|
+
process.stdout.write(`
|
|
518
|
+
${bold(red("Blocked tools:"))}
|
|
519
|
+
`);
|
|
520
|
+
for (const tool of deniedToolNames) {
|
|
521
|
+
const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
|
|
522
|
+
process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
|
|
523
|
+
`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
process.stdout.write(`
|
|
527
|
+
${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
|
|
528
|
+
`);
|
|
529
|
+
process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
|
|
530
|
+
`);
|
|
531
|
+
process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
|
|
532
|
+
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
async function handleReceipts(argv) {
|
|
536
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
537
|
+
const { join } = await import("path");
|
|
538
|
+
let dir = process.cwd();
|
|
539
|
+
const dirIdx = argv.indexOf("--dir");
|
|
540
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
541
|
+
const lastIdx = argv.indexOf("--last");
|
|
542
|
+
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
543
|
+
const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
544
|
+
if (!existsSync(receiptsPath)) {
|
|
545
|
+
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
546
|
+
|
|
547
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
548
|
+
`);
|
|
549
|
+
process.exit(0);
|
|
550
|
+
}
|
|
551
|
+
const raw = readFileSync(receiptsPath, "utf-8");
|
|
552
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
553
|
+
const recent = lines.slice(-count);
|
|
554
|
+
process.stdout.write(`
|
|
555
|
+
${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
556
|
+
|
|
557
|
+
`);
|
|
558
|
+
for (const line of recent) {
|
|
559
|
+
try {
|
|
560
|
+
const entry = JSON.parse(line);
|
|
561
|
+
const payload = entry.payload || {};
|
|
562
|
+
const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
|
|
563
|
+
const decision = payload.decision || "unknown";
|
|
564
|
+
const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
|
|
565
|
+
process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
|
|
566
|
+
`);
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
process.stdout.write(`
|
|
571
|
+
`);
|
|
572
|
+
}
|
|
573
|
+
async function handleBundle(argv) {
|
|
574
|
+
const { readFileSync, writeFileSync, existsSync } = await import("fs");
|
|
575
|
+
const { join } = await import("path");
|
|
576
|
+
const { createAuditBundle } = await import("./bundle-TXOTFJIJ.mjs");
|
|
577
|
+
let dir = process.cwd();
|
|
578
|
+
const dirIdx = argv.indexOf("--dir");
|
|
579
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
580
|
+
const outputIdx = argv.indexOf("--output");
|
|
581
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join(dir, "audit-bundle.json");
|
|
582
|
+
const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
583
|
+
const keyPath = join(dir, "keys", "gateway.json");
|
|
584
|
+
if (!existsSync(receiptsPath)) {
|
|
585
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
586
|
+
|
|
587
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
588
|
+
`);
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
591
|
+
if (!existsSync(keyPath)) {
|
|
592
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
593
|
+
|
|
594
|
+
No key file found at ${keyPath}
|
|
595
|
+
`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
const receipts = readFileSync(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
599
|
+
const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
|
|
600
|
+
const bundle = createAuditBundle({
|
|
601
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
602
|
+
receipts,
|
|
603
|
+
signingKeys: [{
|
|
604
|
+
kty: "OKP",
|
|
605
|
+
crv: "Ed25519",
|
|
606
|
+
kid: keyData.kid || "unknown",
|
|
607
|
+
x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
|
|
608
|
+
use: "sig"
|
|
609
|
+
}]
|
|
610
|
+
});
|
|
611
|
+
writeFileSync(outputPath, JSON.stringify(bundle, null, 2) + "\n");
|
|
612
|
+
process.stdout.write(`
|
|
613
|
+
${bold("protect-mcp bundle")}
|
|
614
|
+
|
|
615
|
+
`);
|
|
616
|
+
process.stdout.write(` Receipts: ${receipts.length}
|
|
617
|
+
`);
|
|
618
|
+
process.stdout.write(` Output: ${outputPath}
|
|
619
|
+
`);
|
|
620
|
+
process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
|
|
621
|
+
|
|
622
|
+
`);
|
|
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
|
+
}
|
|
408
930
|
async function main() {
|
|
409
931
|
const args = process.argv.slice(2);
|
|
410
932
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
411
933
|
printHelp();
|
|
412
934
|
process.exit(0);
|
|
413
935
|
}
|
|
936
|
+
if (args[0] === "quickstart") {
|
|
937
|
+
await handleQuickstart();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
414
940
|
if (args[0] === "init") {
|
|
415
941
|
await handleInit(args.slice(1));
|
|
416
942
|
process.exit(0);
|
|
@@ -423,6 +949,30 @@ async function main() {
|
|
|
423
949
|
await handleStatus(args.slice(1));
|
|
424
950
|
process.exit(0);
|
|
425
951
|
}
|
|
952
|
+
if (args[0] === "digest") {
|
|
953
|
+
await handleDigest(args.slice(1));
|
|
954
|
+
process.exit(0);
|
|
955
|
+
}
|
|
956
|
+
if (args[0] === "receipts") {
|
|
957
|
+
await handleReceipts(args.slice(1));
|
|
958
|
+
process.exit(0);
|
|
959
|
+
}
|
|
960
|
+
if (args[0] === "bundle") {
|
|
961
|
+
await handleBundle(args.slice(1));
|
|
962
|
+
process.exit(0);
|
|
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
|
+
}
|
|
426
976
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
427
977
|
let policy = null;
|
|
428
978
|
let policyDigest = "none";
|
|
@@ -473,6 +1023,85 @@ async function main() {
|
|
|
473
1023
|
const gateway = new ProtectGateway(config);
|
|
474
1024
|
await gateway.start();
|
|
475
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
|
+
}
|
|
476
1105
|
main().catch((err) => {
|
|
477
1106
|
process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
|
|
478
1107
|
`);
|