protect-mcp 0.7.3 → 0.7.5
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/CHANGELOG.md +48 -0
- package/README.md +155 -7
- package/dist/{bundle-XTR3YMPO.mjs → bundle-YUUHAG7O.mjs} +1 -1
- package/dist/{chunk-G67NUBML.mjs → chunk-6E2DHBAR.mjs} +21 -6
- package/dist/chunk-JCMDLN5I.mjs +1053 -0
- package/dist/{chunk-LYKNULYU.mjs → chunk-LJQOALYR.mjs} +152 -0
- package/dist/{chunk-5JXFV37Y.mjs → chunk-PM2ZO57M.mjs} +10 -2
- package/dist/{chunk-J6L4XCTE.mjs → chunk-SETXVE2K.mjs} +77 -2
- package/dist/{chunk-OHUTUFTC.mjs → chunk-VTPZ4G5I.mjs} +19 -15
- package/dist/{chunk-546U3A7R.mjs → chunk-WIPWNWMJ.mjs} +93 -2
- package/dist/chunk-WV4DKYE4.mjs +486 -0
- package/dist/cli.js +4609 -546
- package/dist/cli.mjs +2215 -40
- package/dist/demo-server.d.mts +3 -0
- package/dist/demo-server.d.ts +3 -0
- package/dist/demo-server.js +77 -2
- package/dist/demo-server.mjs +1 -1
- package/dist/{ed25519-DZMMNNVE.mjs → ed25519-BSHMMVNX.mjs} +1 -1
- package/dist/hook-server.js +128 -24
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-R5AO7X6D.mjs → http-transport-JBORN27G.mjs} +3 -3
- package/dist/index.d.mts +158 -10
- package/dist/index.d.ts +158 -10
- package/dist/index.js +2704 -108
- package/dist/index.mjs +1208 -261
- package/dist/receipt-registry-6CAOY6RP.mjs +279 -0
- package/dist/signing-committed-QXCW24RF.mjs +15 -0
- package/package.json +2 -2
- package/dist/chunk-ZBKJANP7.mjs +0 -145
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONNECTOR_PILOTS,
|
|
4
|
+
POLICY_PACKS,
|
|
5
|
+
connectorDoctor,
|
|
3
6
|
formatSimulation,
|
|
7
|
+
getConnectorPilot,
|
|
8
|
+
getPolicyPack,
|
|
4
9
|
parseLogFile,
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
policyPackIds,
|
|
11
|
+
readInstalledConnectorPilots,
|
|
12
|
+
simulate,
|
|
13
|
+
writeConnectorPilots
|
|
14
|
+
} from "./chunk-JCMDLN5I.mjs";
|
|
7
15
|
import {
|
|
8
16
|
ProtectGateway,
|
|
9
17
|
validateCredentials
|
|
10
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-VTPZ4G5I.mjs";
|
|
11
19
|
import {
|
|
20
|
+
buildActionReadback,
|
|
12
21
|
evaluateCedar,
|
|
13
22
|
initSigning,
|
|
14
23
|
isCedarAvailable,
|
|
@@ -17,21 +26,32 @@ import {
|
|
|
17
26
|
policySetFromSource,
|
|
18
27
|
runEvaluatorSelfTest,
|
|
19
28
|
signDecision
|
|
20
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-WIPWNWMJ.mjs";
|
|
21
30
|
import "./chunk-PQJP2ZCI.mjs";
|
|
22
31
|
|
|
23
32
|
// src/cli.ts
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
33
|
+
import { createHash as createHashCli } from "crypto";
|
|
34
|
+
import { readFileSync as readFileSyncCli, existsSync as existsSyncCli, appendFileSync as appendFileSyncCli, mkdirSync as mkdirSyncCli, readdirSync as readdirSyncCli, writeFileSync as writeFileSyncCli } from "fs";
|
|
35
|
+
import { basename as basenameCli, dirname as dirnameCli, join as joinCli, resolve as resolveCli } from "path";
|
|
36
|
+
import { homedir as homedirCli } from "os";
|
|
26
37
|
function printHelp() {
|
|
27
38
|
process.stderr.write(`
|
|
28
|
-
protect-mcp
|
|
39
|
+
protect-mcp: Enterprise security gateway for MCP servers & Claude Code hooks
|
|
29
40
|
|
|
30
41
|
Usage:
|
|
31
42
|
protect-mcp [options] -- <command> [args...]
|
|
32
43
|
protect-mcp serve [--port <port>] [--enforce] [--policy <path>] [--cedar <dir>]
|
|
33
44
|
protect-mcp init-hooks [--dir <path>] [--port <port>]
|
|
34
45
|
protect-mcp quickstart [--connect]
|
|
46
|
+
protect-mcp wrap [--write] [--claude-desktop] [-- <command>]
|
|
47
|
+
protect-mcp dashboard [--port <port>] [--dir <path>] [--open]
|
|
48
|
+
protect-mcp recommend [--dir <path>] [--output <path>] [--write]
|
|
49
|
+
protect-mcp registry init|anchor|status [--dir <path>] [--org <name>] [--hosted]
|
|
50
|
+
protect-mcp trial [--dir <path>] [--hosted]
|
|
51
|
+
protect-mcp killer-demo [--dir <path>] [--hosted]
|
|
52
|
+
protect-mcp connectors list|show|init|doctor [connector|all] [--dir <path>] [--force]
|
|
53
|
+
protect-mcp verify-disclosure --receipt <path> --disclosure <path>
|
|
54
|
+
protect-mcp policy-packs list|show|install [pack] [--dir ./cedar] [--force]
|
|
35
55
|
protect-mcp connect
|
|
36
56
|
protect-mcp init [--dir <path>]
|
|
37
57
|
protect-mcp demo
|
|
@@ -39,6 +59,7 @@ Usage:
|
|
|
39
59
|
protect-mcp status [--dir <path>]
|
|
40
60
|
protect-mcp digest [--today] [--dir <path>]
|
|
41
61
|
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
62
|
+
protect-mcp record [--dir <path>] [--no-open]
|
|
42
63
|
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
43
64
|
protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
|
|
44
65
|
protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
|
|
@@ -59,6 +80,15 @@ Commands:
|
|
|
59
80
|
sign Sign one tool call into a receipt (PostToolUse)
|
|
60
81
|
init-hooks Generate Claude Code hook config + skill + sample Cedar policy
|
|
61
82
|
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
83
|
+
wrap Print or install a protect-mcp wrapper for MCP servers
|
|
84
|
+
dashboard Start a local-only action dashboard from logs/receipts
|
|
85
|
+
recommend Draft a policy from shadow-mode call inventory
|
|
86
|
+
registry Paid-boundary receipt digest registry and verifier page
|
|
87
|
+
trial Build the 10-minute self-serve proof path locally
|
|
88
|
+
killer-demo Build a 3-minute shadow\u2192policy\u2192approval\u2192receipt demo pack
|
|
89
|
+
connectors Install and inspect real connector pilots
|
|
90
|
+
verify-disclosure Verify a v0 selective-disclosure package and explain hidden fields
|
|
91
|
+
policy-packs List, inspect, or install starter Cedar policy packs
|
|
62
92
|
connect Create a ScopeBlind sandbox dashboard and configure receipt upload
|
|
63
93
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
64
94
|
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
@@ -67,6 +97,7 @@ Commands:
|
|
|
67
97
|
status Show tool call statistics from the local decision log
|
|
68
98
|
digest Generate a human-readable summary of agent activity
|
|
69
99
|
receipts Show recent persisted signed receipts
|
|
100
|
+
record Open a local, searchable view of your record in the browser
|
|
70
101
|
bundle Export an offline-verifiable audit bundle
|
|
71
102
|
|
|
72
103
|
Examples:
|
|
@@ -75,6 +106,17 @@ Examples:
|
|
|
75
106
|
protect-mcp init-hooks # One-command Claude Code setup
|
|
76
107
|
protect-mcp quickstart
|
|
77
108
|
protect-mcp quickstart --connect # Quickstart + create dashboard
|
|
109
|
+
protect-mcp wrap -- node my-server.js # Print wrapped MCP command
|
|
110
|
+
protect-mcp wrap --claude-desktop --write # Patch Claude Desktop MCP config
|
|
111
|
+
protect-mcp dashboard --open # Local risk/inventory dashboard
|
|
112
|
+
protect-mcp recommend --write # Draft a policy from observed calls
|
|
113
|
+
protect-mcp registry anchor --hosted # Upload only receipt digests for anchoring
|
|
114
|
+
protect-mcp trial --dir ./scopeblind-trial # Generate self-serve trial artifacts
|
|
115
|
+
protect-mcp killer-demo --dir ./scopeblind-demo # Generate sales-demo artifacts
|
|
116
|
+
protect-mcp connectors init all --force # Install connector pilot configs
|
|
117
|
+
protect-mcp connectors doctor # Check connector credentials safely
|
|
118
|
+
protect-mcp verify-disclosure --receipt committed.json --disclosure tool-only.json
|
|
119
|
+
protect-mcp policy-packs install filesystem-safe --dir ./cedar
|
|
78
120
|
protect-mcp connect # Connect existing setup to dashboard
|
|
79
121
|
protect-mcp -- node my-server.js
|
|
80
122
|
protect-mcp init
|
|
@@ -84,6 +126,7 @@ Examples:
|
|
|
84
126
|
protect-mcp bundle --output audit.json
|
|
85
127
|
|
|
86
128
|
Dashboard:
|
|
129
|
+
npx protect-mcp dashboard Local-only dashboard (127.0.0.1; no account)
|
|
87
130
|
npx protect-mcp connect Create a free ScopeBlind dashboard
|
|
88
131
|
Free up to 20,000 receipts/month
|
|
89
132
|
|
|
@@ -153,7 +196,7 @@ async function handleInit(argv) {
|
|
|
153
196
|
let keypair;
|
|
154
197
|
{
|
|
155
198
|
const { randomBytes } = await import("crypto");
|
|
156
|
-
const { ed25519 } = await import("./ed25519-
|
|
199
|
+
const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
|
|
157
200
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
158
201
|
const privateKey = randomBytes(32);
|
|
159
202
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -466,6 +509,1295 @@ ${bold("protect-mcp status")}
|
|
|
466
509
|
process.stdout.write(`
|
|
467
510
|
Log file: ${dim(logPath)}
|
|
468
511
|
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
function commandNeedsValue(argv, flag) {
|
|
515
|
+
const value = flagValue(argv, flag);
|
|
516
|
+
return Boolean(value && !value.startsWith("--"));
|
|
517
|
+
}
|
|
518
|
+
function absoluteOrCwd(pathValue) {
|
|
519
|
+
return resolveCli(process.cwd(), pathValue);
|
|
520
|
+
}
|
|
521
|
+
function shellQuoteArg(arg) {
|
|
522
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
|
|
523
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
524
|
+
}
|
|
525
|
+
function shellCommand(command, args) {
|
|
526
|
+
return [command, ...args].map(shellQuoteArg).join(" ");
|
|
527
|
+
}
|
|
528
|
+
function wrapperArgsFor(command, opts) {
|
|
529
|
+
const args = ["-y", "protect-mcp@latest"];
|
|
530
|
+
if (opts.cedarDir) args.push("--cedar", opts.cedarDir);
|
|
531
|
+
else args.push("--policy", opts.configPath || absoluteOrCwd("protect-mcp.json"));
|
|
532
|
+
if (opts.enforce) args.push("--enforce");
|
|
533
|
+
args.push("--", ...command);
|
|
534
|
+
return args;
|
|
535
|
+
}
|
|
536
|
+
function claudeDesktopConfigPath() {
|
|
537
|
+
if (process.platform === "darwin") {
|
|
538
|
+
return joinCli(homedirCli(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
539
|
+
}
|
|
540
|
+
if (process.platform === "win32") {
|
|
541
|
+
return joinCli(process.env.APPDATA || joinCli(homedirCli(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
542
|
+
}
|
|
543
|
+
return joinCli(homedirCli(), ".config", "Claude", "claude_desktop_config.json");
|
|
544
|
+
}
|
|
545
|
+
async function ensureLocalConfig(dir = process.cwd()) {
|
|
546
|
+
const { existsSync } = await import("fs");
|
|
547
|
+
const { join, resolve } = await import("path");
|
|
548
|
+
const configPath = join(dir, "protect-mcp.json");
|
|
549
|
+
if (!existsSync(configPath)) {
|
|
550
|
+
process.stderr.write(`${bold("protect-mcp wrap")}
|
|
551
|
+
|
|
552
|
+
No protect-mcp.json found; creating local shadow-mode config first.
|
|
553
|
+
|
|
554
|
+
`);
|
|
555
|
+
await handleInit(["--dir", dir]);
|
|
556
|
+
}
|
|
557
|
+
return resolve(configPath);
|
|
558
|
+
}
|
|
559
|
+
function parseJsonlFile(pathValue) {
|
|
560
|
+
try {
|
|
561
|
+
const raw = readFileSyncCli(pathValue, "utf-8");
|
|
562
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
563
|
+
try {
|
|
564
|
+
return [JSON.parse(line)];
|
|
565
|
+
} catch {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
} catch {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function parseJsonlRecords(pathValue) {
|
|
574
|
+
try {
|
|
575
|
+
const raw = readFileSyncCli(pathValue, "utf-8");
|
|
576
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
577
|
+
try {
|
|
578
|
+
return [{
|
|
579
|
+
value: JSON.parse(line),
|
|
580
|
+
raw: line,
|
|
581
|
+
hash: createHashCli("sha256").update(line).digest("hex")
|
|
582
|
+
}];
|
|
583
|
+
} catch {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function loadPolicyJson(policyPath) {
|
|
592
|
+
try {
|
|
593
|
+
if (!existsSyncCli(policyPath)) return null;
|
|
594
|
+
return JSON.parse(readFileSyncCli(policyPath, "utf-8"));
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function policyCoverageForTool(tool, policy) {
|
|
600
|
+
const tools = policy?.tools && typeof policy.tools === "object" ? policy.tools : {};
|
|
601
|
+
if (tools[tool]) {
|
|
602
|
+
return { status: "exact", label: "Exact rule", policy: tools[tool] };
|
|
603
|
+
}
|
|
604
|
+
if (tools["*"]) {
|
|
605
|
+
return { status: "wildcard", label: "Wildcard fallback", policy: tools["*"] };
|
|
606
|
+
}
|
|
607
|
+
return { status: "none", label: "No rule" };
|
|
608
|
+
}
|
|
609
|
+
function receiptRequestId(receipt) {
|
|
610
|
+
const direct = receipt.request_id || receipt.scope;
|
|
611
|
+
if (typeof direct === "string") return direct;
|
|
612
|
+
const payload = receipt.payload;
|
|
613
|
+
if (payload && typeof payload === "object") {
|
|
614
|
+
const candidate = payload.request_id || payload.scope;
|
|
615
|
+
if (typeof candidate === "string") return candidate;
|
|
616
|
+
}
|
|
617
|
+
const claims = receipt.signed_claims;
|
|
618
|
+
if (claims && typeof claims === "object") {
|
|
619
|
+
const nestedClaims = claims.claims;
|
|
620
|
+
if (nestedClaims && typeof nestedClaims === "object") {
|
|
621
|
+
const candidate = nestedClaims.request_id || nestedClaims.scope;
|
|
622
|
+
if (typeof candidate === "string") return candidate;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return void 0;
|
|
626
|
+
}
|
|
627
|
+
function buildReceiptChains(entries, receipts) {
|
|
628
|
+
const receiptMap = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const receipt of receipts) {
|
|
630
|
+
const requestId = receiptRequestId(receipt.value);
|
|
631
|
+
if (!requestId) continue;
|
|
632
|
+
const rows = receiptMap.get(requestId) || [];
|
|
633
|
+
rows.push(receipt);
|
|
634
|
+
receiptMap.set(requestId, rows);
|
|
635
|
+
}
|
|
636
|
+
const logMap = /* @__PURE__ */ new Map();
|
|
637
|
+
for (const entry of entries) {
|
|
638
|
+
if (!entry.request_id) continue;
|
|
639
|
+
const rows = logMap.get(entry.request_id) || [];
|
|
640
|
+
rows.push(entry);
|
|
641
|
+
logMap.set(entry.request_id, rows);
|
|
642
|
+
}
|
|
643
|
+
return [...logMap.entries()].map(([requestId, logs]) => {
|
|
644
|
+
const relatedReceipts = receiptMap.get(requestId) || [];
|
|
645
|
+
const latest = logs[logs.length - 1];
|
|
646
|
+
return {
|
|
647
|
+
request_id: requestId,
|
|
648
|
+
tool: latest?.tool || "unknown",
|
|
649
|
+
decision: latest?.decision || "unknown",
|
|
650
|
+
reason_code: latest?.reason_code || "",
|
|
651
|
+
action_readback: latest?.action_readback,
|
|
652
|
+
log_events: logs.map((log) => ({
|
|
653
|
+
decision: log.decision,
|
|
654
|
+
reason_code: log.reason_code,
|
|
655
|
+
timestamp: log.timestamp,
|
|
656
|
+
hook_event: log.hook_event
|
|
657
|
+
})),
|
|
658
|
+
receipts: relatedReceipts.map((receipt) => ({
|
|
659
|
+
hash: receipt.hash,
|
|
660
|
+
type: String(receipt.value.type || receipt.value.artifact_type || "receipt")
|
|
661
|
+
})),
|
|
662
|
+
complete: relatedReceipts.length > 0
|
|
663
|
+
};
|
|
664
|
+
}).sort((a, b) => {
|
|
665
|
+
const at = a.log_events[0]?.timestamp || 0;
|
|
666
|
+
const bt = b.log_events[0]?.timestamp || 0;
|
|
667
|
+
return bt - at;
|
|
668
|
+
}).slice(0, 80);
|
|
669
|
+
}
|
|
670
|
+
function riskForTool(toolRaw) {
|
|
671
|
+
const tool = toolRaw.toLowerCase();
|
|
672
|
+
const reasons = [];
|
|
673
|
+
const highPatterns = [
|
|
674
|
+
["delete", "delete/destructive"],
|
|
675
|
+
["remove", "delete/destructive"],
|
|
676
|
+
["rm", "delete/destructive"],
|
|
677
|
+
["write", "writes data"],
|
|
678
|
+
["send", "external send"],
|
|
679
|
+
["email", "external comms"],
|
|
680
|
+
["slack", "external comms"],
|
|
681
|
+
["teams", "external comms"],
|
|
682
|
+
["github", "source-control mutation"],
|
|
683
|
+
["commit", "source-control mutation"],
|
|
684
|
+
["push", "source-control mutation"],
|
|
685
|
+
["deploy", "deployment"],
|
|
686
|
+
["terraform", "cloud infrastructure"],
|
|
687
|
+
["aws", "cloud infrastructure"],
|
|
688
|
+
["gcp", "cloud infrastructure"],
|
|
689
|
+
["azure", "cloud infrastructure"],
|
|
690
|
+
["sql", "database access"],
|
|
691
|
+
["database", "database access"],
|
|
692
|
+
["payment", "spend/payment"],
|
|
693
|
+
["order", "order/transaction"],
|
|
694
|
+
["trade", "trade/transaction"],
|
|
695
|
+
["pms", "portfolio-system mutation"],
|
|
696
|
+
["book", "portfolio-system mutation"],
|
|
697
|
+
["fill", "portfolio-system mutation"],
|
|
698
|
+
["secret", "secrets"],
|
|
699
|
+
["token", "secrets"]
|
|
700
|
+
];
|
|
701
|
+
for (const [needle, label] of highPatterns) {
|
|
702
|
+
if (tool.includes(needle) && !reasons.includes(label)) reasons.push(label);
|
|
703
|
+
}
|
|
704
|
+
if (reasons.length > 0) return { tier: "high", reasons };
|
|
705
|
+
if (tool.includes("read") || tool.includes("search") || tool.includes("list") || tool.includes("fetch")) {
|
|
706
|
+
return { tier: "medium", reasons: ["data access"] };
|
|
707
|
+
}
|
|
708
|
+
return { tier: "low", reasons: ["observed"] };
|
|
709
|
+
}
|
|
710
|
+
function suggestedGuardrailFor(_tool, risk, reasons) {
|
|
711
|
+
const reasonSet = new Set(reasons);
|
|
712
|
+
if (reasonSet.has("delete/destructive") || reasonSet.has("secrets")) {
|
|
713
|
+
return {
|
|
714
|
+
action: "Block by default",
|
|
715
|
+
reason: "Destructive and secret-handling tools should start deny-first.",
|
|
716
|
+
policy: { block: true, min_tier: "privileged" }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (reasonSet.has("order/transaction") || reasonSet.has("trade/transaction") || reasonSet.has("spend/payment") || reasonSet.has("portfolio-system mutation") || reasonSet.has("deployment") || reasonSet.has("cloud infrastructure") || reasonSet.has("database access") || reasonSet.has("external send") || reasonSet.has("source-control mutation")) {
|
|
720
|
+
return {
|
|
721
|
+
action: "Require approval",
|
|
722
|
+
reason: "Consequential tools should require a human approval receipt before enforce mode.",
|
|
723
|
+
policy: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (risk === "medium") {
|
|
727
|
+
return {
|
|
728
|
+
action: "Rate-limit and identify",
|
|
729
|
+
reason: "Read/search/fetch tools can leak data at scale; keep them visible and bounded.",
|
|
730
|
+
policy: { min_tier: "signed-known", rate_limit: "60/hour" }
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
action: "Observe",
|
|
735
|
+
reason: "Low-risk observed tool. Keep receipts and a broad rate limit.",
|
|
736
|
+
policy: { rate_limit: "100/hour" }
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function buildDashboardSummary(dir, policyPath = joinCli(dir, "protect-mcp.json")) {
|
|
740
|
+
const logPath = joinCli(dir, ".protect-mcp-log.jsonl");
|
|
741
|
+
const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
|
|
742
|
+
const keyPath = joinCli(dir, "keys", "gateway.json");
|
|
743
|
+
const entries = parseJsonlFile(logPath);
|
|
744
|
+
const receiptRecords = parseJsonlRecords(receiptPath);
|
|
745
|
+
const receipts = receiptRecords.map((record) => record.value);
|
|
746
|
+
const activePolicy = loadPolicyJson(policyPath);
|
|
747
|
+
const tools = /* @__PURE__ */ new Map();
|
|
748
|
+
for (const entry of entries) {
|
|
749
|
+
const tool = String(entry.tool || "unknown");
|
|
750
|
+
const risk = riskForTool(tool);
|
|
751
|
+
const current = tools.get(tool) || {
|
|
752
|
+
tool,
|
|
753
|
+
calls: 0,
|
|
754
|
+
allows: 0,
|
|
755
|
+
denies: 0,
|
|
756
|
+
reviews: 0,
|
|
757
|
+
risk: risk.tier,
|
|
758
|
+
reasons: risk.reasons
|
|
759
|
+
};
|
|
760
|
+
current.calls += 1;
|
|
761
|
+
if (entry.decision === "allow") current.allows += 1;
|
|
762
|
+
else if (entry.decision === "deny") current.denies += 1;
|
|
763
|
+
else if (entry.decision === "require_approval") current.reviews += 1;
|
|
764
|
+
if (risk.tier === "high" || risk.tier === "medium" && current.risk === "low") current.risk = risk.tier;
|
|
765
|
+
current.reasons = [.../* @__PURE__ */ new Set([...current.reasons, ...risk.reasons])];
|
|
766
|
+
if (typeof entry.timestamp === "number") current.last_seen = new Date(entry.timestamp).toISOString();
|
|
767
|
+
tools.set(tool, current);
|
|
768
|
+
}
|
|
769
|
+
const toolRows = [...tools.values()].sort((a, b) => {
|
|
770
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
771
|
+
return order[a.risk] - order[b.risk] || b.calls - a.calls || a.tool.localeCompare(b.tool);
|
|
772
|
+
}).map((tool) => ({
|
|
773
|
+
...tool,
|
|
774
|
+
suggestion: suggestedGuardrailFor(tool.tool, tool.risk, tool.reasons),
|
|
775
|
+
policy_coverage: policyCoverageForTool(tool.tool, activePolicy)
|
|
776
|
+
}));
|
|
777
|
+
const highRisk = toolRows.filter((t) => t.risk === "high");
|
|
778
|
+
const uncovered = toolRows.filter((t) => t.policy_coverage.status === "none").length;
|
|
779
|
+
const exactCovered = toolRows.filter((t) => t.policy_coverage.status === "exact").length;
|
|
780
|
+
const wildcardCovered = toolRows.filter((t) => t.policy_coverage.status === "wildcard").length;
|
|
781
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
782
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
783
|
+
const review = entries.filter((e) => e.decision === "require_approval").length;
|
|
784
|
+
const pendingApprovals = entries.filter((e) => e.decision === "require_approval").slice(-25).reverse();
|
|
785
|
+
const chains = buildReceiptChains(entries, receiptRecords);
|
|
786
|
+
let key = null;
|
|
787
|
+
if (existsSyncCli(keyPath)) {
|
|
788
|
+
try {
|
|
789
|
+
const parsed = JSON.parse(readFileSyncCli(keyPath, "utf-8"));
|
|
790
|
+
key = {
|
|
791
|
+
kid: parsed.kid || null,
|
|
792
|
+
issuer: parsed.issuer || "protect-mcp",
|
|
793
|
+
publicKeyPrefix: typeof parsed.publicKey === "string" ? `${parsed.publicKey.slice(0, 16)}...` : null
|
|
794
|
+
};
|
|
795
|
+
} catch {
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
dir,
|
|
801
|
+
files: {
|
|
802
|
+
log: logPath,
|
|
803
|
+
receipts: receiptPath,
|
|
804
|
+
key: keyPath,
|
|
805
|
+
policy: policyPath,
|
|
806
|
+
log_exists: existsSyncCli(logPath),
|
|
807
|
+
receipts_exist: existsSyncCli(receiptPath),
|
|
808
|
+
key_exists: existsSyncCli(keyPath),
|
|
809
|
+
policy_exists: existsSyncCli(policyPath)
|
|
810
|
+
},
|
|
811
|
+
totals: {
|
|
812
|
+
decisions: entries.length,
|
|
813
|
+
receipts: receipts.length,
|
|
814
|
+
tools: toolRows.length,
|
|
815
|
+
high_risk_tools: highRisk.length,
|
|
816
|
+
exact_covered: exactCovered,
|
|
817
|
+
wildcard_covered: wildcardCovered,
|
|
818
|
+
uncovered,
|
|
819
|
+
allowed,
|
|
820
|
+
denied,
|
|
821
|
+
review
|
|
822
|
+
},
|
|
823
|
+
key,
|
|
824
|
+
policy: activePolicy ? {
|
|
825
|
+
path: policyPath,
|
|
826
|
+
digest: createHashCli("sha256").update(JSON.stringify(activePolicy)).digest("hex").slice(0, 16),
|
|
827
|
+
default_tier: activePolicy.default_tier || "unknown",
|
|
828
|
+
tools: activePolicy.tools || {}
|
|
829
|
+
} : null,
|
|
830
|
+
tools: toolRows,
|
|
831
|
+
pending_approvals: pendingApprovals,
|
|
832
|
+
receipt_chains: chains,
|
|
833
|
+
recent: entries.slice(-50).reverse(),
|
|
834
|
+
policy_packs: {
|
|
835
|
+
directory: policyPackDirectory(dir),
|
|
836
|
+
installed: installedPolicyPackIds(dir),
|
|
837
|
+
available: POLICY_PACKS.map((pack) => ({
|
|
838
|
+
id: pack.id,
|
|
839
|
+
name: pack.name,
|
|
840
|
+
description: pack.description,
|
|
841
|
+
recommendedMode: pack.recommendedMode,
|
|
842
|
+
files: pack.files.map((file) => ({ path: file.path, contents: file.contents }))
|
|
843
|
+
}))
|
|
844
|
+
},
|
|
845
|
+
connector_pilots: {
|
|
846
|
+
directory: joinCli(dir, ".protect-mcp", "connectors"),
|
|
847
|
+
installed: readInstalledConnectorPilots(dir),
|
|
848
|
+
doctor: connectorDoctor(dir),
|
|
849
|
+
available: CONNECTOR_PILOTS.map((pilot) => ({
|
|
850
|
+
id: pilot.id,
|
|
851
|
+
name: pilot.name,
|
|
852
|
+
category: pilot.category,
|
|
853
|
+
description: pilot.description,
|
|
854
|
+
value: pilot.value,
|
|
855
|
+
tools: pilot.tools,
|
|
856
|
+
actions: pilot.actions,
|
|
857
|
+
setup: pilot.setup
|
|
858
|
+
}))
|
|
859
|
+
},
|
|
860
|
+
registry: dashboardRegistryStatus(dir),
|
|
861
|
+
recommendations: [
|
|
862
|
+
entries.length === 0 ? "Run in shadow mode first: npx protect-mcp -- node your-mcp-server.js" : "",
|
|
863
|
+
highRisk.length > 0 ? "Run npx protect-mcp recommend --write, review the generated policy, then restart your wrapper with --enforce." : "",
|
|
864
|
+
receipts.length === 0 ? "Run npx protect-mcp init so decisions are signed into local receipts." : "",
|
|
865
|
+
"Install a starter policy pack from this dashboard when you know the tool class: filesystem, Git, email, database, cloud spend, secrets, or finance.",
|
|
866
|
+
"Create a registry preview locally, then use hosted digest anchoring when you need independent timestamp evidence.",
|
|
867
|
+
"Export an audit bundle with: npx protect-mcp bundle --output audit.json"
|
|
868
|
+
].filter(Boolean)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function dashboardHtml() {
|
|
872
|
+
return `<!doctype html>
|
|
873
|
+
<html lang="en">
|
|
874
|
+
<head>
|
|
875
|
+
<meta charset="utf-8">
|
|
876
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
877
|
+
<title>protect-mcp local action dashboard</title>
|
|
878
|
+
<style>
|
|
879
|
+
:root {
|
|
880
|
+
color-scheme: light;
|
|
881
|
+
--ink:#12110e; --muted:#6f6b61; --soft:#9a9488; --line:#ded7c9;
|
|
882
|
+
--paper:#f7f3ea; --card:#fffdf7; --black:#11110f;
|
|
883
|
+
--bad:#8f241c; --warn:#8d620f; --ok:#2f6f4e;
|
|
884
|
+
--shadow: 0 22px 70px rgba(36,30,18,.10);
|
|
885
|
+
}
|
|
886
|
+
* { box-sizing: border-box; }
|
|
887
|
+
body { margin:0; background:radial-gradient(circle at top left,#fffdf7 0,#f7f3ea 34%,#e8dfce 100%); color:var(--ink); font:14px/1.45 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
888
|
+
main { width:min(1380px, calc(100vw - 32px)); margin:24px auto 60px; }
|
|
889
|
+
.hero { display:grid; grid-template-columns:1.35fr .65fr; gap:16px; align-items:stretch; }
|
|
890
|
+
.card { background:rgba(255,253,247,.92); border:1px solid var(--line); border-radius:24px; padding:22px; box-shadow:var(--shadow); }
|
|
891
|
+
.kicker { text-transform:uppercase; letter-spacing:.18em; font-size:11px; color:var(--muted); font-weight:800; }
|
|
892
|
+
h1 { font-family: ui-serif, Georgia, serif; font-weight:520; font-size:clamp(38px,5vw,76px); line-height:.92; letter-spacing:-.045em; margin:12px 0 14px; max-width:980px; }
|
|
893
|
+
h2 { margin:0 0 12px; font-size:17px; letter-spacing:-.01em; }
|
|
894
|
+
h3 { margin:0 0 8px; font-size:14px; }
|
|
895
|
+
p { color:var(--muted); margin:0; }
|
|
896
|
+
small { color:var(--muted); }
|
|
897
|
+
.layout { display:grid; grid-template-columns:1.28fr .72fr; gap:16px; margin-top:16px; }
|
|
898
|
+
.stack { display:grid; gap:16px; }
|
|
899
|
+
.stats { display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin:16px 0; }
|
|
900
|
+
.stat { background:#fffaf0; border:1px solid var(--line); border-radius:18px; padding:13px; min-height:86px; }
|
|
901
|
+
.stat strong { display:block; font-size:26px; letter-spacing:-.04em; }
|
|
902
|
+
.actions { display:flex; flex-wrap:wrap; gap:9px; margin-top:16px; }
|
|
903
|
+
button, a.btn { appearance:none; border:1px solid var(--black); background:var(--black); color:#fff; text-decoration:none; border-radius:999px; padding:9px 12px; cursor:pointer; font-weight:800; font-size:13px; }
|
|
904
|
+
button.secondary, a.btn.secondary { background:transparent; color:var(--black); }
|
|
905
|
+
button.ghost { border-color:var(--line); background:#fffaf0; color:var(--ink); }
|
|
906
|
+
button.danger { background:#7f1d18; border-color:#7f1d18; }
|
|
907
|
+
table { width:100%; border-collapse:collapse; }
|
|
908
|
+
th, td { text-align:left; padding:12px 9px; border-bottom:1px solid var(--line); vertical-align:top; }
|
|
909
|
+
th { color:var(--muted); font-size:11px; letter-spacing:.09em; text-transform:uppercase; }
|
|
910
|
+
.pill { display:inline-flex; align-items:center; border-radius:999px; padding:4px 9px; font-size:11px; font-weight:900; white-space:nowrap; }
|
|
911
|
+
.high { background:#f7d9d3; color:var(--bad); }
|
|
912
|
+
.medium { background:#f4e5bd; color:var(--warn); }
|
|
913
|
+
.low { background:#dcebdd; color:var(--ok); }
|
|
914
|
+
.exact { background:#dcebdd; color:var(--ok); }
|
|
915
|
+
.wildcard { background:#e5decc; color:#5e5545; }
|
|
916
|
+
.none { background:#f7d9d3; color:var(--bad); }
|
|
917
|
+
.allow { color:var(--ok); } .deny { color:var(--bad); } .require_approval { color:var(--warn); }
|
|
918
|
+
code { background:#f2eadc; border:1px solid var(--line); border-radius:8px; padding:2px 6px; }
|
|
919
|
+
pre { white-space:pre-wrap; background:#181712; color:#f8f1df; border-radius:16px; padding:14px; overflow:auto; font-size:12px; }
|
|
920
|
+
.muted { color:var(--muted); }
|
|
921
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
922
|
+
.approval { border:1px solid var(--line); background:#fffaf0; border-radius:18px; padding:14px; margin-bottom:10px; }
|
|
923
|
+
.readback { background:#181712; color:#f8f1df; border-radius:16px; padding:12px; margin:10px 0; }
|
|
924
|
+
.readback .label { color:#bdb49f; font-size:11px; text-transform:uppercase; letter-spacing:.12em; }
|
|
925
|
+
.preview { max-height:180px; overflow:auto; }
|
|
926
|
+
.row-actions { display:flex; flex-wrap:wrap; gap:6px; }
|
|
927
|
+
.chain { display:grid; gap:8px; }
|
|
928
|
+
.chain-item { border:1px solid var(--line); border-radius:16px; padding:12px; background:#fffaf0; }
|
|
929
|
+
.pack-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin-top:12px; }
|
|
930
|
+
.pack { border:1px solid var(--line); border-radius:16px; padding:12px; background:#fffaf0; display:grid; gap:8px; }
|
|
931
|
+
.pack.installed { border-color:rgba(47,111,78,.35); background:#f3f8ef; }
|
|
932
|
+
.divider { border:0; border-top:1px solid var(--line); margin:14px 0; }
|
|
933
|
+
.field { display:grid; gap:5px; margin-top:9px; }
|
|
934
|
+
.field label { font-size:11px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted); font-weight:900; }
|
|
935
|
+
input { border:1px solid var(--line); border-radius:12px; background:#fffaf0; color:var(--ink); padding:10px; font:13px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
936
|
+
.toast { position:fixed; right:18px; bottom:18px; background:#111; color:#fff; padding:12px 14px; border-radius:14px; box-shadow:var(--shadow); display:none; max-width:360px; }
|
|
937
|
+
@media (max-width: 980px) { .hero,.layout { grid-template-columns:1fr; } .stats { grid-template-columns:repeat(2,1fr); } main { width:min(100vw - 20px, 1380px); margin-top:14px; } }
|
|
938
|
+
</style>
|
|
939
|
+
</head>
|
|
940
|
+
<body>
|
|
941
|
+
<main>
|
|
942
|
+
<section class="hero">
|
|
943
|
+
<div class="card">
|
|
944
|
+
<div class="kicker">Local Action Dashboard</div>
|
|
945
|
+
<h1>See what agents can do. Control dangerous actions. Prove what happened.</h1>
|
|
946
|
+
<p>Runs on <code>127.0.0.1</code>. Start in shadow mode, switch risky tools to exact-action approval, then export signed evidence without uploading sensitive data.</p>
|
|
947
|
+
<div class="actions">
|
|
948
|
+
<button onclick="refresh()">Refresh</button>
|
|
949
|
+
<button class="secondary" onclick="exportBundle()">Export audit bundle</button>
|
|
950
|
+
<a class="btn secondary" href="/api/summary">Raw JSON</a>
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
<div class="card">
|
|
954
|
+
<div class="kicker">Next move</div>
|
|
955
|
+
<h2>Shadow, review, then enforce</h2>
|
|
956
|
+
<pre>npx protect-mcp wrap -- node your-server.js
|
|
957
|
+
npx protect-mcp dashboard --open
|
|
958
|
+
npx protect-mcp recommend --write
|
|
959
|
+
npx protect-mcp --policy protect-mcp.json --enforce -- node your-server.js</pre>
|
|
960
|
+
<p class="muted" id="dir"></p>
|
|
961
|
+
<p class="muted" id="policy"></p>
|
|
962
|
+
</div>
|
|
963
|
+
</section>
|
|
964
|
+
|
|
965
|
+
<section class="stats" id="stats"></section>
|
|
966
|
+
|
|
967
|
+
<section class="layout">
|
|
968
|
+
<div class="stack">
|
|
969
|
+
<section class="card">
|
|
970
|
+
<h2>Tool Inventory</h2>
|
|
971
|
+
<p>Risk, policy coverage, and one-click guardrail drafting for each observed tool.</p>
|
|
972
|
+
<div style="overflow:auto; margin-top:12px"><table id="tools"></table></div>
|
|
973
|
+
</section>
|
|
974
|
+
<section class="card">
|
|
975
|
+
<h2>Policy Packs</h2>
|
|
976
|
+
<p>Install a starter Cedar pack from the dashboard. This removes the blank-policy problem while keeping final enforcement local and reviewable.</p>
|
|
977
|
+
<div id="policy-packs" class="pack-grid"></div>
|
|
978
|
+
</section>
|
|
979
|
+
<section class="card">
|
|
980
|
+
<h2>Connector Pilots</h2>
|
|
981
|
+
<p>Real tool categories teams already use: GitHub, Gmail/email, filesystem/Git, Slack or Teams, and finance/PMS. Install a pilot, check credentials, then watch those tools in the dashboard.</p>
|
|
982
|
+
<div id="connector-pilots" class="pack-grid"></div>
|
|
983
|
+
</section>
|
|
984
|
+
<section class="card">
|
|
985
|
+
<h2>Call History</h2>
|
|
986
|
+
<p>What agents actually tried to do, including exact-action readbacks when available.</p>
|
|
987
|
+
<div id="recent" style="margin-top:12px"></div>
|
|
988
|
+
</section>
|
|
989
|
+
</div>
|
|
990
|
+
<div class="stack">
|
|
991
|
+
<section class="card">
|
|
992
|
+
<h2>Approval Queue</h2>
|
|
993
|
+
<p>Desktop fallback approval surface. If you start this dashboard with <code>--approval-endpoint</code> and <code>--approval-nonce</code>, Approve forwards to the live local gateway.</p>
|
|
994
|
+
<div id="approvals" style="margin-top:12px"></div>
|
|
995
|
+
</section>
|
|
996
|
+
<section class="card">
|
|
997
|
+
<h2>Receipt Chain</h2>
|
|
998
|
+
<p>Decision logs correlated with signed receipts by request id.</p>
|
|
999
|
+
<div id="chains" class="chain" style="margin-top:12px"></div>
|
|
1000
|
+
</section>
|
|
1001
|
+
<section class="card">
|
|
1002
|
+
<h2>Paid Boundary</h2>
|
|
1003
|
+
<p>Free local receipts stay local. The paid line starts when a ScopeBlind org identity independently timestamps receipt digests without raw prompt, payload, or receipt upload.</p>
|
|
1004
|
+
<div id="registry" style="margin-top:12px"></div>
|
|
1005
|
+
</section>
|
|
1006
|
+
<section class="card">
|
|
1007
|
+
<h2>Recommendations</h2>
|
|
1008
|
+
<div id="recommendations"></div>
|
|
1009
|
+
</section>
|
|
1010
|
+
</div>
|
|
1011
|
+
</section>
|
|
1012
|
+
</main>
|
|
1013
|
+
<div class="toast" id="toast"></div>
|
|
1014
|
+
<script>
|
|
1015
|
+
var state = null;
|
|
1016
|
+
async function refresh(){
|
|
1017
|
+
state = await fetch('/api/summary').then(function(r){ return r.json(); });
|
|
1018
|
+
render(state);
|
|
1019
|
+
}
|
|
1020
|
+
function render(data){
|
|
1021
|
+
document.getElementById('dir').textContent = 'Reading: ' + data.dir;
|
|
1022
|
+
document.getElementById('policy').textContent = 'Policy: ' + ((data.files || {}).policy || 'protect-mcp.json');
|
|
1023
|
+
var t = data.totals || {};
|
|
1024
|
+
document.getElementById('stats').innerHTML = [
|
|
1025
|
+
['Decisions', t.decisions || 0, 'All observed tool decisions'],
|
|
1026
|
+
['High risk', t.high_risk_tools || 0, 'Tools that can mutate, send, trade, deploy, or expose secrets'],
|
|
1027
|
+
['Exact rules', t.exact_covered || 0, 'Tools with explicit policy entries'],
|
|
1028
|
+
['Uncovered', t.uncovered || 0, 'Tools falling through without exact policy'],
|
|
1029
|
+
['Receipts', t.receipts || 0, 'Signed proof records available for audit']
|
|
1030
|
+
].map(function(x){ return '<div class="stat"><span class="muted">'+escapeHtml(x[0])+'</span><strong>'+x[1]+'</strong><small>'+escapeHtml(x[2])+'</small></div>'; }).join('');
|
|
1031
|
+
renderTools(data.tools || []);
|
|
1032
|
+
renderApprovals(data.pending_approvals || []);
|
|
1033
|
+
renderRecent(data.recent || []);
|
|
1034
|
+
renderChains(data.receipt_chains || []);
|
|
1035
|
+
renderPolicyPacks(data.policy_packs || {});
|
|
1036
|
+
renderConnectorPilots(data.connector_pilots || {});
|
|
1037
|
+
renderRegistry(data.registry || {});
|
|
1038
|
+
document.getElementById('recommendations').innerHTML = (data.recommendations || []).map(function(r){ return '<p style="margin:0 0 10px">* '+escapeHtml(r)+'</p>'; }).join('') || '<p class="muted">No recommendations yet.</p>';
|
|
1039
|
+
}
|
|
1040
|
+
function renderTools(tools){
|
|
1041
|
+
document.getElementById('tools').innerHTML = '<thead><tr><th>Risk</th><th>Tool</th><th>Coverage</th><th>Observed</th><th>Suggested guardrail</th><th>Actions</th></tr></thead><tbody>' +
|
|
1042
|
+
tools.map(function(t){
|
|
1043
|
+
var cov = t.policy_coverage || { status:'none', label:'No rule' };
|
|
1044
|
+
var s = t.suggestion || { action:'Observe', reason:'' };
|
|
1045
|
+
return '<tr><td><span class="pill '+t.risk+'">'+escapeHtml(t.risk)+'</span></td>'+
|
|
1046
|
+
'<td><strong>'+escapeHtml(t.tool)+'</strong><br><span class="muted">'+escapeHtml((t.reasons || []).join(', '))+'</span></td>'+
|
|
1047
|
+
'<td><span class="pill '+escapeHtml(cov.status)+'">'+escapeHtml(cov.label)+'</span><br><span class="muted mono">'+escapeHtml(JSON.stringify(cov.policy || {}))+'</span></td>'+
|
|
1048
|
+
'<td>'+t.calls+' calls<br><span class="allow">'+t.allows+' allow</span> \xB7 <span class="deny">'+t.denies+' deny</span> \xB7 <span class="require_approval">'+t.reviews+' review</span></td>'+
|
|
1049
|
+
'<td><strong>'+escapeHtml(s.action)+'</strong><br><span class="muted">'+escapeHtml(s.reason)+'</span></td>'+
|
|
1050
|
+
'<td><div class="row-actions"><button data-policy-tool="'+escapeHtml(t.tool)+'" data-policy-action="require_approval">Require approval</button><button class="danger" data-policy-tool="'+escapeHtml(t.tool)+'" data-policy-action="block">Block</button><button class="ghost" data-policy-tool="'+escapeHtml(t.tool)+'" data-policy-action="observe">Observe</button></div></td></tr>';
|
|
1051
|
+
}).join('') + (tools.length ? '' : '<tr><td colspan="6" class="muted">No tool calls yet. Wrap an MCP server or run the demo.</td></tr>') + '</tbody>';
|
|
1052
|
+
}
|
|
1053
|
+
function renderApprovals(rows){
|
|
1054
|
+
document.getElementById('approvals').innerHTML = rows.map(function(r){
|
|
1055
|
+
var rb = r.action_readback || {};
|
|
1056
|
+
var id = r.request_id || '';
|
|
1057
|
+
return '<div class="approval"><div class="kicker">Pending exact-action review</div><h3>'+escapeHtml(rb.summary || r.tool || 'Unknown action')+'</h3>'+
|
|
1058
|
+
'<div class="readback"><div class="label">You are approving exactly this</div><div><strong>Tool</strong>: '+escapeHtml(r.tool || 'unknown')+'</div><div><strong>Action</strong>: '+escapeHtml(rb.action || r.tool || 'unknown')+'</div><div><strong>Destination</strong>: '+escapeHtml(rb.destination || 'not declared')+'</div><div><strong>Payload hash</strong>: <span class="mono">'+escapeHtml(rb.payload_hash || 'not available')+'</span></div><div><strong>Policy basis</strong>: '+escapeHtml(r.reason_code || 'requires approval')+'</div><div class="preview"><pre>'+escapeHtml(JSON.stringify(rb.payload_preview || {}, null, 2))+'</pre></div></div>'+
|
|
1059
|
+
'<textarea id="reason-'+escapeAttr(id)+'" placeholder="Reason or instruction" style="width:100%; min-height:64px; border:1px solid var(--line); border-radius:12px; padding:10px"></textarea>'+
|
|
1060
|
+
'<div class="row-actions" style="margin-top:10px"><button data-approval-id="'+escapeHtml(id)+'" data-approval-tool="'+escapeHtml(r.tool || '')+'" data-approval-resolution="approve">Approve</button><button class="danger" data-approval-id="'+escapeHtml(id)+'" data-approval-tool="'+escapeHtml(r.tool || '')+'" data-approval-resolution="deny">Deny</button><button class="ghost" data-approval-id="'+escapeHtml(id)+'" data-approval-tool="'+escapeHtml(r.tool || '')+'" data-approval-resolution="edit">Edit</button><button class="ghost" data-approval-id="'+escapeHtml(id)+'" data-approval-tool="'+escapeHtml(r.tool || '')+'" data-approval-resolution="take_over">Take over</button></div></div>';
|
|
1061
|
+
}).join('') || '<p class="muted">No approval-required calls in the local log yet.</p>';
|
|
1062
|
+
}
|
|
1063
|
+
function renderRecent(rows){
|
|
1064
|
+
document.getElementById('recent').innerHTML = rows.slice(0,20).map(function(r){
|
|
1065
|
+
var rb = r.action_readback || {};
|
|
1066
|
+
return '<div class="chain-item"><strong class="'+escapeHtml(r.decision || '')+'">'+escapeHtml(r.decision || 'unknown')+'</strong> \xB7 '+escapeHtml(r.tool || 'unknown')+'<br><span class="muted">'+escapeHtml(rb.summary || r.reason_code || '')+'</span><br><span class="muted mono">'+escapeHtml(r.request_id || '')+'</span></div>';
|
|
1067
|
+
}).join('') || '<p class="muted">No decisions yet.</p>';
|
|
1068
|
+
}
|
|
1069
|
+
function renderChains(rows){
|
|
1070
|
+
document.getElementById('chains').innerHTML = rows.slice(0,12).map(function(c){
|
|
1071
|
+
var hashes = (c.receipts || []).map(function(r){ return '<span class="mono">'+escapeHtml((r.hash || '').slice(0,16))+'...</span>'; }).join('<br>');
|
|
1072
|
+
return '<div class="chain-item"><strong>'+escapeHtml(c.tool || 'unknown')+'</strong> <span class="pill '+(c.complete ? 'exact' : 'none')+'">'+(c.complete ? 'receipt linked' : 'no receipt')+'</span><br><span class="muted">'+escapeHtml(((c.action_readback || {}).summary) || c.reason_code || '')+'</span><br><span class="muted mono">request '+escapeHtml(c.request_id || '')+'</span><div style="margin-top:8px">'+(hashes || '<span class="muted">No signed receipt hash found</span>')+'</div></div>';
|
|
1073
|
+
}).join('') || '<p class="muted">No receipt chains yet.</p>';
|
|
1074
|
+
}
|
|
1075
|
+
function renderPolicyPacks(info){
|
|
1076
|
+
var available = info.available || [];
|
|
1077
|
+
var installed = new Set(info.installed || []);
|
|
1078
|
+
document.getElementById('policy-packs').innerHTML = available.map(function(pack){
|
|
1079
|
+
var isInstalled = installed.has(pack.id);
|
|
1080
|
+
var files = (pack.files || []).map(function(f){ return f.path; }).join(', ');
|
|
1081
|
+
return '<div class="pack '+(isInstalled ? 'installed' : '')+'">'+
|
|
1082
|
+
'<div style="display:flex;gap:8px;align-items:center"><strong>'+escapeHtml(pack.name)+'</strong><span class="pill '+(isInstalled ? 'exact' : 'wildcard')+'">'+(isInstalled ? 'installed' : escapeHtml(pack.recommendedMode || 'shadow-first'))+'</span></div>'+
|
|
1083
|
+
'<p>'+escapeHtml(pack.description || '')+'</p>'+
|
|
1084
|
+
'<small>Writes to <code>'+escapeHtml((info.directory || './cedar') + '/' + files)+'</code></small>'+
|
|
1085
|
+
'<div class="row-actions"><button data-pack-install="'+escapeHtml(pack.id)+'">'+(isInstalled ? 'Reinstall' : 'Install')+'</button><button class="ghost" data-pack-preview="'+escapeHtml(pack.id)+'">Preview</button></div>'+
|
|
1086
|
+
'<pre id="pack-preview-'+escapeAttr(pack.id)+'" style="display:none;max-height:240px">'+escapeHtml((pack.files || []).map(function(f){ return '--- '+f.path+' ---\\n'+f.contents; }).join('\\n\\n'))+'</pre>'+
|
|
1087
|
+
'</div>';
|
|
1088
|
+
}).join('') || '<p class="muted">No policy packs bundled.</p>';
|
|
1089
|
+
}
|
|
1090
|
+
function renderConnectorPilots(info){
|
|
1091
|
+
var available = info.available || [];
|
|
1092
|
+
var installed = new Set((info.installed || []).map(function(row){ return row.id; }));
|
|
1093
|
+
var doctor = {};
|
|
1094
|
+
(info.doctor || []).forEach(function(row){ doctor[row.id] = row; });
|
|
1095
|
+
document.getElementById('connector-pilots').innerHTML = available.map(function(pilot){
|
|
1096
|
+
var row = doctor[pilot.id] || {};
|
|
1097
|
+
var isInstalled = installed.has(pilot.id);
|
|
1098
|
+
var status = row.installed ? (row.usable ? 'installed' : 'needs env') : 'not installed';
|
|
1099
|
+
var statusClass = row.installed ? (row.usable ? 'exact' : 'wildcard') : 'none';
|
|
1100
|
+
var missing = (row.missing_required || []).join(', ');
|
|
1101
|
+
var tools = (pilot.tools || []).slice(0,4).map(function(tool){ return '<code>'+escapeHtml(tool)+'</code>'; }).join(' ');
|
|
1102
|
+
return '<div class="pack '+(isInstalled ? 'installed' : '')+'">'+
|
|
1103
|
+
'<div style="display:flex;gap:8px;align-items:center"><strong>'+escapeHtml(pilot.name)+'</strong><span class="pill '+statusClass+'">'+escapeHtml(status)+'</span></div>'+
|
|
1104
|
+
'<p>'+escapeHtml(pilot.description || '')+'</p>'+
|
|
1105
|
+
'<small>'+escapeHtml(pilot.value || '')+'</small>'+
|
|
1106
|
+
'<div>'+tools+'</div>'+
|
|
1107
|
+
(missing ? '<small>Missing: <code>'+escapeHtml(missing)+'</code></small>' : '<small>'+escapeHtml(row.next || 'Ready for shadow-mode review.')+'</small>')+
|
|
1108
|
+
'<details><summary>Details</summary><pre>'+escapeHtml(JSON.stringify({ setup: pilot.setup, actions: pilot.actions, doctor: row }, null, 2))+'</pre></details>'+
|
|
1109
|
+
'<div class="row-actions"><button data-connector-install="'+escapeHtml(pilot.id)+'">'+(isInstalled ? 'Reinstall' : 'Install')+'</button><button class="ghost" data-connector-doctor="1">Run doctor</button></div>'+
|
|
1110
|
+
'</div>';
|
|
1111
|
+
}).join('') || '<p class="muted">No connector pilots bundled.</p>';
|
|
1112
|
+
}
|
|
1113
|
+
function renderRegistry(reg){
|
|
1114
|
+
var boundaryClass = reg.hosted ? 'exact' : reg.registry_exists ? 'wildcard' : 'none';
|
|
1115
|
+
document.getElementById('registry').innerHTML =
|
|
1116
|
+
'<div class="chain-item"><span class="pill '+boundaryClass+'">'+escapeHtml(reg.boundary || 'not configured')+'</span>'+
|
|
1117
|
+
'<p style="margin-top:8px"><strong>'+escapeHtml(reg.org_name || 'No org identity yet')+'</strong></p>'+
|
|
1118
|
+
'<p class="muted">Digests: '+(reg.records || 0)+' \xB7 Anchors: '+(reg.anchors || 0)+'</p>'+
|
|
1119
|
+
'<p class="muted mono" style="margin-top:6px">'+escapeHtml(reg.registry_path || '')+'</p></div>'+
|
|
1120
|
+
'<div class="field"><label>Org name</label><input id="registry-org" placeholder="Meridian Global Macro" value="'+escapeHtml(reg.org_name || '')+'"></div>'+
|
|
1121
|
+
'<div class="field"><label>Hosted token (optional, not stored)</label><input id="registry-token" type="password" placeholder="SCOPEBLIND_TOKEN for hosted digest anchoring"></div>'+
|
|
1122
|
+
'<div class="row-actions" style="margin-top:10px"><button data-registry-anchor="local">Create local registry preview</button><button class="secondary" data-registry-anchor="hosted">Hosted digest anchor</button></div>'+
|
|
1123
|
+
'<small style="display:block;margin-top:8px">Hosted mode uploads digest metadata only: receipt hash, byte count, receipt type, request id, local issuer/kid, org id, billing account, and public keys. It does not upload prompts, tool payloads, outputs, raw receipts, or private keys.</small>';
|
|
1124
|
+
}
|
|
1125
|
+
async function setPolicy(tool, action){
|
|
1126
|
+
var res = await fetch('/api/tool-policy', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ tool: tool, action: action }) });
|
|
1127
|
+
if(!res.ok){ toast('Policy update failed'); return; }
|
|
1128
|
+
toast('Policy updated: '+tool+' -> '+action+'. Restart the wrapper to apply.');
|
|
1129
|
+
await refresh();
|
|
1130
|
+
}
|
|
1131
|
+
async function resolveApproval(requestId, tool, resolution){
|
|
1132
|
+
var reasonEl = document.getElementById('reason-'+escapeAttr(requestId));
|
|
1133
|
+
var reason = reasonEl ? reasonEl.value : '';
|
|
1134
|
+
var edited = undefined;
|
|
1135
|
+
if(resolution === 'edit'){
|
|
1136
|
+
var raw = prompt('Paste edited JSON payload. This records the edit instruction; rerun the tool with the edited payload.');
|
|
1137
|
+
if(raw){ try { edited = JSON.parse(raw); } catch(e){ toast('Edit payload is not valid JSON'); return; } }
|
|
1138
|
+
}
|
|
1139
|
+
var res = await fetch('/api/approval/resolve', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ request_id: requestId, tool: tool, resolution: resolution, reason: reason, edited_payload: edited }) });
|
|
1140
|
+
var body = await res.json().catch(function(){ return {}; });
|
|
1141
|
+
toast(resolution+' recorded'+(body.forwarded && body.forwarded.ok ? ' and forwarded to live gateway' : ' locally'));
|
|
1142
|
+
}
|
|
1143
|
+
async function installPack(pack){
|
|
1144
|
+
var res = await fetch('/api/policy-packs/install', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ pack: pack, force: true }) });
|
|
1145
|
+
var body = await res.json().catch(function(){ return {}; });
|
|
1146
|
+
if(!res.ok){ toast(body.error || 'Policy pack install failed'); return; }
|
|
1147
|
+
toast('Installed '+pack+' into '+body.dir);
|
|
1148
|
+
await refresh();
|
|
1149
|
+
}
|
|
1150
|
+
async function installConnector(pilot){
|
|
1151
|
+
var res = await fetch('/api/connectors/install', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ pilot: pilot, force: true }) });
|
|
1152
|
+
var body = await res.json().catch(function(){ return {}; });
|
|
1153
|
+
if(!res.ok){ toast(body.error || 'Connector install failed'); return; }
|
|
1154
|
+
toast('Installed '+pilot+' into '+body.dir);
|
|
1155
|
+
await refresh();
|
|
1156
|
+
}
|
|
1157
|
+
function togglePackPreview(pack){
|
|
1158
|
+
var el = document.getElementById('pack-preview-'+escapeAttr(pack));
|
|
1159
|
+
if(el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
1160
|
+
}
|
|
1161
|
+
async function anchorRegistry(mode){
|
|
1162
|
+
var org = document.getElementById('registry-org') ? document.getElementById('registry-org').value : '';
|
|
1163
|
+
var token = document.getElementById('registry-token') ? document.getElementById('registry-token').value : '';
|
|
1164
|
+
var body = { org_name: org, hosted: mode === 'hosted', token: token };
|
|
1165
|
+
var res = await fetch('/api/registry/anchor', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body) });
|
|
1166
|
+
var out = await res.json().catch(function(){ return {}; });
|
|
1167
|
+
if(!res.ok){ toast(out.error || 'Registry anchor failed'); return; }
|
|
1168
|
+
toast((out.uploaded ? 'Hosted anchor complete' : 'Local registry preview written') + ': ' + out.records + ' digest(s)');
|
|
1169
|
+
await refresh();
|
|
1170
|
+
}
|
|
1171
|
+
async function exportBundle(){
|
|
1172
|
+
var res = await fetch('/api/audit-bundle');
|
|
1173
|
+
if(!res.ok){
|
|
1174
|
+
var err = await res.json().catch(function(){ return {}; });
|
|
1175
|
+
toast(err.message || 'Audit bundle export requires signed receipts');
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
var blob = await res.blob();
|
|
1179
|
+
var url = URL.createObjectURL(blob);
|
|
1180
|
+
var a = document.createElement('a');
|
|
1181
|
+
a.href = url;
|
|
1182
|
+
a.download = 'protect-mcp-audit-bundle.json';
|
|
1183
|
+
document.body.appendChild(a);
|
|
1184
|
+
a.click();
|
|
1185
|
+
a.remove();
|
|
1186
|
+
setTimeout(function(){ URL.revokeObjectURL(url); }, 1000);
|
|
1187
|
+
}
|
|
1188
|
+
document.addEventListener('click', function(ev){
|
|
1189
|
+
var target = ev.target && ev.target.closest ? ev.target.closest('[data-policy-tool],[data-approval-id],[data-pack-install],[data-pack-preview],[data-registry-anchor],[data-connector-install],[data-connector-doctor]') : null;
|
|
1190
|
+
if(!target) return;
|
|
1191
|
+
var connectorInstall = target.getAttribute('data-connector-install');
|
|
1192
|
+
if(connectorInstall){ installConnector(connectorInstall); return; }
|
|
1193
|
+
if(target.getAttribute('data-connector-doctor')){ toast('Connector doctor refreshed. Missing secrets are shown as names only, never values.'); refresh(); return; }
|
|
1194
|
+
var packInstall = target.getAttribute('data-pack-install');
|
|
1195
|
+
if(packInstall){ installPack(packInstall); return; }
|
|
1196
|
+
var packPreview = target.getAttribute('data-pack-preview');
|
|
1197
|
+
if(packPreview){ togglePackPreview(packPreview); return; }
|
|
1198
|
+
var anchorMode = target.getAttribute('data-registry-anchor');
|
|
1199
|
+
if(anchorMode){ anchorRegistry(anchorMode); return; }
|
|
1200
|
+
var policyTool = target.getAttribute('data-policy-tool');
|
|
1201
|
+
if(policyTool){
|
|
1202
|
+
setPolicy(policyTool, target.getAttribute('data-policy-action') || 'require_approval');
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
var approvalId = target.getAttribute('data-approval-id');
|
|
1206
|
+
if(approvalId){
|
|
1207
|
+
resolveApproval(approvalId, target.getAttribute('data-approval-tool') || '', target.getAttribute('data-approval-resolution') || 'deny');
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
function toast(msg){ var el=document.getElementById('toast'); el.textContent=msg; el.style.display='block'; setTimeout(function(){el.style.display='none';}, 4200); }
|
|
1211
|
+
function escapeHtml(v){ return String(v == null ? '' : v).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
|
|
1212
|
+
function escapeAttr(v){ return String(v || '').replace(/[^A-Za-z0-9_-]/g,'_'); }
|
|
1213
|
+
refresh();
|
|
1214
|
+
</script>
|
|
1215
|
+
</body>
|
|
1216
|
+
</html>`;
|
|
1217
|
+
}
|
|
1218
|
+
async function handleDashboard(argv) {
|
|
1219
|
+
const { createServer } = await import("http");
|
|
1220
|
+
const { execFile } = await import("child_process");
|
|
1221
|
+
const { resolve } = await import("path");
|
|
1222
|
+
const port = commandNeedsValue(argv, "--port") ? parseInt(flagValue(argv, "--port") || "9877", 10) : 9877;
|
|
1223
|
+
const dir = resolve(commandNeedsValue(argv, "--dir") ? flagValue(argv, "--dir") || process.cwd() : process.cwd());
|
|
1224
|
+
const policyPath = resolve(flagValue(argv, "--policy") || joinCli(dir, "protect-mcp.json"));
|
|
1225
|
+
const approvalEndpoint = flagValue(argv, "--approval-endpoint");
|
|
1226
|
+
const approvalNonce = flagValue(argv, "--approval-nonce");
|
|
1227
|
+
const open = argv.includes("--open");
|
|
1228
|
+
const server = createServer((req, res) => {
|
|
1229
|
+
void (async () => {
|
|
1230
|
+
try {
|
|
1231
|
+
const url2 = new URL(req.url || "/", "http://127.0.0.1");
|
|
1232
|
+
if (url2.pathname === "/api/summary") {
|
|
1233
|
+
const body = JSON.stringify(buildDashboardSummary(dir, policyPath), null, 2);
|
|
1234
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1235
|
+
res.end(body);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (url2.pathname === "/api/tool-policy" && req.method === "POST") {
|
|
1239
|
+
const body = await readJsonBody(req);
|
|
1240
|
+
const tool = typeof body.tool === "string" ? body.tool : "";
|
|
1241
|
+
const action = body.action === "block" || body.action === "observe" ? body.action : "require_approval";
|
|
1242
|
+
if (!tool) {
|
|
1243
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1244
|
+
res.end(JSON.stringify({ error: "missing_tool" }));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const policy = writeToolPolicy(policyPath, tool, action);
|
|
1248
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1249
|
+
res.end(JSON.stringify({ ok: true, policy_path: policyPath, policy }));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (url2.pathname === "/api/policy-packs/install" && req.method === "POST") {
|
|
1253
|
+
const body = await readJsonBody(req);
|
|
1254
|
+
const pack = typeof body.pack === "string" ? body.pack : "";
|
|
1255
|
+
if (!pack) {
|
|
1256
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1257
|
+
res.end(JSON.stringify({ error: "missing_policy_pack" }));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const installed = installPolicyPackToDir(dir, pack, Boolean(body.force));
|
|
1261
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1262
|
+
res.end(JSON.stringify({
|
|
1263
|
+
ok: true,
|
|
1264
|
+
...installed,
|
|
1265
|
+
installed: installedPolicyPackIds(dir)
|
|
1266
|
+
}));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (url2.pathname === "/api/connectors/install" && req.method === "POST") {
|
|
1270
|
+
const body = await readJsonBody(req);
|
|
1271
|
+
const pilot = typeof body.pilot === "string" ? body.pilot : "";
|
|
1272
|
+
if (!pilot) {
|
|
1273
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1274
|
+
res.end(JSON.stringify({ error: "missing_connector_pilot" }));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const installed = writeConnectorPilots({ dir, ids: [pilot], force: Boolean(body.force) });
|
|
1278
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1279
|
+
res.end(JSON.stringify({
|
|
1280
|
+
ok: true,
|
|
1281
|
+
dir: installed.directory,
|
|
1282
|
+
written: installed.written,
|
|
1283
|
+
installed: readInstalledConnectorPilots(dir),
|
|
1284
|
+
doctor: connectorDoctor(dir)
|
|
1285
|
+
}));
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (url2.pathname === "/api/registry/anchor" && req.method === "POST") {
|
|
1289
|
+
const body = await readJsonBody(req);
|
|
1290
|
+
const { createReceiptRegistry } = await import("./receipt-registry-6CAOY6RP.mjs");
|
|
1291
|
+
try {
|
|
1292
|
+
const hosted = Boolean(body.hosted);
|
|
1293
|
+
const result = await createReceiptRegistry({
|
|
1294
|
+
dir,
|
|
1295
|
+
orgName: typeof body.org_name === "string" && body.org_name.trim() ? body.org_name.trim() : void 0,
|
|
1296
|
+
orgId: typeof body.org_id === "string" && body.org_id.trim() ? body.org_id.trim() : void 0,
|
|
1297
|
+
billingAccountId: typeof body.billing_account_id === "string" && body.billing_account_id.trim() ? body.billing_account_id.trim() : void 0,
|
|
1298
|
+
hosted,
|
|
1299
|
+
token: typeof body.token === "string" && body.token.trim() ? body.token.trim() : process.env.SCOPEBLIND_TOKEN,
|
|
1300
|
+
endpoint: typeof body.endpoint === "string" && body.endpoint.trim() ? body.endpoint.trim() : hosted ? process.env.SCOPEBLIND_REGISTRY_ENDPOINT || "https://api.scopeblind.com" : void 0,
|
|
1301
|
+
verifierBaseUrl: typeof body.verifier_base === "string" && body.verifier_base.trim() ? body.verifier_base.trim() : process.env.SCOPEBLIND_VERIFIER_BASE || "https://legate.scopeblind.com"
|
|
1302
|
+
});
|
|
1303
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1304
|
+
res.end(JSON.stringify({
|
|
1305
|
+
ok: true,
|
|
1306
|
+
uploaded: result.uploaded,
|
|
1307
|
+
records: result.registry.records.length,
|
|
1308
|
+
anchors: result.registry.anchors.length,
|
|
1309
|
+
registry_path: result.registryPath,
|
|
1310
|
+
verifier_path: result.verifierPath,
|
|
1311
|
+
registry: dashboardRegistryStatus(dir)
|
|
1312
|
+
}));
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
res.writeHead(409, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1315
|
+
res.end(JSON.stringify({
|
|
1316
|
+
error: "registry_anchor_unavailable",
|
|
1317
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1318
|
+
next_step: "Run protect-mcp with signing enabled so decisions are written as signed receipts, then try again."
|
|
1319
|
+
}));
|
|
1320
|
+
}
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (url2.pathname === "/api/approval/resolve" && req.method === "POST") {
|
|
1324
|
+
const body = await readJsonBody(req);
|
|
1325
|
+
const result = await recordApprovalResolution({ dir, approvalEndpoint, approvalNonce, body });
|
|
1326
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1327
|
+
res.end(JSON.stringify(result));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (url2.pathname === "/api/audit-bundle") {
|
|
1331
|
+
let bundle;
|
|
1332
|
+
try {
|
|
1333
|
+
bundle = await buildAuditBundleForDir(dir);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
res.writeHead(409, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1336
|
+
res.end(JSON.stringify({
|
|
1337
|
+
error: "audit_bundle_unavailable",
|
|
1338
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1339
|
+
next_step: "Run protect-mcp with signing enabled so decisions are written as signed receipts, then export again."
|
|
1340
|
+
}));
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
res.writeHead(200, {
|
|
1344
|
+
"content-type": "application/json; charset=utf-8",
|
|
1345
|
+
"content-disposition": 'attachment; filename="protect-mcp-audit-bundle.json"',
|
|
1346
|
+
"cache-control": "no-store"
|
|
1347
|
+
});
|
|
1348
|
+
res.end(JSON.stringify(bundle, null, 2) + "\n");
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
res.writeHead(200, {
|
|
1352
|
+
"content-type": "text/html; charset=utf-8",
|
|
1353
|
+
"cache-control": "no-store",
|
|
1354
|
+
"content-security-policy": "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'"
|
|
1355
|
+
});
|
|
1356
|
+
res.end(dashboardHtml());
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
1359
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1360
|
+
}
|
|
1361
|
+
})();
|
|
1362
|
+
});
|
|
1363
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
1364
|
+
server.once("error", rejectListen);
|
|
1365
|
+
server.listen(port, "127.0.0.1", () => resolveListen());
|
|
1366
|
+
});
|
|
1367
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1368
|
+
process.stderr.write(`
|
|
1369
|
+
${bold("protect-mcp dashboard")}
|
|
1370
|
+
|
|
1371
|
+
`);
|
|
1372
|
+
process.stderr.write(` Local URL: ${url}
|
|
1373
|
+
`);
|
|
1374
|
+
process.stderr.write(` Reading: ${dir}
|
|
1375
|
+
`);
|
|
1376
|
+
process.stderr.write(` Policy: ${policyPath}
|
|
1377
|
+
`);
|
|
1378
|
+
process.stderr.write(` Network: 127.0.0.1 only; no uploads
|
|
1379
|
+
|
|
1380
|
+
`);
|
|
1381
|
+
if (open) {
|
|
1382
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1383
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1384
|
+
execFile(opener, args, () => {
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function draftPolicyFromSummary(summary) {
|
|
1389
|
+
const files = summary.files || {};
|
|
1390
|
+
const rows = Array.isArray(summary.tools) ? summary.tools : [];
|
|
1391
|
+
const tools = {
|
|
1392
|
+
"*": { rate_limit: "100/hour" }
|
|
1393
|
+
};
|
|
1394
|
+
for (const row of rows) {
|
|
1395
|
+
if (!row.tool || row.tool === "unknown") continue;
|
|
1396
|
+
const suggestion = row.suggestion || suggestedGuardrailFor(row.tool, row.risk || "low", row.reasons || []);
|
|
1397
|
+
tools[row.tool] = suggestion.policy;
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
tools,
|
|
1401
|
+
default_tier: "unknown",
|
|
1402
|
+
signing: files.key_exists ? {
|
|
1403
|
+
key_path: "./keys/gateway.json",
|
|
1404
|
+
issuer: "protect-mcp"
|
|
1405
|
+
} : void 0,
|
|
1406
|
+
notes: [
|
|
1407
|
+
"Generated from local shadow-mode inventory.",
|
|
1408
|
+
"Review before running with --enforce.",
|
|
1409
|
+
"High-risk transaction, deployment, external-send, and database tools require approval.",
|
|
1410
|
+
"Destructive and secret-handling tools are blocked by default."
|
|
1411
|
+
]
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function writeToolPolicy(policyPath, tool, action) {
|
|
1415
|
+
const existing = loadPolicyJson(policyPath) || { tools: {}, default_tier: "unknown" };
|
|
1416
|
+
const tools = existing.tools && typeof existing.tools === "object" ? { ...existing.tools } : {};
|
|
1417
|
+
if (action === "require_approval") {
|
|
1418
|
+
tools[tool] = { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" };
|
|
1419
|
+
} else if (action === "block") {
|
|
1420
|
+
tools[tool] = { block: true, min_tier: "privileged" };
|
|
1421
|
+
} else {
|
|
1422
|
+
tools[tool] = { rate_limit: "100/hour" };
|
|
1423
|
+
}
|
|
1424
|
+
const next = {
|
|
1425
|
+
...existing,
|
|
1426
|
+
tools,
|
|
1427
|
+
default_tier: existing.default_tier || "unknown"
|
|
1428
|
+
};
|
|
1429
|
+
writeFileSyncCli(policyPath, JSON.stringify(next, null, 2) + "\n");
|
|
1430
|
+
return next;
|
|
1431
|
+
}
|
|
1432
|
+
function policyPackDirectory(dir) {
|
|
1433
|
+
return joinCli(dir, "cedar");
|
|
1434
|
+
}
|
|
1435
|
+
function installedPolicyPackIds(dir) {
|
|
1436
|
+
const cedarDir = policyPackDirectory(dir);
|
|
1437
|
+
return POLICY_PACKS.filter(
|
|
1438
|
+
(pack) => pack.files.every((file) => existsSyncCli(joinCli(cedarDir, file.path)))
|
|
1439
|
+
).map((pack) => pack.id);
|
|
1440
|
+
}
|
|
1441
|
+
function installPolicyPackToDir(dir, packId, force = false) {
|
|
1442
|
+
const packs = packId === "all" ? POLICY_PACKS : [getPolicyPack(packId)].filter(Boolean);
|
|
1443
|
+
if (packs.length === 0) throw new Error(`Unknown policy pack: ${packId}`);
|
|
1444
|
+
const outDir = policyPackDirectory(dir);
|
|
1445
|
+
mkdirSyncCli(outDir, { recursive: true });
|
|
1446
|
+
const written = [];
|
|
1447
|
+
for (const pack of packs) {
|
|
1448
|
+
for (const file of pack.files) {
|
|
1449
|
+
const outPath = joinCli(outDir, file.path);
|
|
1450
|
+
if (existsSyncCli(outPath) && !force) {
|
|
1451
|
+
throw new Error(`Refusing to overwrite ${outPath}. Pass force=true if intentional.`);
|
|
1452
|
+
}
|
|
1453
|
+
mkdirSyncCli(dirnameCli(outPath), { recursive: true });
|
|
1454
|
+
writeFileSyncCli(outPath, file.contents.endsWith("\n") ? file.contents : `${file.contents}
|
|
1455
|
+
`);
|
|
1456
|
+
written.push(outPath);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return { dir: outDir, written, packs: packs.map((pack) => pack.id) };
|
|
1460
|
+
}
|
|
1461
|
+
function dashboardRegistryStatus(dir) {
|
|
1462
|
+
const identityPath = joinCli(dir, ".protect-mcp-org.json");
|
|
1463
|
+
const registryPath = joinCli(dir, ".protect-mcp-registry.json");
|
|
1464
|
+
const verifierPath = joinCli(dir, "scopeblind-verifier.html");
|
|
1465
|
+
const identity = existsSyncCli(identityPath) ? (() => {
|
|
1466
|
+
try {
|
|
1467
|
+
return JSON.parse(readFileSyncCli(identityPath, "utf-8"));
|
|
1468
|
+
} catch {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
})() : null;
|
|
1472
|
+
const registry = existsSyncCli(registryPath) ? (() => {
|
|
1473
|
+
try {
|
|
1474
|
+
return JSON.parse(readFileSyncCli(registryPath, "utf-8"));
|
|
1475
|
+
} catch {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
})() : null;
|
|
1479
|
+
const anchors = Array.isArray(registry?.anchors) ? registry.anchors : [];
|
|
1480
|
+
const hosted = anchors.some((anchor) => anchor.timestamp_source === "scopeblind-hosted");
|
|
1481
|
+
return {
|
|
1482
|
+
identity_exists: existsSyncCli(identityPath),
|
|
1483
|
+
registry_exists: existsSyncCli(registryPath),
|
|
1484
|
+
verifier_exists: existsSyncCli(verifierPath),
|
|
1485
|
+
identity_path: identityPath,
|
|
1486
|
+
registry_path: registryPath,
|
|
1487
|
+
verifier_path: verifierPath,
|
|
1488
|
+
org_name: identity?.org_name || (registry?.org && typeof registry.org === "object" ? registry.org.org_name : null),
|
|
1489
|
+
org_id: identity?.org_id || (registry?.org && typeof registry.org === "object" ? registry.org.org_id : null),
|
|
1490
|
+
billing_account_id: identity?.billing_account_id || (registry?.billing && typeof registry.billing === "object" ? registry.billing.billing_account_id : null),
|
|
1491
|
+
records: Array.isArray(registry?.records) ? registry.records.length : 0,
|
|
1492
|
+
anchors: anchors.length,
|
|
1493
|
+
hosted,
|
|
1494
|
+
boundary: hosted ? "hosted digest anchor" : registry ? "local preview only" : "not configured"
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
async function readJsonBody(req) {
|
|
1498
|
+
const chunks = [];
|
|
1499
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1500
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
1501
|
+
return raw ? JSON.parse(raw) : {};
|
|
1502
|
+
}
|
|
1503
|
+
async function buildAuditBundleForDir(dir) {
|
|
1504
|
+
const { createAuditBundle } = await import("./bundle-YUUHAG7O.mjs");
|
|
1505
|
+
const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
|
|
1506
|
+
const keyPath = joinCli(dir, "keys", "gateway.json");
|
|
1507
|
+
if (!existsSyncCli(receiptPath)) throw new Error("No receipt file found.");
|
|
1508
|
+
if (!existsSyncCli(keyPath)) throw new Error("No signing key found.");
|
|
1509
|
+
const receipts = parseJsonlFile(receiptPath);
|
|
1510
|
+
if (receipts.length === 0) throw new Error("No signed receipts found.");
|
|
1511
|
+
const keyData = JSON.parse(readFileSyncCli(keyPath, "utf-8"));
|
|
1512
|
+
return createAuditBundle({
|
|
1513
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
1514
|
+
receipts,
|
|
1515
|
+
selectiveDisclosures: collectSelectiveDisclosurePackages(dir),
|
|
1516
|
+
signingKeys: [{
|
|
1517
|
+
kty: "OKP",
|
|
1518
|
+
crv: "Ed25519",
|
|
1519
|
+
kid: keyData.kid || "unknown",
|
|
1520
|
+
x: Buffer.from(keyData.publicKey || "", "hex").toString("base64url"),
|
|
1521
|
+
use: "sig"
|
|
1522
|
+
}]
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
function collectSelectiveDisclosurePackages(dir) {
|
|
1526
|
+
const out = [];
|
|
1527
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1528
|
+
const candidates = [];
|
|
1529
|
+
const receiptsDir = joinCli(dir, "receipts");
|
|
1530
|
+
if (existsSyncCli(receiptsDir)) {
|
|
1531
|
+
for (const name of readdirSyncCli(receiptsDir)) {
|
|
1532
|
+
if (name.includes("selective-disclosure") && name.endsWith(".json")) {
|
|
1533
|
+
candidates.push(joinCli(receiptsDir, name));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const jsonlPath = joinCli(dir, ".protect-mcp-selective-disclosures.jsonl");
|
|
1538
|
+
if (existsSyncCli(jsonlPath)) {
|
|
1539
|
+
for (const line of readFileSyncCli(jsonlPath, "utf-8").split("\n").map((s) => s.trim()).filter(Boolean)) {
|
|
1540
|
+
try {
|
|
1541
|
+
const parsed = JSON.parse(line);
|
|
1542
|
+
addSelectiveDisclosure(out, seen, parsed);
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
for (const path of candidates) {
|
|
1548
|
+
try {
|
|
1549
|
+
const parsed = JSON.parse(readFileSyncCli(path, "utf-8"));
|
|
1550
|
+
addSelectiveDisclosure(out, seen, parsed);
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return out;
|
|
1555
|
+
}
|
|
1556
|
+
function addSelectiveDisclosure(out, seen, parsed) {
|
|
1557
|
+
if (parsed?.type !== "scopeblind.selective_disclosure.v0") return;
|
|
1558
|
+
const key = [
|
|
1559
|
+
parsed.parent_receipt_hash || "",
|
|
1560
|
+
Array.isArray(parsed.disclosed_fields) ? parsed.disclosed_fields.slice().sort().join(",") : "",
|
|
1561
|
+
Array.isArray(parsed.hidden_fields) ? parsed.hidden_fields.slice().sort().join(",") : ""
|
|
1562
|
+
].join("|");
|
|
1563
|
+
if (seen.has(key)) return;
|
|
1564
|
+
seen.add(key);
|
|
1565
|
+
out.push(parsed);
|
|
1566
|
+
}
|
|
1567
|
+
async function recordApprovalResolution(opts) {
|
|
1568
|
+
const resolution = String(opts.body.resolution || "deny");
|
|
1569
|
+
const requestId = String(opts.body.request_id || "");
|
|
1570
|
+
const tool = String(opts.body.tool || "unknown");
|
|
1571
|
+
const record = {
|
|
1572
|
+
type: "scopeblind.approval_resolution.v1",
|
|
1573
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1574
|
+
request_id: requestId,
|
|
1575
|
+
tool,
|
|
1576
|
+
resolution,
|
|
1577
|
+
reason: typeof opts.body.reason === "string" ? opts.body.reason.slice(0, 1e3) : "",
|
|
1578
|
+
edited_payload: opts.body.edited_payload || void 0,
|
|
1579
|
+
takeover_note: opts.body.takeover_note || void 0,
|
|
1580
|
+
payload_hash: opts.body.payload_hash || void 0
|
|
1581
|
+
};
|
|
1582
|
+
appendFileSyncCli(joinCli(opts.dir, ".protect-mcp-approval-resolutions.jsonl"), JSON.stringify(record) + "\n");
|
|
1583
|
+
let forwarded = null;
|
|
1584
|
+
if (resolution === "approve" && opts.approvalEndpoint && opts.approvalNonce) {
|
|
1585
|
+
const endpoint = opts.approvalEndpoint.replace(/\/$/, "") + "/approve";
|
|
1586
|
+
const response = await fetch(endpoint, {
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
headers: { "content-type": "application/json" },
|
|
1589
|
+
body: JSON.stringify({
|
|
1590
|
+
request_id: requestId,
|
|
1591
|
+
tool,
|
|
1592
|
+
mode: "once",
|
|
1593
|
+
nonce: opts.approvalNonce
|
|
1594
|
+
})
|
|
1595
|
+
});
|
|
1596
|
+
forwarded = {
|
|
1597
|
+
ok: response.ok,
|
|
1598
|
+
status: response.status,
|
|
1599
|
+
body: await response.text().catch(() => "")
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
return { recorded: true, resolution: record, forwarded };
|
|
1603
|
+
}
|
|
1604
|
+
async function handleRecommend(argv) {
|
|
1605
|
+
const { writeFileSync } = await import("fs");
|
|
1606
|
+
const { resolve } = await import("path");
|
|
1607
|
+
const dir = resolve(commandNeedsValue(argv, "--dir") ? flagValue(argv, "--dir") || process.cwd() : process.cwd());
|
|
1608
|
+
const outputPath = resolve(flagValue(argv, "--output") || "protect-mcp.recommended.json");
|
|
1609
|
+
const write = argv.includes("--write");
|
|
1610
|
+
const summary = buildDashboardSummary(dir);
|
|
1611
|
+
const totals = summary.totals;
|
|
1612
|
+
const policy = draftPolicyFromSummary(summary);
|
|
1613
|
+
const rows = Array.isArray(summary.tools) ? summary.tools : [];
|
|
1614
|
+
process.stdout.write(`
|
|
1615
|
+
${bold("protect-mcp recommend")}
|
|
1616
|
+
|
|
1617
|
+
`);
|
|
1618
|
+
process.stdout.write(` Source: ${dir}
|
|
1619
|
+
`);
|
|
1620
|
+
process.stdout.write(` Decisions: ${totals.decisions || 0}
|
|
1621
|
+
`);
|
|
1622
|
+
process.stdout.write(` Tools: ${totals.tools || 0}
|
|
1623
|
+
|
|
1624
|
+
`);
|
|
1625
|
+
if (rows.length === 0) {
|
|
1626
|
+
process.stdout.write(`No tool calls found yet. First run:
|
|
1627
|
+
|
|
1628
|
+
`);
|
|
1629
|
+
process.stdout.write(` ${dim("npx protect-mcp wrap -- node your-mcp-server.js")}
|
|
1630
|
+
`);
|
|
1631
|
+
process.stdout.write(` ${dim("npx protect-mcp dashboard --open")}
|
|
1632
|
+
|
|
1633
|
+
`);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
for (const row of rows) {
|
|
1637
|
+
const suggestion = row.suggestion || suggestedGuardrailFor(row.tool, row.risk, row.reasons);
|
|
1638
|
+
process.stdout.write(` - ${row.tool}: ${bold(suggestion.action)} (${row.risk})
|
|
1639
|
+
`);
|
|
1640
|
+
process.stdout.write(` ${dim(suggestion.reason)}
|
|
1641
|
+
`);
|
|
1642
|
+
}
|
|
1643
|
+
const body = JSON.stringify(policy, null, 2) + "\n";
|
|
1644
|
+
if (!write) {
|
|
1645
|
+
process.stdout.write(`
|
|
1646
|
+
Dry run only. Write the policy with:
|
|
1647
|
+
`);
|
|
1648
|
+
process.stdout.write(` ${dim("npx protect-mcp recommend --write")}
|
|
1649
|
+
|
|
1650
|
+
`);
|
|
1651
|
+
process.stdout.write(dim(body));
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
writeFileSync(outputPath, body);
|
|
1655
|
+
process.stdout.write(`
|
|
1656
|
+
${green("\u2713 Wrote recommended policy")}
|
|
1657
|
+
`);
|
|
1658
|
+
process.stdout.write(` Output: ${outputPath}
|
|
1659
|
+
`);
|
|
1660
|
+
process.stdout.write(` Review it, then restart your wrapper with:
|
|
1661
|
+
`);
|
|
1662
|
+
process.stdout.write(` ${dim(shellCommand("npx", ["protect-mcp", "--policy", outputPath, "--enforce", "--", "node", "your-mcp-server.js"]))}
|
|
1663
|
+
|
|
1664
|
+
`);
|
|
1665
|
+
}
|
|
1666
|
+
async function handleWrap(argv) {
|
|
1667
|
+
const { existsSync, readFileSync, writeFileSync } = await import("fs");
|
|
1668
|
+
const { resolve } = await import("path");
|
|
1669
|
+
const configFlag = flagValue(argv, "--config");
|
|
1670
|
+
const cedarFlag = flagValue(argv, "--cedar");
|
|
1671
|
+
const enforce = argv.includes("--enforce");
|
|
1672
|
+
const write = argv.includes("--write");
|
|
1673
|
+
const claudeDesktop = argv.includes("--claude-desktop") || argv.includes("--claude");
|
|
1674
|
+
const serverName = flagValue(argv, "--server");
|
|
1675
|
+
const separator = argv.indexOf("--");
|
|
1676
|
+
const childCommand = separator >= 0 ? argv.slice(separator + 1).filter(Boolean) : [];
|
|
1677
|
+
const configPath = cedarFlag ? void 0 : resolve(configFlag || await ensureLocalConfig(process.cwd()));
|
|
1678
|
+
const cedarDir = cedarFlag ? resolve(cedarFlag) : void 0;
|
|
1679
|
+
if (childCommand.length > 0) {
|
|
1680
|
+
const args = wrapperArgsFor(childCommand, { configPath, cedarDir, enforce });
|
|
1681
|
+
process.stdout.write(`
|
|
1682
|
+
${bold("protect-mcp wrap")}
|
|
1683
|
+
|
|
1684
|
+
`);
|
|
1685
|
+
process.stdout.write(`Use this command in your MCP client config:
|
|
1686
|
+
|
|
1687
|
+
`);
|
|
1688
|
+
process.stdout.write(` ${shellCommand("npx", args)}
|
|
1689
|
+
|
|
1690
|
+
`);
|
|
1691
|
+
process.stdout.write(`Claude Desktop JSON snippet:
|
|
1692
|
+
|
|
1693
|
+
`);
|
|
1694
|
+
process.stdout.write(dim(JSON.stringify({
|
|
1695
|
+
command: "npx",
|
|
1696
|
+
args
|
|
1697
|
+
}, null, 2)) + "\n\n");
|
|
1698
|
+
process.stdout.write(`Then inspect calls with: ${dim("npx protect-mcp dashboard --open")}
|
|
1699
|
+
|
|
1700
|
+
`);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const claudePath = resolve(flagValue(argv, "--path") || claudeDesktopConfigPath());
|
|
1704
|
+
if (!claudeDesktop && !existsSync(claudePath)) {
|
|
1705
|
+
process.stdout.write(`
|
|
1706
|
+
${bold("protect-mcp wrap")}
|
|
1707
|
+
|
|
1708
|
+
`);
|
|
1709
|
+
process.stdout.write(`No command was passed after "--" and no Claude Desktop config was found.
|
|
1710
|
+
|
|
1711
|
+
`);
|
|
1712
|
+
process.stdout.write(`Examples:
|
|
1713
|
+
`);
|
|
1714
|
+
process.stdout.write(` ${dim("npx protect-mcp wrap -- node server.js")}
|
|
1715
|
+
`);
|
|
1716
|
+
process.stdout.write(` ${dim("npx protect-mcp wrap --claude-desktop --write")}
|
|
1717
|
+
|
|
1718
|
+
`);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (!existsSync(claudePath)) {
|
|
1722
|
+
process.stderr.write(`protect-mcp wrap: Claude Desktop config not found at ${claudePath}
|
|
1723
|
+
`);
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
let parsed;
|
|
1727
|
+
try {
|
|
1728
|
+
parsed = JSON.parse(readFileSync(claudePath, "utf-8"));
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
process.stderr.write(`protect-mcp wrap: could not parse ${claudePath}: ${err instanceof Error ? err.message : err}
|
|
1731
|
+
`);
|
|
1732
|
+
process.exit(1);
|
|
1733
|
+
}
|
|
1734
|
+
const servers = parsed.mcpServers || {};
|
|
1735
|
+
const names = Object.keys(servers).filter((name) => !serverName || name === serverName);
|
|
1736
|
+
if (names.length === 0) {
|
|
1737
|
+
process.stderr.write(`protect-mcp wrap: no MCP servers found${serverName ? ` matching "${serverName}"` : ""}.
|
|
1738
|
+
`);
|
|
1739
|
+
process.exit(1);
|
|
1740
|
+
}
|
|
1741
|
+
const next = { ...parsed, mcpServers: { ...servers } };
|
|
1742
|
+
const changes = [];
|
|
1743
|
+
for (const name of names) {
|
|
1744
|
+
const before = servers[name] || {};
|
|
1745
|
+
const originalCommand = before.command;
|
|
1746
|
+
const originalArgs = Array.isArray(before.args) ? before.args : [];
|
|
1747
|
+
if (!originalCommand) {
|
|
1748
|
+
changes.push({ name, before, after: before, skipped: "missing command" });
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
if (originalCommand === "npx" && originalArgs.some((arg) => String(arg).includes("protect-mcp"))) {
|
|
1752
|
+
changes.push({ name, before, after: before, skipped: "already wrapped" });
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
const wrappedArgs = wrapperArgsFor([originalCommand, ...originalArgs], { configPath, cedarDir, enforce });
|
|
1756
|
+
const after = { ...before, command: "npx", args: wrappedArgs };
|
|
1757
|
+
next.mcpServers[name] = after;
|
|
1758
|
+
changes.push({ name, before, after });
|
|
1759
|
+
}
|
|
1760
|
+
process.stdout.write(`
|
|
1761
|
+
${bold("protect-mcp wrap: Claude Desktop")}
|
|
1762
|
+
|
|
1763
|
+
`);
|
|
1764
|
+
process.stdout.write(`Config: ${claudePath}
|
|
1765
|
+
`);
|
|
1766
|
+
process.stdout.write(`Mode: ${enforce ? "enforce" : "shadow"}
|
|
1767
|
+
|
|
1768
|
+
`);
|
|
1769
|
+
for (const change of changes) {
|
|
1770
|
+
if (change.skipped) {
|
|
1771
|
+
process.stdout.write(` - ${change.name}: ${yellow(change.skipped)}
|
|
1772
|
+
`);
|
|
1773
|
+
} else {
|
|
1774
|
+
process.stdout.write(` - ${change.name}: ${green("will wrap")}
|
|
1775
|
+
`);
|
|
1776
|
+
process.stdout.write(` ${dim(`${change.before.command || ""} ${(change.before.args || []).join(" ")}`)}
|
|
1777
|
+
`);
|
|
1778
|
+
process.stdout.write(` ${dim(shellCommand("npx", change.after.args || []))}
|
|
1779
|
+
`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (!write) {
|
|
1783
|
+
process.stdout.write(`
|
|
1784
|
+
Dry run only. Apply with:
|
|
1785
|
+
`);
|
|
1786
|
+
process.stdout.write(` ${dim("npx protect-mcp wrap --claude-desktop --write")}
|
|
1787
|
+
|
|
1788
|
+
`);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
const backupPath = `${claudePath}.bak.${Date.now()}`;
|
|
1792
|
+
writeFileSync(backupPath, readFileSync(claudePath, "utf-8"));
|
|
1793
|
+
writeFileSync(claudePath, JSON.stringify(next, null, 2) + "\n");
|
|
1794
|
+
process.stdout.write(`
|
|
1795
|
+
${green("\u2713 Claude Desktop config updated")}
|
|
1796
|
+
`);
|
|
1797
|
+
process.stdout.write(` Backup: ${backupPath}
|
|
1798
|
+
`);
|
|
1799
|
+
process.stdout.write(` Restart Claude Desktop, then run: ${dim("npx protect-mcp dashboard --open")}
|
|
1800
|
+
|
|
469
1801
|
`);
|
|
470
1802
|
}
|
|
471
1803
|
function bold(s) {
|
|
@@ -612,10 +1944,166 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
|
612
1944
|
process.stdout.write(`
|
|
613
1945
|
`);
|
|
614
1946
|
}
|
|
1947
|
+
var _pkgV = null;
|
|
1948
|
+
async function pkgVersion() {
|
|
1949
|
+
if (_pkgV) return _pkgV;
|
|
1950
|
+
let v = "0.0.0";
|
|
1951
|
+
try {
|
|
1952
|
+
const { readFileSync, existsSync, realpathSync } = await import("fs");
|
|
1953
|
+
const { dirname, join, resolve } = await import("path");
|
|
1954
|
+
let base = "";
|
|
1955
|
+
try {
|
|
1956
|
+
base = dirname(realpathSync(resolve(process.argv[1] || "")));
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
const candidates = [
|
|
1960
|
+
base ? join(base, "..", "package.json") : "",
|
|
1961
|
+
base ? join(base, "package.json") : ""
|
|
1962
|
+
].filter(Boolean);
|
|
1963
|
+
for (const p of candidates) {
|
|
1964
|
+
if (existsSync(p)) {
|
|
1965
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8"));
|
|
1966
|
+
if (parsed && parsed.name === "protect-mcp" && parsed.version) {
|
|
1967
|
+
v = parsed.version;
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
_pkgV = v;
|
|
1975
|
+
return v;
|
|
1976
|
+
}
|
|
1977
|
+
function mapRecordEntry(e) {
|
|
1978
|
+
const p = e && e.payload && typeof e.payload === "object" ? e.payload : e;
|
|
1979
|
+
const dec = String(p.decision || e.decision || "").toLowerCase();
|
|
1980
|
+
const verdict = /den|block|reject|refus/.test(dec) ? "blocked" : /ask|approv|hold|escal|review|pending/.test(dec) ? "held" : "allowed";
|
|
1981
|
+
const tsRaw = e.issued_at || e.timestamp || p.timestamp || p.issued_at;
|
|
1982
|
+
const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
1983
|
+
const ts = isFinite(ms) ? new Date(ms).toISOString() : "";
|
|
1984
|
+
const tool = String(p.tool || e.tool || "action");
|
|
1985
|
+
const reason = String(p.reason_code || e.reason_code || p.policy_engine || "signed");
|
|
1986
|
+
const hook = String(p.hook_event || e.hook_event || "");
|
|
1987
|
+
const signed = !!(e.signature || e.sig || e.receipt_hash || typeof e.type === "string" && e.type.indexOf("receipt") >= 0);
|
|
1988
|
+
let digest = "";
|
|
1989
|
+
if (e.receipt_hash) digest = String(e.receipt_hash);
|
|
1990
|
+
else if (e.digest) digest = String(e.digest);
|
|
1991
|
+
else if (p.payload_digest && p.payload_digest.output_hash) digest = String(p.payload_digest.output_hash);
|
|
1992
|
+
return { ts, tool, verdict, reason, hook, signed, id: String(e.request_id || p.request_id || ""), digest };
|
|
1993
|
+
}
|
|
1994
|
+
async function handleRecord(argv) {
|
|
1995
|
+
const { readFileSync, existsSync, writeFileSync } = await import("fs");
|
|
1996
|
+
const { join } = await import("path");
|
|
1997
|
+
const osMod = await import("os");
|
|
1998
|
+
const cp = await import("child_process");
|
|
1999
|
+
let dir = process.cwd();
|
|
2000
|
+
const di = argv.indexOf("--dir");
|
|
2001
|
+
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
2002
|
+
const recPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
2003
|
+
const logPath = join(dir, ".protect-mcp-log.jsonl");
|
|
2004
|
+
const chosen = existsSync(recPath) ? recPath : existsSync(logPath) ? logPath : null;
|
|
2005
|
+
if (!chosen) {
|
|
2006
|
+
process.stderr.write(`
|
|
2007
|
+
${bold("protect-mcp record")}
|
|
2008
|
+
|
|
2009
|
+
No record found in ${dir}.
|
|
2010
|
+
Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
|
|
2011
|
+
|
|
2012
|
+
`);
|
|
2013
|
+
process.exit(0);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
const raw = readFileSync(chosen, "utf-8");
|
|
2017
|
+
const recs = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
2018
|
+
try {
|
|
2019
|
+
return JSON.parse(l);
|
|
2020
|
+
} catch {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
}).filter((x) => x !== null).map(mapRecordEntry);
|
|
2024
|
+
const meta = { file: chosen, signed: chosen === recPath, count: recs.length };
|
|
2025
|
+
const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
|
|
2026
|
+
const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
|
|
2027
|
+
writeFileSync(out, html);
|
|
2028
|
+
if (!argv.includes("--no-open")) {
|
|
2029
|
+
const platform = process.platform;
|
|
2030
|
+
const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
2031
|
+
const openArgs = platform === "win32" ? ["/c", "start", "", out] : [out];
|
|
2032
|
+
try {
|
|
2033
|
+
const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
|
|
2034
|
+
child.unref();
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
process.stdout.write(`
|
|
2039
|
+
${bold("\u{1F6E1}\uFE0F Your record")} ${recs.length} decision${recs.length === 1 ? "" : "s"}
|
|
2040
|
+
`);
|
|
2041
|
+
process.stdout.write(` Opened a searchable view in your browser. Everything stayed on this machine.
|
|
2042
|
+
`);
|
|
2043
|
+
if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
|
|
2044
|
+
`);
|
|
2045
|
+
process.stdout.write(` ${dim("If it did not open, open this file: " + out)}
|
|
2046
|
+
|
|
2047
|
+
`);
|
|
2048
|
+
process.exit(0);
|
|
2049
|
+
}
|
|
2050
|
+
var RECORD_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>protect-mcp record</title>
|
|
2051
|
+
<style>
|
|
2052
|
+
:root{--paper:#f6f4ef;--ink:#1b1815;--soft:#524d46;--faint:#8a837a;--line:#ddd7c9;--g:#3f6146;--gb:#e7eee3;--a:#8f6216;--ab:#f2e8d3;--r:#7d3535;--rb:#f2e0dc}
|
|
2053
|
+
*{box-sizing:border-box}
|
|
2054
|
+
body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
2055
|
+
.wrap{max-width:1000px;margin:0 auto;padding:26px 22px 60px}
|
|
2056
|
+
h1{font-size:24px;margin:0 0 4px;letter-spacing:-.012em}
|
|
2057
|
+
.meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
2058
|
+
.bar{margin:18px 0 12px}
|
|
2059
|
+
input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit}
|
|
2060
|
+
.chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
|
2061
|
+
.chip{cursor:pointer;font-size:12px;padding:3px 10px;border-radius:100px;border:1px solid var(--line);background:#fff;color:var(--soft)}
|
|
2062
|
+
.chip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
|
|
2063
|
+
.count{color:var(--faint);font-size:12px;font-family:ui-monospace,Menlo,monospace;margin-bottom:8px}
|
|
2064
|
+
.row{border:1px solid var(--line);border-radius:9px;background:#fcfbf7;padding:11px 13px;margin-bottom:8px;cursor:pointer}
|
|
2065
|
+
.top{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
|
|
2066
|
+
.pill{font-size:11px;font-weight:600;padding:2px 9px;border-radius:100px}
|
|
2067
|
+
.pill.allowed{background:var(--gb);color:var(--g)}.pill.held{background:var(--ab);color:var(--a)}.pill.blocked{background:var(--rb);color:var(--r)}
|
|
2068
|
+
.tag{font-size:11px;padding:1px 7px;border-radius:100px;background:var(--paper);border:1px solid var(--line);color:var(--faint)}
|
|
2069
|
+
.when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
2070
|
+
.det{margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:12px;color:var(--soft);font-family:ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all;display:none}
|
|
2071
|
+
.row.open .det{display:block}
|
|
2072
|
+
.foot{margin-top:22px;color:var(--faint);font-size:12px;line-height:1.6;border-top:1px solid var(--line);padding-top:14px}
|
|
2073
|
+
.foot b{color:var(--soft)}
|
|
2074
|
+
</style></head><body><div class="wrap">
|
|
2075
|
+
<h1>Your record</h1>
|
|
2076
|
+
<div class="meta" id="meta"></div>
|
|
2077
|
+
<div class="bar"><input id="q" placeholder="Search your record: tool, reason, verdict"></div>
|
|
2078
|
+
<div class="chips" id="chips"></div>
|
|
2079
|
+
<div class="count" id="count"></div>
|
|
2080
|
+
<div id="list"></div>
|
|
2081
|
+
<div class="foot">Signed decisions from your own gate. This page is local and nothing was uploaded. Verify authoritatively with <b>npx protect-mcp receipts</b> or the open verifier. protect-mcp governs proposed actions before they run.</div>
|
|
2082
|
+
</div>
|
|
2083
|
+
<script>
|
|
2084
|
+
var RECORDS=__DATA__;var META=__META__;var Q="",ACT={};
|
|
2085
|
+
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
2086
|
+
function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
|
|
2087
|
+
function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+d.toLocaleTimeString(undefined,{hour:"2-digit",minute:"2-digit"})}
|
|
2088
|
+
function fvals(key){var m={};RECORDS.forEach(function(r){var v=key==="Decision"?vlabel(r.verdict):r[key.toLowerCase()];if(v){m[v]=(m[v]||0)+1}});return Object.keys(m).sort(function(a,b){return m[b]-m[a]}).slice(0,8).map(function(v){return[v,m[v]]})}
|
|
2089
|
+
function match(r){if(ACT.Decision&&vlabel(r.verdict)!==ACT.Decision)return false;if(ACT.Tool&&r.tool!==ACT.Tool)return false;if(ACT.Reason&&r.reason!==ACT.Reason)return false;if(Q){var h=(r.tool+" "+r.reason+" "+vlabel(r.verdict)+" "+r.hook).toLowerCase();if(h.indexOf(Q)<0)return false}return true}
|
|
2090
|
+
function render(){
|
|
2091
|
+
document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local";
|
|
2092
|
+
var chips="";["Decision","Tool","Reason"].forEach(function(key){fvals(key).forEach(function(p){var on=ACT[key]===p[0];chips+='<span class="chip'+(on?" on":"")+'" data-k="'+key+'" data-v="'+esc(p[0])+'">'+esc(p[0])+" "+p[1]+"</span>"})});
|
|
2093
|
+
document.getElementById("chips").innerHTML=chips;
|
|
2094
|
+
var rows=RECORDS.filter(match);
|
|
2095
|
+
document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records";
|
|
2096
|
+
var html="";rows.slice(0,800).forEach(function(r){html+='<div class="row"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+'<span class="tag">'+(r.signed?"signed":"log")+'</span><span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r,null,2))+"</div></div>"});
|
|
2097
|
+
document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>'}
|
|
2098
|
+
document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
|
|
2099
|
+
document.getElementById("chips").addEventListener("click",function(e){var c=e.target.closest(".chip");if(!c)return;var k=c.getAttribute("data-k"),v=c.getAttribute("data-v");ACT[k]=ACT[k]===v?undefined:v;render()});
|
|
2100
|
+
document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
|
|
2101
|
+
render();
|
|
2102
|
+
</script></body></html>`;
|
|
615
2103
|
async function handleBundle(argv) {
|
|
616
2104
|
const { readFileSync, writeFileSync, existsSync } = await import("fs");
|
|
617
2105
|
const { join } = await import("path");
|
|
618
|
-
const { createAuditBundle } = await import("./bundle-
|
|
2106
|
+
const { createAuditBundle } = await import("./bundle-YUUHAG7O.mjs");
|
|
619
2107
|
let dir = process.cwd();
|
|
620
2108
|
const dirIdx = argv.indexOf("--dir");
|
|
621
2109
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
@@ -642,6 +2130,7 @@ No key file found at ${keyPath}
|
|
|
642
2130
|
const bundle = createAuditBundle({
|
|
643
2131
|
tenant: keyData.issuer || "protect-mcp",
|
|
644
2132
|
receipts,
|
|
2133
|
+
selectiveDisclosures: collectSelectiveDisclosurePackages(dir),
|
|
645
2134
|
signingKeys: [{
|
|
646
2135
|
kty: "OKP",
|
|
647
2136
|
crv: "Ed25519",
|
|
@@ -656,6 +2145,8 @@ ${bold("protect-mcp bundle")}
|
|
|
656
2145
|
|
|
657
2146
|
`);
|
|
658
2147
|
process.stdout.write(` Receipts: ${receipts.length}
|
|
2148
|
+
`);
|
|
2149
|
+
process.stdout.write(` Disclosures: ${collectSelectiveDisclosurePackages(dir).length}
|
|
659
2150
|
`);
|
|
660
2151
|
process.stdout.write(` Output: ${outputPath}
|
|
661
2152
|
`);
|
|
@@ -730,7 +2221,7 @@ ${bold("protect-mcp connect")}
|
|
|
730
2221
|
`));
|
|
731
2222
|
process.stderr.write(` Receipts will be uploaded automatically.
|
|
732
2223
|
`);
|
|
733
|
-
process.stderr.write(dim(` Free tier: 20,000 receipts/month
|
|
2224
|
+
process.stderr.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
|
|
734
2225
|
`));
|
|
735
2226
|
process.stderr.write(`
|
|
736
2227
|
${"\u2500".repeat(50)}
|
|
@@ -773,7 +2264,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
773
2264
|
const { randomBytes } = await import("crypto");
|
|
774
2265
|
let keypair;
|
|
775
2266
|
try {
|
|
776
|
-
const { ed25519 } = await import("./ed25519-
|
|
2267
|
+
const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
|
|
777
2268
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
778
2269
|
const privateKey = randomBytes(32);
|
|
779
2270
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -828,7 +2319,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
828
2319
|
`));
|
|
829
2320
|
process.stdout.write(` Receipts will be uploaded automatically.
|
|
830
2321
|
`);
|
|
831
|
-
process.stdout.write(dim(` Free tier: 20,000 receipts/month
|
|
2322
|
+
process.stdout.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
|
|
832
2323
|
`));
|
|
833
2324
|
process.stdout.write(`
|
|
834
2325
|
`);
|
|
@@ -857,6 +2348,648 @@ ${bold("protect-mcp quickstart")}
|
|
|
857
2348
|
process.env.PROTECT_MCP_CONFIG = configPath;
|
|
858
2349
|
await handleDemo();
|
|
859
2350
|
}
|
|
2351
|
+
async function handleRegistry(argv) {
|
|
2352
|
+
const subcommand = argv[0] || "status";
|
|
2353
|
+
const dir = resolveCli(flagValue(argv, "--dir") || process.cwd());
|
|
2354
|
+
const orgName = flagValue(argv, "--org") || process.env.SCOPEBLIND_ORG;
|
|
2355
|
+
const orgId = flagValue(argv, "--org-id") || process.env.SCOPEBLIND_ORG_ID;
|
|
2356
|
+
const billingAccountId = flagValue(argv, "--billing-account") || process.env.SCOPEBLIND_BILLING_ACCOUNT;
|
|
2357
|
+
const endpoint = flagValue(argv, "--endpoint") || process.env.SCOPEBLIND_REGISTRY_ENDPOINT || (argv.includes("--hosted") ? "https://api.scopeblind.com" : void 0);
|
|
2358
|
+
const token = flagValue(argv, "--token") || process.env.SCOPEBLIND_TOKEN;
|
|
2359
|
+
const verifierBaseUrl = flagValue(argv, "--verifier-base") || process.env.SCOPEBLIND_VERIFIER_BASE || "https://legate.scopeblind.com";
|
|
2360
|
+
const registryMod = await import("./receipt-registry-6CAOY6RP.mjs");
|
|
2361
|
+
if (subcommand === "init") {
|
|
2362
|
+
const identity = registryMod.createOrgIdentity({
|
|
2363
|
+
dir,
|
|
2364
|
+
orgName,
|
|
2365
|
+
orgId,
|
|
2366
|
+
billingAccountId
|
|
2367
|
+
});
|
|
2368
|
+
const path = registryMod.writeOrgIdentity(dir, identity);
|
|
2369
|
+
process.stdout.write(`
|
|
2370
|
+
${bold("protect-mcp registry init")}
|
|
2371
|
+
|
|
2372
|
+
`);
|
|
2373
|
+
process.stdout.write(` Org: ${identity.org_name}
|
|
2374
|
+
`);
|
|
2375
|
+
process.stdout.write(` Org ID: ${identity.org_id}
|
|
2376
|
+
`);
|
|
2377
|
+
process.stdout.write(` Billing account: ${identity.billing_account_id}
|
|
2378
|
+
`);
|
|
2379
|
+
process.stdout.write(` Public keys: ${identity.public_key_directory.length}
|
|
2380
|
+
`);
|
|
2381
|
+
process.stdout.write(` Wrote: ${path}
|
|
2382
|
+
|
|
2383
|
+
`);
|
|
2384
|
+
process.stdout.write(`${dim("No prompts, tool payloads, raw receipts, or private keys are included.")}
|
|
2385
|
+
|
|
2386
|
+
`);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (subcommand === "anchor") {
|
|
2390
|
+
process.stdout.write(`
|
|
2391
|
+
${bold("protect-mcp registry anchor")}
|
|
2392
|
+
|
|
2393
|
+
`);
|
|
2394
|
+
const result = await registryMod.createReceiptRegistry({
|
|
2395
|
+
dir,
|
|
2396
|
+
orgName,
|
|
2397
|
+
orgId,
|
|
2398
|
+
billingAccountId,
|
|
2399
|
+
endpoint,
|
|
2400
|
+
token,
|
|
2401
|
+
hosted: argv.includes("--hosted") || Boolean(endpoint || token),
|
|
2402
|
+
verifierBaseUrl,
|
|
2403
|
+
outPath: flagValue(argv, "--output")
|
|
2404
|
+
});
|
|
2405
|
+
process.stdout.write(` Org: ${result.registry.org.org_name}
|
|
2406
|
+
`);
|
|
2407
|
+
process.stdout.write(` Billing account: ${result.registry.billing.billing_account_id}
|
|
2408
|
+
`);
|
|
2409
|
+
process.stdout.write(` Digests: ${result.registry.records.length}
|
|
2410
|
+
`);
|
|
2411
|
+
process.stdout.write(` Anchors: ${result.registry.anchors.length}
|
|
2412
|
+
`);
|
|
2413
|
+
process.stdout.write(` Boundary: ${result.uploaded ? green("hosted digest anchor") : yellow("local preview only")}
|
|
2414
|
+
`);
|
|
2415
|
+
process.stdout.write(` Registry: ${result.registryPath}
|
|
2416
|
+
`);
|
|
2417
|
+
process.stdout.write(` Verifier page: ${result.verifierPath}
|
|
2418
|
+
|
|
2419
|
+
`);
|
|
2420
|
+
process.stdout.write(` Uploaded fields: ${result.registry.privacy.uploaded_fields.join(", ")}
|
|
2421
|
+
`);
|
|
2422
|
+
process.stdout.write(` Excluded fields: ${result.registry.privacy.excluded_fields.join(", ")}
|
|
2423
|
+
|
|
2424
|
+
`);
|
|
2425
|
+
if (!result.uploaded) {
|
|
2426
|
+
process.stdout.write(`${yellow(" This is not an independent timestamp yet.")}
|
|
2427
|
+
`);
|
|
2428
|
+
process.stdout.write(` Run with ${dim("--hosted --token $SCOPEBLIND_TOKEN")} to make the paid boundary real.
|
|
2429
|
+
|
|
2430
|
+
`);
|
|
2431
|
+
}
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (subcommand === "status") {
|
|
2435
|
+
const registryPath = joinCli(dir, registryMod.REGISTRY_FILE);
|
|
2436
|
+
const identityPath = joinCli(dir, registryMod.ORG_IDENTITY_FILE);
|
|
2437
|
+
process.stdout.write(`
|
|
2438
|
+
${bold("protect-mcp registry status")}
|
|
2439
|
+
|
|
2440
|
+
`);
|
|
2441
|
+
if (existsSyncCli(identityPath)) {
|
|
2442
|
+
const identity = JSON.parse(readFileSyncCli(identityPath, "utf-8"));
|
|
2443
|
+
process.stdout.write(` Org: ${identity.org_name || "unknown"}
|
|
2444
|
+
`);
|
|
2445
|
+
process.stdout.write(` Org ID: ${identity.org_id || "unknown"}
|
|
2446
|
+
`);
|
|
2447
|
+
process.stdout.write(` Billing account: ${identity.billing_account_id || "unknown"}
|
|
2448
|
+
`);
|
|
2449
|
+
} else {
|
|
2450
|
+
process.stdout.write(` Org identity: ${yellow("missing")} (${identityPath})
|
|
2451
|
+
`);
|
|
2452
|
+
}
|
|
2453
|
+
if (existsSyncCli(registryPath)) {
|
|
2454
|
+
const registry = JSON.parse(readFileSyncCli(registryPath, "utf-8"));
|
|
2455
|
+
const hosted = Array.isArray(registry.anchors) && registry.anchors.some((a) => a.timestamp_source === "scopeblind-hosted");
|
|
2456
|
+
process.stdout.write(` Registry: ${registryPath}
|
|
2457
|
+
`);
|
|
2458
|
+
process.stdout.write(` Digests: ${registry.records?.length || 0}
|
|
2459
|
+
`);
|
|
2460
|
+
process.stdout.write(` Anchors: ${registry.anchors?.length || 0}
|
|
2461
|
+
`);
|
|
2462
|
+
process.stdout.write(` Boundary: ${hosted ? green("hosted digest anchor") : yellow("local preview only")}
|
|
2463
|
+
`);
|
|
2464
|
+
process.stdout.write(` Verifier page: ${joinCli(dir, registryMod.VERIFIER_PAGE_FILE)}
|
|
2465
|
+
`);
|
|
2466
|
+
} else {
|
|
2467
|
+
process.stdout.write(` Registry: ${yellow("missing")} (${registryPath})
|
|
2468
|
+
`);
|
|
2469
|
+
process.stdout.write(` Next: ${dim("npx protect-mcp registry anchor --hosted")}
|
|
2470
|
+
`);
|
|
2471
|
+
}
|
|
2472
|
+
process.stdout.write(`
|
|
2473
|
+
`);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
process.stderr.write("Usage: protect-mcp registry init|anchor|status [--dir <path>] [--org <name>] [--hosted]\\n");
|
|
2477
|
+
process.exit(1);
|
|
2478
|
+
}
|
|
2479
|
+
async function handleKillerDemo(argv) {
|
|
2480
|
+
const { mkdtempSync } = await import("fs");
|
|
2481
|
+
const { tmpdir } = await import("os");
|
|
2482
|
+
const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
|
|
2483
|
+
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
2484
|
+
const { randomBytes } = await import("crypto");
|
|
2485
|
+
const artifacts = await import("@veritasacta/artifacts");
|
|
2486
|
+
const {
|
|
2487
|
+
createSelectiveDisclosurePackage,
|
|
2488
|
+
signCommittedDecision,
|
|
2489
|
+
verifySelectiveDisclosurePackage
|
|
2490
|
+
} = await import("./signing-committed-QXCW24RF.mjs");
|
|
2491
|
+
const registryMod = await import("./receipt-registry-6CAOY6RP.mjs");
|
|
2492
|
+
const dir = resolveCli(flagValue(argv, "--dir") || mkdtempSync(joinCli(tmpdir(), "scopeblind-killer-demo-")));
|
|
2493
|
+
mkdirSyncCli(dir, { recursive: true });
|
|
2494
|
+
mkdirSyncCli(joinCli(dir, "keys"), { recursive: true });
|
|
2495
|
+
mkdirSyncCli(joinCli(dir, "receipts"), { recursive: true });
|
|
2496
|
+
const privateKeyBytes = randomBytes(32);
|
|
2497
|
+
const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes);
|
|
2498
|
+
const keypair = {
|
|
2499
|
+
privateKey: bytesToHex(privateKeyBytes),
|
|
2500
|
+
publicKey: bytesToHex(publicKeyBytes),
|
|
2501
|
+
kid: `killer-demo-${Date.now()}`,
|
|
2502
|
+
issuer: "scopeblind-killer-demo"
|
|
2503
|
+
};
|
|
2504
|
+
const keyPath = joinCli(dir, "keys", "gateway.json");
|
|
2505
|
+
writeFileSyncCli(keyPath, JSON.stringify({
|
|
2506
|
+
...keypair,
|
|
2507
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2508
|
+
warning: "Demo key only. Do not use for production."
|
|
2509
|
+
}, null, 2) + "\n");
|
|
2510
|
+
const shadowConfigPath = joinCli(dir, "protect-mcp.shadow.json");
|
|
2511
|
+
const policyPackPath = joinCli(dir, "protect-mcp.policy-pack.json");
|
|
2512
|
+
const config = {
|
|
2513
|
+
tools: { "*": { rate_limit: "100/hour" } },
|
|
2514
|
+
default_tier: "signed-known",
|
|
2515
|
+
signing: { key_path: keyPath, issuer: keypair.issuer, enabled: true }
|
|
2516
|
+
};
|
|
2517
|
+
const policyPack = {
|
|
2518
|
+
tools: {
|
|
2519
|
+
"*": { rate_limit: "100/hour" },
|
|
2520
|
+
read_file: { rate_limit: "60/hour" },
|
|
2521
|
+
github_create_pr: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
|
|
2522
|
+
send_email: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
|
|
2523
|
+
pms_book_fill: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
|
|
2524
|
+
delete_file: { block: true, min_tier: "privileged" }
|
|
2525
|
+
},
|
|
2526
|
+
default_tier: "signed-known",
|
|
2527
|
+
signing: { key_path: keyPath, issuer: keypair.issuer, enabled: true },
|
|
2528
|
+
notes: ["Demo policy pack: approvals for GitHub, email, and PMS booking; destructive tools blocked."]
|
|
2529
|
+
};
|
|
2530
|
+
writeFileSyncCli(shadowConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
2531
|
+
writeFileSyncCli(policyPackPath, JSON.stringify(policyPack, null, 2) + "\n");
|
|
2532
|
+
await initSigning({ enabled: true, key_path: keyPath, issuer: keypair.issuer });
|
|
2533
|
+
const logPath = joinCli(dir, ".protect-mcp-log.jsonl");
|
|
2534
|
+
const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
|
|
2535
|
+
const shadowCalls = [
|
|
2536
|
+
{ tool: "read_file", input: { path: "/research/macro-notes.md" }, reason: "observe_mode" },
|
|
2537
|
+
{ tool: "github_create_pr", input: { repo: "scopeblind/legate", branch: "agent/pms-adapter", title: "Wire mock PMS adapter" }, reason: "observe_mode" },
|
|
2538
|
+
{ tool: "send_email", input: { to: "ops@examplefund.com", subject: "Booking update", body: "Draft only", api_key: "demo-secret" }, reason: "observe_mode" },
|
|
2539
|
+
{ tool: "pms_book_fill", input: { account: "Meridian Global Macro", symbol: "AAPL", side: "BUY", quantity: 50, price: 182.4, strategy: "US Large Cap Tactical", bearerToken: "demo-secret" }, reason: "observe_mode" }
|
|
2540
|
+
];
|
|
2541
|
+
for (const [idx, call] of shadowCalls.entries()) {
|
|
2542
|
+
const requestId2 = `demo-shadow-${idx + 1}`;
|
|
2543
|
+
appendFileSyncCli(logPath, JSON.stringify({
|
|
2544
|
+
v: 2,
|
|
2545
|
+
tool: call.tool,
|
|
2546
|
+
decision: "allow",
|
|
2547
|
+
reason_code: call.reason,
|
|
2548
|
+
request_id: requestId2,
|
|
2549
|
+
timestamp: Date.now() + idx,
|
|
2550
|
+
mode: "shadow",
|
|
2551
|
+
policy_digest: "shadow-policy",
|
|
2552
|
+
action_readback: buildActionReadback(call.tool, call.input)
|
|
2553
|
+
}) + "\n");
|
|
2554
|
+
}
|
|
2555
|
+
const sensitiveInput = {
|
|
2556
|
+
account: "Meridian Global Macro",
|
|
2557
|
+
symbol: "AAPL",
|
|
2558
|
+
side: "BUY",
|
|
2559
|
+
quantity: 50,
|
|
2560
|
+
price: 182.4,
|
|
2561
|
+
strategy: "US Large Cap Tactical",
|
|
2562
|
+
trader_note: "Do not reveal portfolio context outside the desk.",
|
|
2563
|
+
api_key: "demo-pms-secret"
|
|
2564
|
+
};
|
|
2565
|
+
const readback = buildActionReadback("pms_book_fill", sensitiveInput);
|
|
2566
|
+
const requestId = "demo-sensitive-pms-booking";
|
|
2567
|
+
const requireApprovalEntry = {
|
|
2568
|
+
v: 2,
|
|
2569
|
+
tool: "pms_book_fill",
|
|
2570
|
+
decision: "require_approval",
|
|
2571
|
+
reason_code: "requires_human_approval",
|
|
2572
|
+
request_id: requestId,
|
|
2573
|
+
timestamp: Date.now() + 10,
|
|
2574
|
+
mode: "enforce",
|
|
2575
|
+
policy_digest: createHashCli("sha256").update(JSON.stringify(policyPack)).digest("hex").slice(0, 16),
|
|
2576
|
+
action_readback: readback
|
|
2577
|
+
};
|
|
2578
|
+
appendFileSyncCli(logPath, JSON.stringify(requireApprovalEntry) + "\n");
|
|
2579
|
+
appendFileSyncCli(joinCli(dir, ".protect-mcp-approval-resolutions.jsonl"), JSON.stringify({
|
|
2580
|
+
type: "scopeblind.approval_resolution.v1",
|
|
2581
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2582
|
+
request_id: requestId,
|
|
2583
|
+
tool: "pms_book_fill",
|
|
2584
|
+
resolution: "approve",
|
|
2585
|
+
reason: "Matches the ticket and stays inside mandate.",
|
|
2586
|
+
payload_hash: readback.payload_hash
|
|
2587
|
+
}) + "\n");
|
|
2588
|
+
const executedEntry = {
|
|
2589
|
+
...requireApprovalEntry,
|
|
2590
|
+
decision: "allow",
|
|
2591
|
+
reason_code: "approval_granted",
|
|
2592
|
+
timestamp: Date.now() + 20,
|
|
2593
|
+
payload_digest: {
|
|
2594
|
+
output_hash: createHashCli("sha256").update("mock-pms-booking-confirmed").digest("hex"),
|
|
2595
|
+
output_size: 26,
|
|
2596
|
+
truncated: false
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
appendFileSyncCli(logPath, JSON.stringify(executedEntry) + "\n");
|
|
2600
|
+
const signed = signDecision(executedEntry);
|
|
2601
|
+
if (!signed.signed) throw new Error(`demo signing failed: ${signed.warning || signed.error || "unknown"}`);
|
|
2602
|
+
appendFileSyncCli(receiptPath, signed.signed + "\n");
|
|
2603
|
+
writeFileSyncCli(joinCli(dir, "receipts", "approved-pms-booking.receipt.json"), JSON.stringify(JSON.parse(signed.signed), null, 2) + "\n");
|
|
2604
|
+
const receiptArtifact = JSON.parse(signed.signed);
|
|
2605
|
+
const tamperedArtifact = JSON.parse(signed.signed);
|
|
2606
|
+
if (tamperedArtifact.payload && typeof tamperedArtifact.payload === "object") {
|
|
2607
|
+
tamperedArtifact.payload.decision = "deny";
|
|
2608
|
+
tamperedArtifact.payload.tool = "send_email";
|
|
2609
|
+
} else {
|
|
2610
|
+
tamperedArtifact.tool = "send_email";
|
|
2611
|
+
}
|
|
2612
|
+
const validOriginal = artifacts.verifyArtifact(receiptArtifact, keypair.publicKey);
|
|
2613
|
+
const validTampered = artifacts.verifyArtifact(tamperedArtifact, keypair.publicKey);
|
|
2614
|
+
writeFileSyncCli(joinCli(dir, "receipts", "tampered.receipt.json"), JSON.stringify(tamperedArtifact, null, 2) + "\n");
|
|
2615
|
+
const committed = signCommittedDecision(
|
|
2616
|
+
executedEntry,
|
|
2617
|
+
["tool", "payload_digest", "swarm"],
|
|
2618
|
+
keypair.privateKey,
|
|
2619
|
+
keypair.publicKey,
|
|
2620
|
+
keypair.kid,
|
|
2621
|
+
keypair.issuer
|
|
2622
|
+
);
|
|
2623
|
+
const committedReceipt = JSON.parse(committed.signed);
|
|
2624
|
+
const disclosurePackage = createSelectiveDisclosurePackage(committedReceipt, ["tool"], committed.openings);
|
|
2625
|
+
const disclosureVerification = verifySelectiveDisclosurePackage(committedReceipt, disclosurePackage);
|
|
2626
|
+
appendFileSyncCli(receiptPath, committed.signed + "\n");
|
|
2627
|
+
writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.receipt.json"), JSON.stringify(committedReceipt, null, 2) + "\n");
|
|
2628
|
+
writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.package.json"), JSON.stringify(disclosurePackage, null, 2) + "\n");
|
|
2629
|
+
writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.tool-only.json"), JSON.stringify(disclosurePackage, null, 2) + "\n");
|
|
2630
|
+
writeFileSyncCli(joinCli(dir, "verification-results.json"), JSON.stringify({
|
|
2631
|
+
original_receipt_valid: validOriginal,
|
|
2632
|
+
tampered_receipt_valid: validTampered,
|
|
2633
|
+
selective_disclosure_valid: disclosureVerification.valid,
|
|
2634
|
+
selective_disclosure_explanation: disclosureVerification.explanation
|
|
2635
|
+
}, null, 2) + "\n");
|
|
2636
|
+
const registry = await registryMod.createReceiptRegistry({
|
|
2637
|
+
dir,
|
|
2638
|
+
orgName: flagValue(argv, "--org") || "Meridian Global Macro Demo",
|
|
2639
|
+
billingAccountId: flagValue(argv, "--billing-account") || "demo_billing_digest_only",
|
|
2640
|
+
hosted: argv.includes("--hosted"),
|
|
2641
|
+
endpoint: flagValue(argv, "--endpoint") || process.env.SCOPEBLIND_REGISTRY_ENDPOINT || (argv.includes("--hosted") ? "https://api.scopeblind.com" : void 0),
|
|
2642
|
+
token: flagValue(argv, "--token") || process.env.SCOPEBLIND_TOKEN,
|
|
2643
|
+
verifierBaseUrl: flagValue(argv, "--verifier-base") || "https://legate.scopeblind.com"
|
|
2644
|
+
});
|
|
2645
|
+
const runbook = [
|
|
2646
|
+
"# ScopeBlind Killer Demo",
|
|
2647
|
+
"",
|
|
2648
|
+
"Three-minute flow, generated locally.",
|
|
2649
|
+
"",
|
|
2650
|
+
"## 1. Agent has tools",
|
|
2651
|
+
"",
|
|
2652
|
+
"Mock tools represented: filesystem `read_file`, GitHub `github_create_pr`, email `send_email`, and PMS `pms_book_fill`.",
|
|
2653
|
+
"",
|
|
2654
|
+
"## 2. Shadow mode shows risk",
|
|
2655
|
+
"",
|
|
2656
|
+
"Open the dashboard against this directory:",
|
|
2657
|
+
"",
|
|
2658
|
+
"```bash",
|
|
2659
|
+
`npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`,
|
|
2660
|
+
"```",
|
|
2661
|
+
"",
|
|
2662
|
+
"You will see GitHub, email, and PMS calls ranked as high risk.",
|
|
2663
|
+
"",
|
|
2664
|
+
"## 3. Apply policy pack",
|
|
2665
|
+
"",
|
|
2666
|
+
`Policy pack: \`${policyPackPath}\`.`,
|
|
2667
|
+
"",
|
|
2668
|
+
"It requires approval for GitHub PRs, outbound email, and PMS booking; destructive file deletion is blocked.",
|
|
2669
|
+
"",
|
|
2670
|
+
"## 4. Sensitive action requires exact approval",
|
|
2671
|
+
"",
|
|
2672
|
+
`Request id: \`${requestId}\``,
|
|
2673
|
+
"",
|
|
2674
|
+
`Exact readback summary: \`${readback.summary}\``,
|
|
2675
|
+
"",
|
|
2676
|
+
`Payload hash: \`${readback.payload_hash}\``,
|
|
2677
|
+
"",
|
|
2678
|
+
"Secret-like fields are redacted from the approval preview but still affect the hash.",
|
|
2679
|
+
"",
|
|
2680
|
+
"## 5. User approves; tool executes through gateway",
|
|
2681
|
+
"",
|
|
2682
|
+
"Approval resolution: `.protect-mcp-approval-resolutions.jsonl`",
|
|
2683
|
+
"",
|
|
2684
|
+
"Signed receipt: `receipts/approved-pms-booking.receipt.json`",
|
|
2685
|
+
"",
|
|
2686
|
+
"## 6. Offline verification and tamper failure",
|
|
2687
|
+
"",
|
|
2688
|
+
"Verification result: `verification-results.json`.",
|
|
2689
|
+
"",
|
|
2690
|
+
"Expected: original valid, tampered invalid.",
|
|
2691
|
+
"",
|
|
2692
|
+
"## 7. Selective disclosure",
|
|
2693
|
+
"",
|
|
2694
|
+
"Committed receipt: `receipts/selective-disclosure.receipt.json`",
|
|
2695
|
+
"",
|
|
2696
|
+
"Tool-only v0 disclosure package: `receipts/selective-disclosure.tool-only.json`",
|
|
2697
|
+
"",
|
|
2698
|
+
"The disclosure opens only the committed `tool` field. Other committed fields, such as `payload_digest`, remain hidden but bound to the signed `committed_fields_root`.",
|
|
2699
|
+
"",
|
|
2700
|
+
"This demonstrates hiding sensitive context while revealing the minimum needed proof. It is salted commitment disclosure, not full zero-knowledge.",
|
|
2701
|
+
"",
|
|
2702
|
+
"## 8. Paid boundary MVP",
|
|
2703
|
+
"",
|
|
2704
|
+
`Registry: \`${registry.registryPath}\``,
|
|
2705
|
+
"",
|
|
2706
|
+
`Verifier page: \`${registry.verifierPath}\``,
|
|
2707
|
+
"",
|
|
2708
|
+
`Boundary: ${registry.uploaded ? "hosted digest anchor with independent timestamp" : "local preview only; hosted anchoring not used"}.`,
|
|
2709
|
+
"",
|
|
2710
|
+
"No raw prompt, payload, output, private key, or raw receipt is uploaded by the registry flow. Hosted mode submits receipt digests, request ids, org public keys, and billing account metadata only.",
|
|
2711
|
+
""
|
|
2712
|
+
].join("\n");
|
|
2713
|
+
writeFileSyncCli(joinCli(dir, "DEMO-RUNBOOK.md"), runbook);
|
|
2714
|
+
writeFileSyncCli(joinCli(dir, "demo-summary.json"), JSON.stringify({
|
|
2715
|
+
dir,
|
|
2716
|
+
dashboard_command: `npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`,
|
|
2717
|
+
policy_pack: policyPackPath,
|
|
2718
|
+
receipt: joinCli(dir, "receipts", "approved-pms-booking.receipt.json"),
|
|
2719
|
+
tampered_receipt: joinCli(dir, "receipts", "tampered.receipt.json"),
|
|
2720
|
+
selective_disclosure_receipt: joinCli(dir, "receipts", "selective-disclosure.receipt.json"),
|
|
2721
|
+
selective_disclosure_package: joinCli(dir, "receipts", "selective-disclosure.tool-only.json"),
|
|
2722
|
+
verification_results: joinCli(dir, "verification-results.json"),
|
|
2723
|
+
registry: registry.registryPath,
|
|
2724
|
+
verifier_page: registry.verifierPath,
|
|
2725
|
+
runbook: joinCli(dir, "DEMO-RUNBOOK.md"),
|
|
2726
|
+
original_valid: validOriginal.valid,
|
|
2727
|
+
tampered_valid: validTampered.valid,
|
|
2728
|
+
selective_disclosure_valid: disclosureVerification.valid
|
|
2729
|
+
}, null, 2) + "\n");
|
|
2730
|
+
process.stdout.write(`
|
|
2731
|
+
${bold("protect-mcp killer-demo")}
|
|
2732
|
+
|
|
2733
|
+
`);
|
|
2734
|
+
process.stdout.write(` Demo dir: ${dir}
|
|
2735
|
+
`);
|
|
2736
|
+
process.stdout.write(` Dashboard: ${dim(`npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`)}
|
|
2737
|
+
`);
|
|
2738
|
+
process.stdout.write(` Runbook: ${joinCli(dir, "DEMO-RUNBOOK.md")}
|
|
2739
|
+
`);
|
|
2740
|
+
process.stdout.write(` Signed receipt: ${joinCli(dir, "receipts", "approved-pms-booking.receipt.json")}
|
|
2741
|
+
`);
|
|
2742
|
+
process.stdout.write(` Tamper check: original=${validOriginal.valid ? green("valid") : red("invalid")} tampered=${validTampered.valid ? red("valid") : green("invalid")}
|
|
2743
|
+
`);
|
|
2744
|
+
process.stdout.write(` Registry: ${registry.registryPath}
|
|
2745
|
+
`);
|
|
2746
|
+
process.stdout.write(` Verifier page: ${registry.verifierPath}
|
|
2747
|
+
`);
|
|
2748
|
+
process.stdout.write(` Boundary: ${registry.uploaded ? green("hosted digest anchor") : yellow("local preview only")}
|
|
2749
|
+
|
|
2750
|
+
`);
|
|
2751
|
+
}
|
|
2752
|
+
async function handleVerifyDisclosure(argv) {
|
|
2753
|
+
const receiptPath = flagValue(argv, "--receipt");
|
|
2754
|
+
const disclosurePath = flagValue(argv, "--disclosure");
|
|
2755
|
+
if (!receiptPath || !disclosurePath) {
|
|
2756
|
+
process.stderr.write("Usage: protect-mcp verify-disclosure --receipt <committed-receipt.json> --disclosure <selective-disclosure.json>\\n");
|
|
2757
|
+
process.exit(1);
|
|
2758
|
+
}
|
|
2759
|
+
const { verifySelectiveDisclosurePackage } = await import("./signing-committed-QXCW24RF.mjs");
|
|
2760
|
+
const receipt = JSON.parse(readFileSyncCli(resolveCli(receiptPath), "utf-8"));
|
|
2761
|
+
const disclosure = JSON.parse(readFileSyncCli(resolveCli(disclosurePath), "utf-8"));
|
|
2762
|
+
const result = verifySelectiveDisclosurePackage(receipt, disclosure);
|
|
2763
|
+
process.stdout.write(`
|
|
2764
|
+
${bold("protect-mcp verify-disclosure")}
|
|
2765
|
+
|
|
2766
|
+
`);
|
|
2767
|
+
process.stdout.write(` Result: ${result.valid ? green("valid") : red("invalid")}
|
|
2768
|
+
`);
|
|
2769
|
+
process.stdout.write(` Receipt hash: ${result.receipt_hash_valid ? green("matches") : red("mismatch")}
|
|
2770
|
+
`);
|
|
2771
|
+
process.stdout.write(` Signature: ${result.signature_valid === true ? green("valid") : result.signature_valid === null ? yellow("not checked") : red("invalid")}
|
|
2772
|
+
`);
|
|
2773
|
+
process.stdout.write(` Commitment root: ${result.commitment_root_valid ? green("matches") : red("mismatch")}
|
|
2774
|
+
`);
|
|
2775
|
+
process.stdout.write(` Disclosed fields: ${result.disclosed_fields.length ? result.disclosed_fields.join(", ") : "none"}
|
|
2776
|
+
`);
|
|
2777
|
+
process.stdout.write(` Hidden fields: ${result.hidden_fields.length ? result.hidden_fields.join(", ") : "none"}
|
|
2778
|
+
|
|
2779
|
+
`);
|
|
2780
|
+
for (const line of result.explanation) {
|
|
2781
|
+
process.stdout.write(` - ${line}
|
|
2782
|
+
`);
|
|
2783
|
+
}
|
|
2784
|
+
if (result.errors.length > 0) {
|
|
2785
|
+
process.stdout.write(`
|
|
2786
|
+
${red("Errors:")}
|
|
2787
|
+
`);
|
|
2788
|
+
for (const err of result.errors) process.stdout.write(` - ${err}
|
|
2789
|
+
`);
|
|
2790
|
+
}
|
|
2791
|
+
process.stdout.write("\n");
|
|
2792
|
+
if (!result.valid) process.exit(2);
|
|
2793
|
+
}
|
|
2794
|
+
async function handlePolicyPacks(argv) {
|
|
2795
|
+
const subcommand = argv[0] || "list";
|
|
2796
|
+
const packArg = argv[1];
|
|
2797
|
+
const dir = resolveCli(flagValue(argv, "--dir") || "./cedar");
|
|
2798
|
+
const force = argv.includes("--force");
|
|
2799
|
+
if (subcommand === "list") {
|
|
2800
|
+
process.stdout.write(`
|
|
2801
|
+
${bold("protect-mcp policy-packs")}
|
|
2802
|
+
|
|
2803
|
+
`);
|
|
2804
|
+
for (const pack of POLICY_PACKS) {
|
|
2805
|
+
process.stdout.write(` ${bold(pack.id.padEnd(22))} ${pack.name}
|
|
2806
|
+
`);
|
|
2807
|
+
process.stdout.write(` ${dim(" ".repeat(24) + pack.description)}
|
|
2808
|
+
`);
|
|
2809
|
+
process.stdout.write(` ${dim(" ".repeat(24) + `recommended: ${pack.recommendedMode}`)}
|
|
2810
|
+
|
|
2811
|
+
`);
|
|
2812
|
+
}
|
|
2813
|
+
process.stdout.write(`Install one: ${dim("protect-mcp policy-packs install filesystem-safe --dir ./cedar")}
|
|
2814
|
+
`);
|
|
2815
|
+
process.stdout.write(`Install all: ${dim("protect-mcp policy-packs install all --dir ./cedar")}
|
|
2816
|
+
|
|
2817
|
+
`);
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
if (subcommand === "show") {
|
|
2821
|
+
const pack = getPolicyPack(packArg || "");
|
|
2822
|
+
if (!pack) {
|
|
2823
|
+
process.stderr.write(`Unknown policy pack "${packArg || ""}". Available: ${policyPackIds().join(", ")}
|
|
2824
|
+
`);
|
|
2825
|
+
process.exit(1);
|
|
2826
|
+
}
|
|
2827
|
+
process.stdout.write(`
|
|
2828
|
+
${bold(pack.name)} (${pack.id})
|
|
2829
|
+
|
|
2830
|
+
`);
|
|
2831
|
+
process.stdout.write(`${pack.description}
|
|
2832
|
+
`);
|
|
2833
|
+
process.stdout.write(`Recommended rollout: ${pack.recommendedMode}
|
|
2834
|
+
|
|
2835
|
+
`);
|
|
2836
|
+
for (const file of pack.files) {
|
|
2837
|
+
process.stdout.write(`${dim(`--- ${file.path} ---`)}
|
|
2838
|
+
`);
|
|
2839
|
+
process.stdout.write(file.contents.endsWith("\n") ? file.contents : `${file.contents}
|
|
2840
|
+
`);
|
|
2841
|
+
process.stdout.write("\n");
|
|
2842
|
+
}
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (subcommand === "install") {
|
|
2846
|
+
const packs = packArg === "all" ? POLICY_PACKS : [getPolicyPack(packArg || "")].filter(Boolean);
|
|
2847
|
+
if (packs.length === 0) {
|
|
2848
|
+
process.stderr.write(`Usage: protect-mcp policy-packs install <${policyPackIds().join("|")}|all> [--dir ./cedar] [--force]
|
|
2849
|
+
`);
|
|
2850
|
+
process.exit(1);
|
|
2851
|
+
}
|
|
2852
|
+
mkdirSyncCli(dir, { recursive: true });
|
|
2853
|
+
const written = [];
|
|
2854
|
+
for (const pack of packs) {
|
|
2855
|
+
for (const file of pack.files) {
|
|
2856
|
+
const outPath = joinCli(dir, file.path);
|
|
2857
|
+
if (existsSyncCli(outPath) && !force) {
|
|
2858
|
+
process.stderr.write(`Refusing to overwrite ${outPath}. Re-run with --force if intentional.
|
|
2859
|
+
`);
|
|
2860
|
+
process.exit(1);
|
|
2861
|
+
}
|
|
2862
|
+
writeFileSyncCli(outPath, file.contents.endsWith("\n") ? file.contents : `${file.contents}
|
|
2863
|
+
`);
|
|
2864
|
+
written.push(outPath);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
process.stdout.write(`
|
|
2868
|
+
${bold("protect-mcp policy-packs install")}
|
|
2869
|
+
|
|
2870
|
+
`);
|
|
2871
|
+
process.stdout.write(` Directory: ${dir}
|
|
2872
|
+
`);
|
|
2873
|
+
for (const outPath of written) process.stdout.write(` Wrote: ${outPath}
|
|
2874
|
+
`);
|
|
2875
|
+
process.stdout.write(`
|
|
2876
|
+
Next: ${dim(`protect-mcp serve --cedar ${dir}`)} for shadow mode, then add ${dim("--enforce")} after reviewing receipts.
|
|
2877
|
+
|
|
2878
|
+
`);
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
process.stderr.write("Usage: protect-mcp policy-packs list|show|install [pack] [--dir ./cedar] [--force]\n");
|
|
2882
|
+
process.exit(1);
|
|
2883
|
+
}
|
|
2884
|
+
async function handleConnectors(argv) {
|
|
2885
|
+
const subcommand = argv[0] || "list";
|
|
2886
|
+
const pilotArg = argv[1];
|
|
2887
|
+
const dir = resolveCli(flagValue(argv, "--dir") || process.cwd());
|
|
2888
|
+
const force = argv.includes("--force");
|
|
2889
|
+
if (subcommand === "list") {
|
|
2890
|
+
process.stdout.write(`
|
|
2891
|
+
${bold("protect-mcp connector pilots")}
|
|
2892
|
+
|
|
2893
|
+
`);
|
|
2894
|
+
for (const pilot of CONNECTOR_PILOTS) {
|
|
2895
|
+
process.stdout.write(` ${bold(pilot.id.padEnd(18))} ${pilot.name}
|
|
2896
|
+
`);
|
|
2897
|
+
process.stdout.write(` ${dim(" ".repeat(20) + pilot.description)}
|
|
2898
|
+
`);
|
|
2899
|
+
process.stdout.write(` ${dim(" ".repeat(20) + `tools: ${pilot.tools.join(", ")}`)}
|
|
2900
|
+
|
|
2901
|
+
`);
|
|
2902
|
+
}
|
|
2903
|
+
process.stdout.write(`Install all: ${dim("protect-mcp connectors init all --force")}
|
|
2904
|
+
`);
|
|
2905
|
+
process.stdout.write(`Check credentials: ${dim("protect-mcp connectors doctor")}
|
|
2906
|
+
|
|
2907
|
+
`);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (subcommand === "show") {
|
|
2911
|
+
const pilot = getConnectorPilot(pilotArg || "");
|
|
2912
|
+
if (!pilot) {
|
|
2913
|
+
process.stderr.write(`Unknown connector pilot "${pilotArg || ""}". Available: ${CONNECTOR_PILOTS.map((p) => p.id).join(", ")}
|
|
2914
|
+
`);
|
|
2915
|
+
process.exit(1);
|
|
2916
|
+
}
|
|
2917
|
+
process.stdout.write(`
|
|
2918
|
+
${bold(pilot.name)} (${pilot.id})
|
|
2919
|
+
|
|
2920
|
+
`);
|
|
2921
|
+
process.stdout.write(`${pilot.description}
|
|
2922
|
+
|
|
2923
|
+
`);
|
|
2924
|
+
process.stdout.write(`${bold("Why it matters:")} ${pilot.value}
|
|
2925
|
+
|
|
2926
|
+
`);
|
|
2927
|
+
process.stdout.write(`${bold("Tools:")} ${pilot.tools.join(", ")}
|
|
2928
|
+
|
|
2929
|
+
`);
|
|
2930
|
+
process.stdout.write(`${bold("Setup:")}
|
|
2931
|
+
`);
|
|
2932
|
+
for (const step of pilot.setup) process.stdout.write(` - ${step}
|
|
2933
|
+
`);
|
|
2934
|
+
process.stdout.write(`
|
|
2935
|
+
${bold("Starter policy:")}
|
|
2936
|
+
${pilot.cedar}
|
|
2937
|
+
`);
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (subcommand === "init") {
|
|
2941
|
+
const ids = pilotArg ? [pilotArg] : ["all"];
|
|
2942
|
+
const installed = writeConnectorPilots({ dir, ids, force });
|
|
2943
|
+
process.stdout.write(`
|
|
2944
|
+
${bold("protect-mcp connectors init")}
|
|
2945
|
+
|
|
2946
|
+
`);
|
|
2947
|
+
process.stdout.write(` Directory: ${installed.directory}
|
|
2948
|
+
`);
|
|
2949
|
+
for (const outPath of installed.written) process.stdout.write(` Wrote: ${outPath}
|
|
2950
|
+
`);
|
|
2951
|
+
process.stdout.write(`
|
|
2952
|
+
Next: ${dim("protect-mcp connectors doctor")} then ${dim("protect-mcp dashboard --open")}.
|
|
2953
|
+
|
|
2954
|
+
`);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
if (subcommand === "doctor") {
|
|
2958
|
+
let rows = connectorDoctor(dir);
|
|
2959
|
+
if (pilotArg && pilotArg !== "all") {
|
|
2960
|
+
const pilot = getConnectorPilot(pilotArg);
|
|
2961
|
+
if (!pilot) {
|
|
2962
|
+
process.stderr.write(`Unknown connector pilot "${pilotArg}". Available: ${CONNECTOR_PILOTS.map((p) => p.id).join(", ")}
|
|
2963
|
+
`);
|
|
2964
|
+
process.exit(1);
|
|
2965
|
+
}
|
|
2966
|
+
rows = rows.filter((row) => row.id === pilot.id);
|
|
2967
|
+
}
|
|
2968
|
+
process.stdout.write(`
|
|
2969
|
+
${bold("protect-mcp connectors doctor")}
|
|
2970
|
+
|
|
2971
|
+
`);
|
|
2972
|
+
for (const row of rows) {
|
|
2973
|
+
const missing = Array.isArray(row.missing_required) && row.missing_required.length > 0 ? row.missing_required.join(", ") : "";
|
|
2974
|
+
const status = row.installed ? row.usable ? green("ready") : yellow("needs setup") : dim("not installed");
|
|
2975
|
+
process.stdout.write(` ${bold(String(row.id).padEnd(18))} ${status}
|
|
2976
|
+
`);
|
|
2977
|
+
process.stdout.write(` ${dim(" ".repeat(20) + `mode: ${String(row.mode || "unknown")}`)}
|
|
2978
|
+
`);
|
|
2979
|
+
if (missing) process.stdout.write(` ${yellow(" ".repeat(20) + `missing: ${missing}`)}
|
|
2980
|
+
`);
|
|
2981
|
+
process.stdout.write(` ${dim(" ".repeat(20) + String(row.next || ""))}
|
|
2982
|
+
|
|
2983
|
+
`);
|
|
2984
|
+
}
|
|
2985
|
+
process.stdout.write(`${dim("Secret values are never printed; only missing variable names are shown.")}
|
|
2986
|
+
|
|
2987
|
+
`);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
process.stderr.write("Usage: protect-mcp connectors list|show|init|doctor [connector|all] [--dir <path>] [--force]\n");
|
|
2991
|
+
process.exit(1);
|
|
2992
|
+
}
|
|
860
2993
|
async function handleTrace(argv) {
|
|
861
2994
|
const receiptId = argv[0];
|
|
862
2995
|
if (!receiptId) {
|
|
@@ -935,7 +3068,7 @@ ${bold("protect-mcp trace")}
|
|
|
935
3068
|
process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
|
|
936
3069
|
`);
|
|
937
3070
|
if (rendered.has(id)) {
|
|
938
|
-
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle
|
|
3071
|
+
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle: already rendered)")}
|
|
939
3072
|
`);
|
|
940
3073
|
return;
|
|
941
3074
|
}
|
|
@@ -1119,7 +3252,7 @@ ${bold("protect-mcp init-hooks")}
|
|
|
1119
3252
|
if (!existsSync(keysDir)) mkdirSync(keysDir, { recursive: true });
|
|
1120
3253
|
const { randomBytes: rb } = await import("crypto");
|
|
1121
3254
|
try {
|
|
1122
|
-
const { ed25519 } = await import("./ed25519-
|
|
3255
|
+
const { ed25519 } = await import("./ed25519-BSHMMVNX.mjs");
|
|
1123
3256
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
1124
3257
|
const privateKey = rb(32);
|
|
1125
3258
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -1138,7 +3271,7 @@ ${bold("protect-mcp init-hooks")}
|
|
|
1138
3271
|
|
|
1139
3272
|
`);
|
|
1140
3273
|
} catch {
|
|
1141
|
-
process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys
|
|
3274
|
+
process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys, signing disabled
|
|
1142
3275
|
|
|
1143
3276
|
`);
|
|
1144
3277
|
}
|
|
@@ -1209,13 +3342,14 @@ ${bold("protect-mcp init-hooks")}
|
|
|
1209
3342
|
process.stdout.write(` Every tool call will be receipted automatically.
|
|
1210
3343
|
|
|
1211
3344
|
`);
|
|
1212
|
-
process.stdout.write(` 3.
|
|
3345
|
+
process.stdout.write(` 3. See your record: a searchable view of every decision.
|
|
1213
3346
|
`);
|
|
1214
|
-
process.stdout.write(` ${dim(`
|
|
3347
|
+
process.stdout.write(` ${dim(`npx protect-mcp record`)}
|
|
1215
3348
|
`);
|
|
1216
|
-
process.stdout.write(` ${dim(`
|
|
3349
|
+
process.stdout.write(` ${dim(`Everything stays on this machine. Nothing is uploaded.`)}
|
|
3350
|
+
|
|
1217
3351
|
`);
|
|
1218
|
-
process.stdout.write(`
|
|
3352
|
+
process.stdout.write(` Prefer the terminal? ${dim(`npx protect-mcp receipts`)}, or ${dim("/verify-receipt")} in Claude Code.
|
|
1219
3353
|
|
|
1220
3354
|
`);
|
|
1221
3355
|
process.stdout.write(` 4. View policy suggestions:
|
|
@@ -1225,9 +3359,9 @@ ${bold("protect-mcp init-hooks")}
|
|
|
1225
3359
|
`);
|
|
1226
3360
|
process.stdout.write(`${bold("Key facts:")}
|
|
1227
3361
|
`);
|
|
1228
|
-
process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}
|
|
3362
|
+
process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}: they cannot be overridden
|
|
1229
3363
|
`);
|
|
1230
|
-
process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}
|
|
3364
|
+
process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}, so there is zero latency impact on tool execution
|
|
1231
3365
|
`);
|
|
1232
3366
|
process.stdout.write(` \u2022 Receipts are Ed25519-signed and append-only
|
|
1233
3367
|
`);
|
|
@@ -1246,15 +3380,7 @@ async function sendInstallTelemetry() {
|
|
|
1246
3380
|
if (existsSync(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
|
|
1247
3381
|
return;
|
|
1248
3382
|
}
|
|
1249
|
-
|
|
1250
|
-
try {
|
|
1251
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
1252
|
-
const pkgPath = join(thisDir, "..", "package.json");
|
|
1253
|
-
if (existsSync(pkgPath)) {
|
|
1254
|
-
version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
1255
|
-
}
|
|
1256
|
-
} catch {
|
|
1257
|
-
}
|
|
3383
|
+
const version = await pkgVersion();
|
|
1258
3384
|
const controller = new AbortController();
|
|
1259
3385
|
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1260
3386
|
fetch("https://api.scopeblind.com/telemetry/install", {
|
|
@@ -1428,6 +3554,7 @@ async function main() {
|
|
|
1428
3554
|
sendInstallTelemetry().catch(() => {
|
|
1429
3555
|
});
|
|
1430
3556
|
const args = process.argv.slice(2);
|
|
3557
|
+
process.env.PROTECT_MCP_VERSION = process.env.PROTECT_MCP_VERSION || await pkgVersion();
|
|
1431
3558
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
1432
3559
|
printHelp();
|
|
1433
3560
|
process.exit(0);
|
|
@@ -1466,6 +3593,10 @@ async function main() {
|
|
|
1466
3593
|
await startHookServer({ port, policyPath: policyPath2, cedarDir: cedarDir2, enforce: enforce2, verbose: verbose2 });
|
|
1467
3594
|
return;
|
|
1468
3595
|
}
|
|
3596
|
+
if (args[0] === "record") {
|
|
3597
|
+
await handleRecord(args.slice(1));
|
|
3598
|
+
process.exit(0);
|
|
3599
|
+
}
|
|
1469
3600
|
if (args[0] === "init-hooks") {
|
|
1470
3601
|
await handleInitHooks(args.slice(1));
|
|
1471
3602
|
process.exit(0);
|
|
@@ -1474,6 +3605,50 @@ async function main() {
|
|
|
1474
3605
|
await handleQuickstart(args.slice(1));
|
|
1475
3606
|
return;
|
|
1476
3607
|
}
|
|
3608
|
+
if (args[0] === "wrap") {
|
|
3609
|
+
await handleWrap(args.slice(1));
|
|
3610
|
+
process.exit(0);
|
|
3611
|
+
}
|
|
3612
|
+
if (args[0] === "dashboard") {
|
|
3613
|
+
await handleDashboard(args.slice(1));
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (args[0] === "recommend") {
|
|
3617
|
+
await handleRecommend(args.slice(1));
|
|
3618
|
+
process.exit(0);
|
|
3619
|
+
}
|
|
3620
|
+
if (args[0] === "registry") {
|
|
3621
|
+
await handleRegistry(args.slice(1));
|
|
3622
|
+
process.exit(0);
|
|
3623
|
+
}
|
|
3624
|
+
if (args[0] === "trial") {
|
|
3625
|
+
await handleKillerDemo(args.slice(1));
|
|
3626
|
+
process.stdout.write(`${bold("Next: open the local dashboard")}
|
|
3627
|
+
`);
|
|
3628
|
+
process.stdout.write(` npx protect-mcp dashboard --dir ${dim(flagValue(args.slice(1), "--dir") || "<demo dir printed above>")} --open
|
|
3629
|
+
|
|
3630
|
+
`);
|
|
3631
|
+
process.stdout.write(`${dim("No ScopeBlind account is required for local receipts. Add --hosted with SCOPEBLIND_TOKEN when you want independent digest anchoring.")}
|
|
3632
|
+
|
|
3633
|
+
`);
|
|
3634
|
+
process.exit(0);
|
|
3635
|
+
}
|
|
3636
|
+
if (args[0] === "killer-demo") {
|
|
3637
|
+
await handleKillerDemo(args.slice(1));
|
|
3638
|
+
process.exit(0);
|
|
3639
|
+
}
|
|
3640
|
+
if (args[0] === "verify-disclosure") {
|
|
3641
|
+
await handleVerifyDisclosure(args.slice(1));
|
|
3642
|
+
process.exit(0);
|
|
3643
|
+
}
|
|
3644
|
+
if (args[0] === "policy-packs") {
|
|
3645
|
+
await handlePolicyPacks(args.slice(1));
|
|
3646
|
+
process.exit(0);
|
|
3647
|
+
}
|
|
3648
|
+
if (args[0] === "connectors") {
|
|
3649
|
+
await handleConnectors(args.slice(1));
|
|
3650
|
+
process.exit(0);
|
|
3651
|
+
}
|
|
1477
3652
|
if (args[0] === "connect") {
|
|
1478
3653
|
await handleConnect();
|
|
1479
3654
|
process.exit(0);
|
|
@@ -1610,7 +3785,7 @@ async function main() {
|
|
|
1610
3785
|
if (useHttp) {
|
|
1611
3786
|
const portIdx = args.indexOf("--port");
|
|
1612
3787
|
const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
|
|
1613
|
-
const { startHttpTransport } = await import("./http-transport-
|
|
3788
|
+
const { startHttpTransport } = await import("./http-transport-JBORN27G.mjs");
|
|
1614
3789
|
startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
|
|
1615
3790
|
return;
|
|
1616
3791
|
}
|
|
@@ -1679,7 +3854,7 @@ async function handleDoctor() {
|
|
|
1679
3854
|
process.stdout.write(green2(`Node.js ${nodeVersion}
|
|
1680
3855
|
`));
|
|
1681
3856
|
} else {
|
|
1682
|
-
process.stdout.write(red2(`Node.js ${nodeVersion}
|
|
3857
|
+
process.stdout.write(red2(`Node.js ${nodeVersion}, requires >= 18
|
|
1683
3858
|
`));
|
|
1684
3859
|
issues++;
|
|
1685
3860
|
}
|
|
@@ -1690,7 +3865,7 @@ async function handleDoctor() {
|
|
|
1690
3865
|
if (config.signing?.private_key || config.signing?.key_file) {
|
|
1691
3866
|
process.stdout.write(green2("Signing keys configured\n"));
|
|
1692
3867
|
} else {
|
|
1693
|
-
process.stdout.write(yellow2("Config found but no signing keys
|
|
3868
|
+
process.stdout.write(yellow2("Config found but no signing keys. Run: protect-mcp init\n"));
|
|
1694
3869
|
issues++;
|
|
1695
3870
|
}
|
|
1696
3871
|
} catch {
|
|
@@ -1698,7 +3873,7 @@ async function handleDoctor() {
|
|
|
1698
3873
|
issues++;
|
|
1699
3874
|
}
|
|
1700
3875
|
} else {
|
|
1701
|
-
process.stdout.write(yellow2("No scopeblind.config.json
|
|
3876
|
+
process.stdout.write(yellow2("No scopeblind.config.json. Run: protect-mcp init\n"));
|
|
1702
3877
|
}
|
|
1703
3878
|
let policyFound = false;
|
|
1704
3879
|
for (const dir of ["cedar", "policies", "."]) {
|
|
@@ -1723,14 +3898,14 @@ async function handleDoctor() {
|
|
|
1723
3898
|
}
|
|
1724
3899
|
}
|
|
1725
3900
|
if (!policyFound) {
|
|
1726
|
-
process.stdout.write(yellow2("No policy files found
|
|
3901
|
+
process.stdout.write(yellow2("No policy files found, running in shadow mode (allow all)\n"));
|
|
1727
3902
|
}
|
|
1728
3903
|
try {
|
|
1729
3904
|
const cedarAvailable = await isCedarAvailable();
|
|
1730
3905
|
if (cedarAvailable) {
|
|
1731
3906
|
process.stdout.write(green2("Cedar WASM engine available\n"));
|
|
1732
3907
|
} else {
|
|
1733
|
-
process.stdout.write(dim2(" Cedar WASM not installed
|
|
3908
|
+
process.stdout.write(dim2(" Cedar WASM not installed. Install: npm install @cedar-policy/cedar-wasm\n"));
|
|
1734
3909
|
}
|
|
1735
3910
|
} catch {
|
|
1736
3911
|
process.stdout.write(dim2(" Cedar WASM not installed\n"));
|
|
@@ -1746,7 +3921,7 @@ async function handleDoctor() {
|
|
|
1746
3921
|
process.stdout.write(green2("Decision log exists\n"));
|
|
1747
3922
|
}
|
|
1748
3923
|
} else {
|
|
1749
|
-
process.stdout.write(dim2(" No decision log yet
|
|
3924
|
+
process.stdout.write(dim2(" No decision log yet, will be created on first tool call\n"));
|
|
1750
3925
|
}
|
|
1751
3926
|
if (existsSync(receiptFile)) {
|
|
1752
3927
|
try {
|
|
@@ -1761,17 +3936,17 @@ async function handleDoctor() {
|
|
|
1761
3936
|
execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
|
|
1762
3937
|
process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
|
|
1763
3938
|
} catch {
|
|
1764
|
-
process.stdout.write(dim2(" Verifier not cached
|
|
3939
|
+
process.stdout.write(dim2(" Verifier not cached. Install: npm install -g @veritasacta/verify\n"));
|
|
1765
3940
|
}
|
|
1766
3941
|
try {
|
|
1767
3942
|
const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
|
|
1768
3943
|
if (res.ok) {
|
|
1769
3944
|
process.stdout.write(green2("ScopeBlind API reachable\n"));
|
|
1770
3945
|
} else {
|
|
1771
|
-
process.stdout.write(yellow2("ScopeBlind API returned non-200
|
|
3946
|
+
process.stdout.write(yellow2("ScopeBlind API returned non-200, receipts will be stored locally\n"));
|
|
1772
3947
|
}
|
|
1773
3948
|
} catch {
|
|
1774
|
-
process.stdout.write(dim2(" ScopeBlind API not reachable
|
|
3949
|
+
process.stdout.write(dim2(" ScopeBlind API not reachable, offline mode (receipts stored locally)\n"));
|
|
1775
3950
|
}
|
|
1776
3951
|
process.stdout.write("\nRestraint self-test:\n");
|
|
1777
3952
|
try {
|