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/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
- simulate
6
- } from "./chunk-ZBKJANP7.mjs";
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-OHUTUFTC.mjs";
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-546U3A7R.mjs";
29
+ } from "./chunk-WIPWNWMJ.mjs";
21
30
  import "./chunk-PQJP2ZCI.mjs";
22
31
 
23
32
  // src/cli.ts
24
- import { readFileSync as readFileSyncCli, existsSync as existsSyncCli, appendFileSync as appendFileSyncCli, mkdirSync as mkdirSyncCli } from "fs";
25
- import { basename as basenameCli, join as joinCli } from "path";
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 \u2014 Enterprise security gateway for MCP servers & Claude Code hooks
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-DZMMNNVE.mjs");
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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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-XTR3YMPO.mjs");
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 \u2014 no credit card required.
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-DZMMNNVE.mjs");
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 \u2014 no credit card required.
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 \u2014 already rendered)")}
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-DZMMNNVE.mjs");
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 \u2014 signing disabled
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. Check receipts:
3345
+ process.stdout.write(` 3. See your record: a searchable view of every decision.
1213
3346
  `);
1214
- process.stdout.write(` ${dim(`curl http://127.0.0.1:${port}/receipts/latest`)}
3347
+ process.stdout.write(` ${dim(`npx protect-mcp record`)}
1215
3348
  `);
1216
- process.stdout.write(` ${dim(`npx protect-mcp receipts`)}
3349
+ process.stdout.write(` ${dim(`Everything stays on this machine. Nothing is uploaded.`)}
3350
+
1217
3351
  `);
1218
- process.stdout.write(` Or use ${dim("/verify-receipt")} inside Claude Code.
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")} \u2014 cannot be overridden
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")} \u2014 zero latency impact on tool execution
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
- let version = "unknown";
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-R5AO7X6D.mjs");
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} \u2014 requires >= 18
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 \u2014 run: protect-mcp init\n"));
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 \u2014 run: protect-mcp init\n"));
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 \u2014 running in shadow mode (allow all)\n"));
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 \u2014 install: npm install @cedar-policy/cedar-wasm\n"));
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 \u2014 will be created on first tool call\n"));
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 \u2014 install: npm install -g @veritasacta/verify\n"));
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 \u2014 receipts will be stored locally\n"));
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 \u2014 offline mode (receipts stored locally)\n"));
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 {