protect-mcp 0.7.2 → 0.7.4

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-36UID5WY.mjs";
7
15
  import {
8
16
  ProtectGateway,
9
17
  validateCredentials
10
- } from "./chunk-OHUTUFTC.mjs";
18
+ } from "./chunk-UBZJ3VI2.mjs";
11
19
  import {
20
+ buildActionReadback,
12
21
  evaluateCedar,
13
22
  initSigning,
14
23
  isCedarAvailable,
@@ -17,12 +26,14 @@ import {
17
26
  policySetFromSource,
18
27
  runEvaluatorSelfTest,
19
28
  signDecision
20
- } from "./chunk-546U3A7R.mjs";
29
+ } from "./chunk-D2RDY2JR.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
39
  protect-mcp \u2014 Enterprise security gateway for MCP servers & Claude Code hooks
@@ -32,6 +43,15 @@ Usage:
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
@@ -59,6 +79,15 @@ Commands:
59
79
  sign Sign one tool call into a receipt (PostToolUse)
60
80
  init-hooks Generate Claude Code hook config + skill + sample Cedar policy
61
81
  quickstart Zero-config onboarding: init + demo + show receipts in one command
82
+ wrap Print or install a protect-mcp wrapper for MCP servers
83
+ dashboard Start a local-only action dashboard from logs/receipts
84
+ recommend Draft a policy from shadow-mode call inventory
85
+ registry Paid-boundary receipt digest registry and verifier page
86
+ trial Build the 10-minute self-serve proof path locally
87
+ killer-demo Build a 3-minute shadow\u2192policy\u2192approval\u2192receipt demo pack
88
+ connectors Install and inspect real connector pilots
89
+ verify-disclosure Verify a v0 selective-disclosure package and explain hidden fields
90
+ policy-packs List, inspect, or install starter Cedar policy packs
62
91
  connect Create a ScopeBlind sandbox dashboard and configure receipt upload
63
92
  init Generate config template, Ed25519 keypair, and sample policy
64
93
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
@@ -75,6 +104,17 @@ Examples:
75
104
  protect-mcp init-hooks # One-command Claude Code setup
76
105
  protect-mcp quickstart
77
106
  protect-mcp quickstart --connect # Quickstart + create dashboard
107
+ protect-mcp wrap -- node my-server.js # Print wrapped MCP command
108
+ protect-mcp wrap --claude-desktop --write # Patch Claude Desktop MCP config
109
+ protect-mcp dashboard --open # Local risk/inventory dashboard
110
+ protect-mcp recommend --write # Draft a policy from observed calls
111
+ protect-mcp registry anchor --hosted # Upload only receipt digests for anchoring
112
+ protect-mcp trial --dir ./scopeblind-trial # Generate self-serve trial artifacts
113
+ protect-mcp killer-demo --dir ./scopeblind-demo # Generate sales-demo artifacts
114
+ protect-mcp connectors init all --force # Install connector pilot configs
115
+ protect-mcp connectors doctor # Check connector credentials safely
116
+ protect-mcp verify-disclosure --receipt committed.json --disclosure tool-only.json
117
+ protect-mcp policy-packs install filesystem-safe --dir ./cedar
78
118
  protect-mcp connect # Connect existing setup to dashboard
79
119
  protect-mcp -- node my-server.js
80
120
  protect-mcp init
@@ -84,6 +124,7 @@ Examples:
84
124
  protect-mcp bundle --output audit.json
85
125
 
86
126
  Dashboard:
127
+ npx protect-mcp dashboard Local-only dashboard (127.0.0.1; no account)
87
128
  npx protect-mcp connect Create a free ScopeBlind dashboard
88
129
  Free up to 20,000 receipts/month
89
130
 
@@ -466,6 +507,1295 @@ ${bold("protect-mcp status")}
466
507
  process.stdout.write(`
467
508
  Log file: ${dim(logPath)}
468
509
 
510
+ `);
511
+ }
512
+ function commandNeedsValue(argv, flag) {
513
+ const value = flagValue(argv, flag);
514
+ return Boolean(value && !value.startsWith("--"));
515
+ }
516
+ function absoluteOrCwd(pathValue) {
517
+ return resolveCli(process.cwd(), pathValue);
518
+ }
519
+ function shellQuoteArg(arg) {
520
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
521
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
522
+ }
523
+ function shellCommand(command, args) {
524
+ return [command, ...args].map(shellQuoteArg).join(" ");
525
+ }
526
+ function wrapperArgsFor(command, opts) {
527
+ const args = ["-y", "protect-mcp@latest"];
528
+ if (opts.cedarDir) args.push("--cedar", opts.cedarDir);
529
+ else args.push("--policy", opts.configPath || absoluteOrCwd("protect-mcp.json"));
530
+ if (opts.enforce) args.push("--enforce");
531
+ args.push("--", ...command);
532
+ return args;
533
+ }
534
+ function claudeDesktopConfigPath() {
535
+ if (process.platform === "darwin") {
536
+ return joinCli(homedirCli(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
537
+ }
538
+ if (process.platform === "win32") {
539
+ return joinCli(process.env.APPDATA || joinCli(homedirCli(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
540
+ }
541
+ return joinCli(homedirCli(), ".config", "Claude", "claude_desktop_config.json");
542
+ }
543
+ async function ensureLocalConfig(dir = process.cwd()) {
544
+ const { existsSync } = await import("fs");
545
+ const { join, resolve } = await import("path");
546
+ const configPath = join(dir, "protect-mcp.json");
547
+ if (!existsSync(configPath)) {
548
+ process.stderr.write(`${bold("protect-mcp wrap")}
549
+
550
+ No protect-mcp.json found; creating local shadow-mode config first.
551
+
552
+ `);
553
+ await handleInit(["--dir", dir]);
554
+ }
555
+ return resolve(configPath);
556
+ }
557
+ function parseJsonlFile(pathValue) {
558
+ try {
559
+ const raw = readFileSyncCli(pathValue, "utf-8");
560
+ return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
561
+ try {
562
+ return [JSON.parse(line)];
563
+ } catch {
564
+ return [];
565
+ }
566
+ });
567
+ } catch {
568
+ return [];
569
+ }
570
+ }
571
+ function parseJsonlRecords(pathValue) {
572
+ try {
573
+ const raw = readFileSyncCli(pathValue, "utf-8");
574
+ return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
575
+ try {
576
+ return [{
577
+ value: JSON.parse(line),
578
+ raw: line,
579
+ hash: createHashCli("sha256").update(line).digest("hex")
580
+ }];
581
+ } catch {
582
+ return [];
583
+ }
584
+ });
585
+ } catch {
586
+ return [];
587
+ }
588
+ }
589
+ function loadPolicyJson(policyPath) {
590
+ try {
591
+ if (!existsSyncCli(policyPath)) return null;
592
+ return JSON.parse(readFileSyncCli(policyPath, "utf-8"));
593
+ } catch {
594
+ return null;
595
+ }
596
+ }
597
+ function policyCoverageForTool(tool, policy) {
598
+ const tools = policy?.tools && typeof policy.tools === "object" ? policy.tools : {};
599
+ if (tools[tool]) {
600
+ return { status: "exact", label: "Exact rule", policy: tools[tool] };
601
+ }
602
+ if (tools["*"]) {
603
+ return { status: "wildcard", label: "Wildcard fallback", policy: tools["*"] };
604
+ }
605
+ return { status: "none", label: "No rule" };
606
+ }
607
+ function receiptRequestId(receipt) {
608
+ const direct = receipt.request_id || receipt.scope;
609
+ if (typeof direct === "string") return direct;
610
+ const payload = receipt.payload;
611
+ if (payload && typeof payload === "object") {
612
+ const candidate = payload.request_id || payload.scope;
613
+ if (typeof candidate === "string") return candidate;
614
+ }
615
+ const claims = receipt.signed_claims;
616
+ if (claims && typeof claims === "object") {
617
+ const nestedClaims = claims.claims;
618
+ if (nestedClaims && typeof nestedClaims === "object") {
619
+ const candidate = nestedClaims.request_id || nestedClaims.scope;
620
+ if (typeof candidate === "string") return candidate;
621
+ }
622
+ }
623
+ return void 0;
624
+ }
625
+ function buildReceiptChains(entries, receipts) {
626
+ const receiptMap = /* @__PURE__ */ new Map();
627
+ for (const receipt of receipts) {
628
+ const requestId = receiptRequestId(receipt.value);
629
+ if (!requestId) continue;
630
+ const rows = receiptMap.get(requestId) || [];
631
+ rows.push(receipt);
632
+ receiptMap.set(requestId, rows);
633
+ }
634
+ const logMap = /* @__PURE__ */ new Map();
635
+ for (const entry of entries) {
636
+ if (!entry.request_id) continue;
637
+ const rows = logMap.get(entry.request_id) || [];
638
+ rows.push(entry);
639
+ logMap.set(entry.request_id, rows);
640
+ }
641
+ return [...logMap.entries()].map(([requestId, logs]) => {
642
+ const relatedReceipts = receiptMap.get(requestId) || [];
643
+ const latest = logs[logs.length - 1];
644
+ return {
645
+ request_id: requestId,
646
+ tool: latest?.tool || "unknown",
647
+ decision: latest?.decision || "unknown",
648
+ reason_code: latest?.reason_code || "",
649
+ action_readback: latest?.action_readback,
650
+ log_events: logs.map((log) => ({
651
+ decision: log.decision,
652
+ reason_code: log.reason_code,
653
+ timestamp: log.timestamp,
654
+ hook_event: log.hook_event
655
+ })),
656
+ receipts: relatedReceipts.map((receipt) => ({
657
+ hash: receipt.hash,
658
+ type: String(receipt.value.type || receipt.value.artifact_type || "receipt")
659
+ })),
660
+ complete: relatedReceipts.length > 0
661
+ };
662
+ }).sort((a, b) => {
663
+ const at = a.log_events[0]?.timestamp || 0;
664
+ const bt = b.log_events[0]?.timestamp || 0;
665
+ return bt - at;
666
+ }).slice(0, 80);
667
+ }
668
+ function riskForTool(toolRaw) {
669
+ const tool = toolRaw.toLowerCase();
670
+ const reasons = [];
671
+ const highPatterns = [
672
+ ["delete", "delete/destructive"],
673
+ ["remove", "delete/destructive"],
674
+ ["rm", "delete/destructive"],
675
+ ["write", "writes data"],
676
+ ["send", "external send"],
677
+ ["email", "external comms"],
678
+ ["slack", "external comms"],
679
+ ["teams", "external comms"],
680
+ ["github", "source-control mutation"],
681
+ ["commit", "source-control mutation"],
682
+ ["push", "source-control mutation"],
683
+ ["deploy", "deployment"],
684
+ ["terraform", "cloud infrastructure"],
685
+ ["aws", "cloud infrastructure"],
686
+ ["gcp", "cloud infrastructure"],
687
+ ["azure", "cloud infrastructure"],
688
+ ["sql", "database access"],
689
+ ["database", "database access"],
690
+ ["payment", "spend/payment"],
691
+ ["order", "order/transaction"],
692
+ ["trade", "trade/transaction"],
693
+ ["pms", "portfolio-system mutation"],
694
+ ["book", "portfolio-system mutation"],
695
+ ["fill", "portfolio-system mutation"],
696
+ ["secret", "secrets"],
697
+ ["token", "secrets"]
698
+ ];
699
+ for (const [needle, label] of highPatterns) {
700
+ if (tool.includes(needle) && !reasons.includes(label)) reasons.push(label);
701
+ }
702
+ if (reasons.length > 0) return { tier: "high", reasons };
703
+ if (tool.includes("read") || tool.includes("search") || tool.includes("list") || tool.includes("fetch")) {
704
+ return { tier: "medium", reasons: ["data access"] };
705
+ }
706
+ return { tier: "low", reasons: ["observed"] };
707
+ }
708
+ function suggestedGuardrailFor(_tool, risk, reasons) {
709
+ const reasonSet = new Set(reasons);
710
+ if (reasonSet.has("delete/destructive") || reasonSet.has("secrets")) {
711
+ return {
712
+ action: "Block by default",
713
+ reason: "Destructive and secret-handling tools should start deny-first.",
714
+ policy: { block: true, min_tier: "privileged" }
715
+ };
716
+ }
717
+ 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")) {
718
+ return {
719
+ action: "Require approval",
720
+ reason: "Consequential tools should require a human approval receipt before enforce mode.",
721
+ policy: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" }
722
+ };
723
+ }
724
+ if (risk === "medium") {
725
+ return {
726
+ action: "Rate-limit and identify",
727
+ reason: "Read/search/fetch tools can leak data at scale; keep them visible and bounded.",
728
+ policy: { min_tier: "signed-known", rate_limit: "60/hour" }
729
+ };
730
+ }
731
+ return {
732
+ action: "Observe",
733
+ reason: "Low-risk observed tool. Keep receipts and a broad rate limit.",
734
+ policy: { rate_limit: "100/hour" }
735
+ };
736
+ }
737
+ function buildDashboardSummary(dir, policyPath = joinCli(dir, "protect-mcp.json")) {
738
+ const logPath = joinCli(dir, ".protect-mcp-log.jsonl");
739
+ const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
740
+ const keyPath = joinCli(dir, "keys", "gateway.json");
741
+ const entries = parseJsonlFile(logPath);
742
+ const receiptRecords = parseJsonlRecords(receiptPath);
743
+ const receipts = receiptRecords.map((record) => record.value);
744
+ const activePolicy = loadPolicyJson(policyPath);
745
+ const tools = /* @__PURE__ */ new Map();
746
+ for (const entry of entries) {
747
+ const tool = String(entry.tool || "unknown");
748
+ const risk = riskForTool(tool);
749
+ const current = tools.get(tool) || {
750
+ tool,
751
+ calls: 0,
752
+ allows: 0,
753
+ denies: 0,
754
+ reviews: 0,
755
+ risk: risk.tier,
756
+ reasons: risk.reasons
757
+ };
758
+ current.calls += 1;
759
+ if (entry.decision === "allow") current.allows += 1;
760
+ else if (entry.decision === "deny") current.denies += 1;
761
+ else if (entry.decision === "require_approval") current.reviews += 1;
762
+ if (risk.tier === "high" || risk.tier === "medium" && current.risk === "low") current.risk = risk.tier;
763
+ current.reasons = [.../* @__PURE__ */ new Set([...current.reasons, ...risk.reasons])];
764
+ if (typeof entry.timestamp === "number") current.last_seen = new Date(entry.timestamp).toISOString();
765
+ tools.set(tool, current);
766
+ }
767
+ const toolRows = [...tools.values()].sort((a, b) => {
768
+ const order = { high: 0, medium: 1, low: 2 };
769
+ return order[a.risk] - order[b.risk] || b.calls - a.calls || a.tool.localeCompare(b.tool);
770
+ }).map((tool) => ({
771
+ ...tool,
772
+ suggestion: suggestedGuardrailFor(tool.tool, tool.risk, tool.reasons),
773
+ policy_coverage: policyCoverageForTool(tool.tool, activePolicy)
774
+ }));
775
+ const highRisk = toolRows.filter((t) => t.risk === "high");
776
+ const uncovered = toolRows.filter((t) => t.policy_coverage.status === "none").length;
777
+ const exactCovered = toolRows.filter((t) => t.policy_coverage.status === "exact").length;
778
+ const wildcardCovered = toolRows.filter((t) => t.policy_coverage.status === "wildcard").length;
779
+ const allowed = entries.filter((e) => e.decision === "allow").length;
780
+ const denied = entries.filter((e) => e.decision === "deny").length;
781
+ const review = entries.filter((e) => e.decision === "require_approval").length;
782
+ const pendingApprovals = entries.filter((e) => e.decision === "require_approval").slice(-25).reverse();
783
+ const chains = buildReceiptChains(entries, receiptRecords);
784
+ let key = null;
785
+ if (existsSyncCli(keyPath)) {
786
+ try {
787
+ const parsed = JSON.parse(readFileSyncCli(keyPath, "utf-8"));
788
+ key = {
789
+ kid: parsed.kid || null,
790
+ issuer: parsed.issuer || "protect-mcp",
791
+ publicKeyPrefix: typeof parsed.publicKey === "string" ? `${parsed.publicKey.slice(0, 16)}...` : null
792
+ };
793
+ } catch {
794
+ }
795
+ }
796
+ return {
797
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
798
+ dir,
799
+ files: {
800
+ log: logPath,
801
+ receipts: receiptPath,
802
+ key: keyPath,
803
+ policy: policyPath,
804
+ log_exists: existsSyncCli(logPath),
805
+ receipts_exist: existsSyncCli(receiptPath),
806
+ key_exists: existsSyncCli(keyPath),
807
+ policy_exists: existsSyncCli(policyPath)
808
+ },
809
+ totals: {
810
+ decisions: entries.length,
811
+ receipts: receipts.length,
812
+ tools: toolRows.length,
813
+ high_risk_tools: highRisk.length,
814
+ exact_covered: exactCovered,
815
+ wildcard_covered: wildcardCovered,
816
+ uncovered,
817
+ allowed,
818
+ denied,
819
+ review
820
+ },
821
+ key,
822
+ policy: activePolicy ? {
823
+ path: policyPath,
824
+ digest: createHashCli("sha256").update(JSON.stringify(activePolicy)).digest("hex").slice(0, 16),
825
+ default_tier: activePolicy.default_tier || "unknown",
826
+ tools: activePolicy.tools || {}
827
+ } : null,
828
+ tools: toolRows,
829
+ pending_approvals: pendingApprovals,
830
+ receipt_chains: chains,
831
+ recent: entries.slice(-50).reverse(),
832
+ policy_packs: {
833
+ directory: policyPackDirectory(dir),
834
+ installed: installedPolicyPackIds(dir),
835
+ available: POLICY_PACKS.map((pack) => ({
836
+ id: pack.id,
837
+ name: pack.name,
838
+ description: pack.description,
839
+ recommendedMode: pack.recommendedMode,
840
+ files: pack.files.map((file) => ({ path: file.path, contents: file.contents }))
841
+ }))
842
+ },
843
+ connector_pilots: {
844
+ directory: joinCli(dir, ".protect-mcp", "connectors"),
845
+ installed: readInstalledConnectorPilots(dir),
846
+ doctor: connectorDoctor(dir),
847
+ available: CONNECTOR_PILOTS.map((pilot) => ({
848
+ id: pilot.id,
849
+ name: pilot.name,
850
+ category: pilot.category,
851
+ description: pilot.description,
852
+ value: pilot.value,
853
+ tools: pilot.tools,
854
+ actions: pilot.actions,
855
+ setup: pilot.setup
856
+ }))
857
+ },
858
+ registry: dashboardRegistryStatus(dir),
859
+ recommendations: [
860
+ entries.length === 0 ? "Run in shadow mode first: npx protect-mcp -- node your-mcp-server.js" : "",
861
+ highRisk.length > 0 ? "Run npx protect-mcp recommend --write, review the generated policy, then restart your wrapper with --enforce." : "",
862
+ receipts.length === 0 ? "Run npx protect-mcp init so decisions are signed into local receipts." : "",
863
+ "Install a starter policy pack from this dashboard when you know the tool class: filesystem, Git, email, database, cloud spend, secrets, or finance.",
864
+ "Create a registry preview locally, then use hosted digest anchoring when you need independent timestamp evidence.",
865
+ "Export an audit bundle with: npx protect-mcp bundle --output audit.json"
866
+ ].filter(Boolean)
867
+ };
868
+ }
869
+ function dashboardHtml() {
870
+ return `<!doctype html>
871
+ <html lang="en">
872
+ <head>
873
+ <meta charset="utf-8">
874
+ <meta name="viewport" content="width=device-width, initial-scale=1">
875
+ <title>protect-mcp local action dashboard</title>
876
+ <style>
877
+ :root {
878
+ color-scheme: light;
879
+ --ink:#12110e; --muted:#6f6b61; --soft:#9a9488; --line:#ded7c9;
880
+ --paper:#f7f3ea; --card:#fffdf7; --black:#11110f;
881
+ --bad:#8f241c; --warn:#8d620f; --ok:#2f6f4e;
882
+ --shadow: 0 22px 70px rgba(36,30,18,.10);
883
+ }
884
+ * { box-sizing: border-box; }
885
+ 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; }
886
+ main { width:min(1380px, calc(100vw - 32px)); margin:24px auto 60px; }
887
+ .hero { display:grid; grid-template-columns:1.35fr .65fr; gap:16px; align-items:stretch; }
888
+ .card { background:rgba(255,253,247,.92); border:1px solid var(--line); border-radius:24px; padding:22px; box-shadow:var(--shadow); }
889
+ .kicker { text-transform:uppercase; letter-spacing:.18em; font-size:11px; color:var(--muted); font-weight:800; }
890
+ 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; }
891
+ h2 { margin:0 0 12px; font-size:17px; letter-spacing:-.01em; }
892
+ h3 { margin:0 0 8px; font-size:14px; }
893
+ p { color:var(--muted); margin:0; }
894
+ small { color:var(--muted); }
895
+ .layout { display:grid; grid-template-columns:1.28fr .72fr; gap:16px; margin-top:16px; }
896
+ .stack { display:grid; gap:16px; }
897
+ .stats { display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin:16px 0; }
898
+ .stat { background:#fffaf0; border:1px solid var(--line); border-radius:18px; padding:13px; min-height:86px; }
899
+ .stat strong { display:block; font-size:26px; letter-spacing:-.04em; }
900
+ .actions { display:flex; flex-wrap:wrap; gap:9px; margin-top:16px; }
901
+ 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; }
902
+ button.secondary, a.btn.secondary { background:transparent; color:var(--black); }
903
+ button.ghost { border-color:var(--line); background:#fffaf0; color:var(--ink); }
904
+ button.danger { background:#7f1d18; border-color:#7f1d18; }
905
+ table { width:100%; border-collapse:collapse; }
906
+ th, td { text-align:left; padding:12px 9px; border-bottom:1px solid var(--line); vertical-align:top; }
907
+ th { color:var(--muted); font-size:11px; letter-spacing:.09em; text-transform:uppercase; }
908
+ .pill { display:inline-flex; align-items:center; border-radius:999px; padding:4px 9px; font-size:11px; font-weight:900; white-space:nowrap; }
909
+ .high { background:#f7d9d3; color:var(--bad); }
910
+ .medium { background:#f4e5bd; color:var(--warn); }
911
+ .low { background:#dcebdd; color:var(--ok); }
912
+ .exact { background:#dcebdd; color:var(--ok); }
913
+ .wildcard { background:#e5decc; color:#5e5545; }
914
+ .none { background:#f7d9d3; color:var(--bad); }
915
+ .allow { color:var(--ok); } .deny { color:var(--bad); } .require_approval { color:var(--warn); }
916
+ code { background:#f2eadc; border:1px solid var(--line); border-radius:8px; padding:2px 6px; }
917
+ pre { white-space:pre-wrap; background:#181712; color:#f8f1df; border-radius:16px; padding:14px; overflow:auto; font-size:12px; }
918
+ .muted { color:var(--muted); }
919
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
920
+ .approval { border:1px solid var(--line); background:#fffaf0; border-radius:18px; padding:14px; margin-bottom:10px; }
921
+ .readback { background:#181712; color:#f8f1df; border-radius:16px; padding:12px; margin:10px 0; }
922
+ .readback .label { color:#bdb49f; font-size:11px; text-transform:uppercase; letter-spacing:.12em; }
923
+ .preview { max-height:180px; overflow:auto; }
924
+ .row-actions { display:flex; flex-wrap:wrap; gap:6px; }
925
+ .chain { display:grid; gap:8px; }
926
+ .chain-item { border:1px solid var(--line); border-radius:16px; padding:12px; background:#fffaf0; }
927
+ .pack-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin-top:12px; }
928
+ .pack { border:1px solid var(--line); border-radius:16px; padding:12px; background:#fffaf0; display:grid; gap:8px; }
929
+ .pack.installed { border-color:rgba(47,111,78,.35); background:#f3f8ef; }
930
+ .divider { border:0; border-top:1px solid var(--line); margin:14px 0; }
931
+ .field { display:grid; gap:5px; margin-top:9px; }
932
+ .field label { font-size:11px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted); font-weight:900; }
933
+ 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; }
934
+ .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; }
935
+ @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; } }
936
+ </style>
937
+ </head>
938
+ <body>
939
+ <main>
940
+ <section class="hero">
941
+ <div class="card">
942
+ <div class="kicker">Local Action Dashboard</div>
943
+ <h1>See what agents can do. Control dangerous actions. Prove what happened.</h1>
944
+ <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>
945
+ <div class="actions">
946
+ <button onclick="refresh()">Refresh</button>
947
+ <button class="secondary" onclick="exportBundle()">Export audit bundle</button>
948
+ <a class="btn secondary" href="/api/summary">Raw JSON</a>
949
+ </div>
950
+ </div>
951
+ <div class="card">
952
+ <div class="kicker">Next move</div>
953
+ <h2>Shadow, review, then enforce</h2>
954
+ <pre>npx protect-mcp wrap -- node your-server.js
955
+ npx protect-mcp dashboard --open
956
+ npx protect-mcp recommend --write
957
+ npx protect-mcp --policy protect-mcp.json --enforce -- node your-server.js</pre>
958
+ <p class="muted" id="dir"></p>
959
+ <p class="muted" id="policy"></p>
960
+ </div>
961
+ </section>
962
+
963
+ <section class="stats" id="stats"></section>
964
+
965
+ <section class="layout">
966
+ <div class="stack">
967
+ <section class="card">
968
+ <h2>Tool Inventory</h2>
969
+ <p>Risk, policy coverage, and one-click guardrail drafting for each observed tool.</p>
970
+ <div style="overflow:auto; margin-top:12px"><table id="tools"></table></div>
971
+ </section>
972
+ <section class="card">
973
+ <h2>Policy Packs</h2>
974
+ <p>Install a starter Cedar pack from the dashboard. This removes the blank-policy problem while keeping final enforcement local and reviewable.</p>
975
+ <div id="policy-packs" class="pack-grid"></div>
976
+ </section>
977
+ <section class="card">
978
+ <h2>Connector Pilots</h2>
979
+ <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>
980
+ <div id="connector-pilots" class="pack-grid"></div>
981
+ </section>
982
+ <section class="card">
983
+ <h2>Call History</h2>
984
+ <p>What agents actually tried to do, including exact-action readbacks when available.</p>
985
+ <div id="recent" style="margin-top:12px"></div>
986
+ </section>
987
+ </div>
988
+ <div class="stack">
989
+ <section class="card">
990
+ <h2>Approval Queue</h2>
991
+ <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>
992
+ <div id="approvals" style="margin-top:12px"></div>
993
+ </section>
994
+ <section class="card">
995
+ <h2>Receipt Chain</h2>
996
+ <p>Decision logs correlated with signed receipts by request id.</p>
997
+ <div id="chains" class="chain" style="margin-top:12px"></div>
998
+ </section>
999
+ <section class="card">
1000
+ <h2>Paid Boundary</h2>
1001
+ <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>
1002
+ <div id="registry" style="margin-top:12px"></div>
1003
+ </section>
1004
+ <section class="card">
1005
+ <h2>Recommendations</h2>
1006
+ <div id="recommendations"></div>
1007
+ </section>
1008
+ </div>
1009
+ </section>
1010
+ </main>
1011
+ <div class="toast" id="toast"></div>
1012
+ <script>
1013
+ var state = null;
1014
+ async function refresh(){
1015
+ state = await fetch('/api/summary').then(function(r){ return r.json(); });
1016
+ render(state);
1017
+ }
1018
+ function render(data){
1019
+ document.getElementById('dir').textContent = 'Reading: ' + data.dir;
1020
+ document.getElementById('policy').textContent = 'Policy: ' + ((data.files || {}).policy || 'protect-mcp.json');
1021
+ var t = data.totals || {};
1022
+ document.getElementById('stats').innerHTML = [
1023
+ ['Decisions', t.decisions || 0, 'All observed tool decisions'],
1024
+ ['High risk', t.high_risk_tools || 0, 'Tools that can mutate, send, trade, deploy, or expose secrets'],
1025
+ ['Exact rules', t.exact_covered || 0, 'Tools with explicit policy entries'],
1026
+ ['Uncovered', t.uncovered || 0, 'Tools falling through without exact policy'],
1027
+ ['Receipts', t.receipts || 0, 'Signed proof records available for audit']
1028
+ ].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('');
1029
+ renderTools(data.tools || []);
1030
+ renderApprovals(data.pending_approvals || []);
1031
+ renderRecent(data.recent || []);
1032
+ renderChains(data.receipt_chains || []);
1033
+ renderPolicyPacks(data.policy_packs || {});
1034
+ renderConnectorPilots(data.connector_pilots || {});
1035
+ renderRegistry(data.registry || {});
1036
+ 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>';
1037
+ }
1038
+ function renderTools(tools){
1039
+ 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>' +
1040
+ tools.map(function(t){
1041
+ var cov = t.policy_coverage || { status:'none', label:'No rule' };
1042
+ var s = t.suggestion || { action:'Observe', reason:'' };
1043
+ return '<tr><td><span class="pill '+t.risk+'">'+escapeHtml(t.risk)+'</span></td>'+
1044
+ '<td><strong>'+escapeHtml(t.tool)+'</strong><br><span class="muted">'+escapeHtml((t.reasons || []).join(', '))+'</span></td>'+
1045
+ '<td><span class="pill '+escapeHtml(cov.status)+'">'+escapeHtml(cov.label)+'</span><br><span class="muted mono">'+escapeHtml(JSON.stringify(cov.policy || {}))+'</span></td>'+
1046
+ '<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>'+
1047
+ '<td><strong>'+escapeHtml(s.action)+'</strong><br><span class="muted">'+escapeHtml(s.reason)+'</span></td>'+
1048
+ '<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>';
1049
+ }).join('') + (tools.length ? '' : '<tr><td colspan="6" class="muted">No tool calls yet. Wrap an MCP server or run the demo.</td></tr>') + '</tbody>';
1050
+ }
1051
+ function renderApprovals(rows){
1052
+ document.getElementById('approvals').innerHTML = rows.map(function(r){
1053
+ var rb = r.action_readback || {};
1054
+ var id = r.request_id || '';
1055
+ return '<div class="approval"><div class="kicker">Pending exact-action review</div><h3>'+escapeHtml(rb.summary || r.tool || 'Unknown action')+'</h3>'+
1056
+ '<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>'+
1057
+ '<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>'+
1058
+ '<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>';
1059
+ }).join('') || '<p class="muted">No approval-required calls in the local log yet.</p>';
1060
+ }
1061
+ function renderRecent(rows){
1062
+ document.getElementById('recent').innerHTML = rows.slice(0,20).map(function(r){
1063
+ var rb = r.action_readback || {};
1064
+ 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>';
1065
+ }).join('') || '<p class="muted">No decisions yet.</p>';
1066
+ }
1067
+ function renderChains(rows){
1068
+ document.getElementById('chains').innerHTML = rows.slice(0,12).map(function(c){
1069
+ var hashes = (c.receipts || []).map(function(r){ return '<span class="mono">'+escapeHtml((r.hash || '').slice(0,16))+'...</span>'; }).join('<br>');
1070
+ 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>';
1071
+ }).join('') || '<p class="muted">No receipt chains yet.</p>';
1072
+ }
1073
+ function renderPolicyPacks(info){
1074
+ var available = info.available || [];
1075
+ var installed = new Set(info.installed || []);
1076
+ document.getElementById('policy-packs').innerHTML = available.map(function(pack){
1077
+ var isInstalled = installed.has(pack.id);
1078
+ var files = (pack.files || []).map(function(f){ return f.path; }).join(', ');
1079
+ return '<div class="pack '+(isInstalled ? 'installed' : '')+'">'+
1080
+ '<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>'+
1081
+ '<p>'+escapeHtml(pack.description || '')+'</p>'+
1082
+ '<small>Writes to <code>'+escapeHtml((info.directory || './cedar') + '/' + files)+'</code></small>'+
1083
+ '<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>'+
1084
+ '<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>'+
1085
+ '</div>';
1086
+ }).join('') || '<p class="muted">No policy packs bundled.</p>';
1087
+ }
1088
+ function renderConnectorPilots(info){
1089
+ var available = info.available || [];
1090
+ var installed = new Set((info.installed || []).map(function(row){ return row.id; }));
1091
+ var doctor = {};
1092
+ (info.doctor || []).forEach(function(row){ doctor[row.id] = row; });
1093
+ document.getElementById('connector-pilots').innerHTML = available.map(function(pilot){
1094
+ var row = doctor[pilot.id] || {};
1095
+ var isInstalled = installed.has(pilot.id);
1096
+ var status = row.installed ? (row.usable ? 'installed' : 'needs env') : 'not installed';
1097
+ var statusClass = row.installed ? (row.usable ? 'exact' : 'wildcard') : 'none';
1098
+ var missing = (row.missing_required || []).join(', ');
1099
+ var tools = (pilot.tools || []).slice(0,4).map(function(tool){ return '<code>'+escapeHtml(tool)+'</code>'; }).join(' ');
1100
+ return '<div class="pack '+(isInstalled ? 'installed' : '')+'">'+
1101
+ '<div style="display:flex;gap:8px;align-items:center"><strong>'+escapeHtml(pilot.name)+'</strong><span class="pill '+statusClass+'">'+escapeHtml(status)+'</span></div>'+
1102
+ '<p>'+escapeHtml(pilot.description || '')+'</p>'+
1103
+ '<small>'+escapeHtml(pilot.value || '')+'</small>'+
1104
+ '<div>'+tools+'</div>'+
1105
+ (missing ? '<small>Missing: <code>'+escapeHtml(missing)+'</code></small>' : '<small>'+escapeHtml(row.next || 'Ready for shadow-mode review.')+'</small>')+
1106
+ '<details><summary>Details</summary><pre>'+escapeHtml(JSON.stringify({ setup: pilot.setup, actions: pilot.actions, doctor: row }, null, 2))+'</pre></details>'+
1107
+ '<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>'+
1108
+ '</div>';
1109
+ }).join('') || '<p class="muted">No connector pilots bundled.</p>';
1110
+ }
1111
+ function renderRegistry(reg){
1112
+ var boundaryClass = reg.hosted ? 'exact' : reg.registry_exists ? 'wildcard' : 'none';
1113
+ document.getElementById('registry').innerHTML =
1114
+ '<div class="chain-item"><span class="pill '+boundaryClass+'">'+escapeHtml(reg.boundary || 'not configured')+'</span>'+
1115
+ '<p style="margin-top:8px"><strong>'+escapeHtml(reg.org_name || 'No org identity yet')+'</strong></p>'+
1116
+ '<p class="muted">Digests: '+(reg.records || 0)+' \xB7 Anchors: '+(reg.anchors || 0)+'</p>'+
1117
+ '<p class="muted mono" style="margin-top:6px">'+escapeHtml(reg.registry_path || '')+'</p></div>'+
1118
+ '<div class="field"><label>Org name</label><input id="registry-org" placeholder="Meridian Global Macro" value="'+escapeHtml(reg.org_name || '')+'"></div>'+
1119
+ '<div class="field"><label>Hosted token (optional, not stored)</label><input id="registry-token" type="password" placeholder="SCOPEBLIND_TOKEN for hosted digest anchoring"></div>'+
1120
+ '<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>'+
1121
+ '<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>';
1122
+ }
1123
+ async function setPolicy(tool, action){
1124
+ var res = await fetch('/api/tool-policy', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ tool: tool, action: action }) });
1125
+ if(!res.ok){ toast('Policy update failed'); return; }
1126
+ toast('Policy updated: '+tool+' -> '+action+'. Restart the wrapper to apply.');
1127
+ await refresh();
1128
+ }
1129
+ async function resolveApproval(requestId, tool, resolution){
1130
+ var reasonEl = document.getElementById('reason-'+escapeAttr(requestId));
1131
+ var reason = reasonEl ? reasonEl.value : '';
1132
+ var edited = undefined;
1133
+ if(resolution === 'edit'){
1134
+ var raw = prompt('Paste edited JSON payload. This records the edit instruction; rerun the tool with the edited payload.');
1135
+ if(raw){ try { edited = JSON.parse(raw); } catch(e){ toast('Edit payload is not valid JSON'); return; } }
1136
+ }
1137
+ 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 }) });
1138
+ var body = await res.json().catch(function(){ return {}; });
1139
+ toast(resolution+' recorded'+(body.forwarded && body.forwarded.ok ? ' and forwarded to live gateway' : ' locally'));
1140
+ }
1141
+ async function installPack(pack){
1142
+ var res = await fetch('/api/policy-packs/install', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ pack: pack, force: true }) });
1143
+ var body = await res.json().catch(function(){ return {}; });
1144
+ if(!res.ok){ toast(body.error || 'Policy pack install failed'); return; }
1145
+ toast('Installed '+pack+' into '+body.dir);
1146
+ await refresh();
1147
+ }
1148
+ async function installConnector(pilot){
1149
+ var res = await fetch('/api/connectors/install', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ pilot: pilot, force: true }) });
1150
+ var body = await res.json().catch(function(){ return {}; });
1151
+ if(!res.ok){ toast(body.error || 'Connector install failed'); return; }
1152
+ toast('Installed '+pilot+' into '+body.dir);
1153
+ await refresh();
1154
+ }
1155
+ function togglePackPreview(pack){
1156
+ var el = document.getElementById('pack-preview-'+escapeAttr(pack));
1157
+ if(el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
1158
+ }
1159
+ async function anchorRegistry(mode){
1160
+ var org = document.getElementById('registry-org') ? document.getElementById('registry-org').value : '';
1161
+ var token = document.getElementById('registry-token') ? document.getElementById('registry-token').value : '';
1162
+ var body = { org_name: org, hosted: mode === 'hosted', token: token };
1163
+ var res = await fetch('/api/registry/anchor', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body) });
1164
+ var out = await res.json().catch(function(){ return {}; });
1165
+ if(!res.ok){ toast(out.error || 'Registry anchor failed'); return; }
1166
+ toast((out.uploaded ? 'Hosted anchor complete' : 'Local registry preview written') + ': ' + out.records + ' digest(s)');
1167
+ await refresh();
1168
+ }
1169
+ async function exportBundle(){
1170
+ var res = await fetch('/api/audit-bundle');
1171
+ if(!res.ok){
1172
+ var err = await res.json().catch(function(){ return {}; });
1173
+ toast(err.message || 'Audit bundle export requires signed receipts');
1174
+ return;
1175
+ }
1176
+ var blob = await res.blob();
1177
+ var url = URL.createObjectURL(blob);
1178
+ var a = document.createElement('a');
1179
+ a.href = url;
1180
+ a.download = 'protect-mcp-audit-bundle.json';
1181
+ document.body.appendChild(a);
1182
+ a.click();
1183
+ a.remove();
1184
+ setTimeout(function(){ URL.revokeObjectURL(url); }, 1000);
1185
+ }
1186
+ document.addEventListener('click', function(ev){
1187
+ 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;
1188
+ if(!target) return;
1189
+ var connectorInstall = target.getAttribute('data-connector-install');
1190
+ if(connectorInstall){ installConnector(connectorInstall); return; }
1191
+ if(target.getAttribute('data-connector-doctor')){ toast('Connector doctor refreshed. Missing secrets are shown as names only, never values.'); refresh(); return; }
1192
+ var packInstall = target.getAttribute('data-pack-install');
1193
+ if(packInstall){ installPack(packInstall); return; }
1194
+ var packPreview = target.getAttribute('data-pack-preview');
1195
+ if(packPreview){ togglePackPreview(packPreview); return; }
1196
+ var anchorMode = target.getAttribute('data-registry-anchor');
1197
+ if(anchorMode){ anchorRegistry(anchorMode); return; }
1198
+ var policyTool = target.getAttribute('data-policy-tool');
1199
+ if(policyTool){
1200
+ setPolicy(policyTool, target.getAttribute('data-policy-action') || 'require_approval');
1201
+ return;
1202
+ }
1203
+ var approvalId = target.getAttribute('data-approval-id');
1204
+ if(approvalId){
1205
+ resolveApproval(approvalId, target.getAttribute('data-approval-tool') || '', target.getAttribute('data-approval-resolution') || 'deny');
1206
+ }
1207
+ });
1208
+ function toast(msg){ var el=document.getElementById('toast'); el.textContent=msg; el.style.display='block'; setTimeout(function(){el.style.display='none';}, 4200); }
1209
+ function escapeHtml(v){ return String(v == null ? '' : v).replace(/[&<>"']/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]; }); }
1210
+ function escapeAttr(v){ return String(v || '').replace(/[^A-Za-z0-9_-]/g,'_'); }
1211
+ refresh();
1212
+ </script>
1213
+ </body>
1214
+ </html>`;
1215
+ }
1216
+ async function handleDashboard(argv) {
1217
+ const { createServer } = await import("http");
1218
+ const { execFile } = await import("child_process");
1219
+ const { resolve } = await import("path");
1220
+ const port = commandNeedsValue(argv, "--port") ? parseInt(flagValue(argv, "--port") || "9877", 10) : 9877;
1221
+ const dir = resolve(commandNeedsValue(argv, "--dir") ? flagValue(argv, "--dir") || process.cwd() : process.cwd());
1222
+ const policyPath = resolve(flagValue(argv, "--policy") || joinCli(dir, "protect-mcp.json"));
1223
+ const approvalEndpoint = flagValue(argv, "--approval-endpoint");
1224
+ const approvalNonce = flagValue(argv, "--approval-nonce");
1225
+ const open = argv.includes("--open");
1226
+ const server = createServer((req, res) => {
1227
+ void (async () => {
1228
+ try {
1229
+ const url2 = new URL(req.url || "/", "http://127.0.0.1");
1230
+ if (url2.pathname === "/api/summary") {
1231
+ const body = JSON.stringify(buildDashboardSummary(dir, policyPath), null, 2);
1232
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
1233
+ res.end(body);
1234
+ return;
1235
+ }
1236
+ if (url2.pathname === "/api/tool-policy" && req.method === "POST") {
1237
+ const body = await readJsonBody(req);
1238
+ const tool = typeof body.tool === "string" ? body.tool : "";
1239
+ const action = body.action === "block" || body.action === "observe" ? body.action : "require_approval";
1240
+ if (!tool) {
1241
+ res.writeHead(400, { "content-type": "application/json" });
1242
+ res.end(JSON.stringify({ error: "missing_tool" }));
1243
+ return;
1244
+ }
1245
+ const policy = writeToolPolicy(policyPath, tool, action);
1246
+ res.writeHead(200, { "content-type": "application/json" });
1247
+ res.end(JSON.stringify({ ok: true, policy_path: policyPath, policy }));
1248
+ return;
1249
+ }
1250
+ if (url2.pathname === "/api/policy-packs/install" && req.method === "POST") {
1251
+ const body = await readJsonBody(req);
1252
+ const pack = typeof body.pack === "string" ? body.pack : "";
1253
+ if (!pack) {
1254
+ res.writeHead(400, { "content-type": "application/json" });
1255
+ res.end(JSON.stringify({ error: "missing_policy_pack" }));
1256
+ return;
1257
+ }
1258
+ const installed = installPolicyPackToDir(dir, pack, Boolean(body.force));
1259
+ res.writeHead(200, { "content-type": "application/json" });
1260
+ res.end(JSON.stringify({
1261
+ ok: true,
1262
+ ...installed,
1263
+ installed: installedPolicyPackIds(dir)
1264
+ }));
1265
+ return;
1266
+ }
1267
+ if (url2.pathname === "/api/connectors/install" && req.method === "POST") {
1268
+ const body = await readJsonBody(req);
1269
+ const pilot = typeof body.pilot === "string" ? body.pilot : "";
1270
+ if (!pilot) {
1271
+ res.writeHead(400, { "content-type": "application/json" });
1272
+ res.end(JSON.stringify({ error: "missing_connector_pilot" }));
1273
+ return;
1274
+ }
1275
+ const installed = writeConnectorPilots({ dir, ids: [pilot], force: Boolean(body.force) });
1276
+ res.writeHead(200, { "content-type": "application/json" });
1277
+ res.end(JSON.stringify({
1278
+ ok: true,
1279
+ dir: installed.directory,
1280
+ written: installed.written,
1281
+ installed: readInstalledConnectorPilots(dir),
1282
+ doctor: connectorDoctor(dir)
1283
+ }));
1284
+ return;
1285
+ }
1286
+ if (url2.pathname === "/api/registry/anchor" && req.method === "POST") {
1287
+ const body = await readJsonBody(req);
1288
+ const { createReceiptRegistry } = await import("./receipt-registry-6CAOY6RP.mjs");
1289
+ try {
1290
+ const hosted = Boolean(body.hosted);
1291
+ const result = await createReceiptRegistry({
1292
+ dir,
1293
+ orgName: typeof body.org_name === "string" && body.org_name.trim() ? body.org_name.trim() : void 0,
1294
+ orgId: typeof body.org_id === "string" && body.org_id.trim() ? body.org_id.trim() : void 0,
1295
+ billingAccountId: typeof body.billing_account_id === "string" && body.billing_account_id.trim() ? body.billing_account_id.trim() : void 0,
1296
+ hosted,
1297
+ token: typeof body.token === "string" && body.token.trim() ? body.token.trim() : process.env.SCOPEBLIND_TOKEN,
1298
+ endpoint: typeof body.endpoint === "string" && body.endpoint.trim() ? body.endpoint.trim() : hosted ? process.env.SCOPEBLIND_REGISTRY_ENDPOINT || "https://api.scopeblind.com" : void 0,
1299
+ verifierBaseUrl: typeof body.verifier_base === "string" && body.verifier_base.trim() ? body.verifier_base.trim() : process.env.SCOPEBLIND_VERIFIER_BASE || "https://legate.scopeblind.com"
1300
+ });
1301
+ res.writeHead(200, { "content-type": "application/json" });
1302
+ res.end(JSON.stringify({
1303
+ ok: true,
1304
+ uploaded: result.uploaded,
1305
+ records: result.registry.records.length,
1306
+ anchors: result.registry.anchors.length,
1307
+ registry_path: result.registryPath,
1308
+ verifier_path: result.verifierPath,
1309
+ registry: dashboardRegistryStatus(dir)
1310
+ }));
1311
+ } catch (err) {
1312
+ res.writeHead(409, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
1313
+ res.end(JSON.stringify({
1314
+ error: "registry_anchor_unavailable",
1315
+ message: err instanceof Error ? err.message : String(err),
1316
+ next_step: "Run protect-mcp with signing enabled so decisions are written as signed receipts, then try again."
1317
+ }));
1318
+ }
1319
+ return;
1320
+ }
1321
+ if (url2.pathname === "/api/approval/resolve" && req.method === "POST") {
1322
+ const body = await readJsonBody(req);
1323
+ const result = await recordApprovalResolution({ dir, approvalEndpoint, approvalNonce, body });
1324
+ res.writeHead(200, { "content-type": "application/json" });
1325
+ res.end(JSON.stringify(result));
1326
+ return;
1327
+ }
1328
+ if (url2.pathname === "/api/audit-bundle") {
1329
+ let bundle;
1330
+ try {
1331
+ bundle = await buildAuditBundleForDir(dir);
1332
+ } catch (err) {
1333
+ res.writeHead(409, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
1334
+ res.end(JSON.stringify({
1335
+ error: "audit_bundle_unavailable",
1336
+ message: err instanceof Error ? err.message : String(err),
1337
+ next_step: "Run protect-mcp with signing enabled so decisions are written as signed receipts, then export again."
1338
+ }));
1339
+ return;
1340
+ }
1341
+ res.writeHead(200, {
1342
+ "content-type": "application/json; charset=utf-8",
1343
+ "content-disposition": 'attachment; filename="protect-mcp-audit-bundle.json"',
1344
+ "cache-control": "no-store"
1345
+ });
1346
+ res.end(JSON.stringify(bundle, null, 2) + "\n");
1347
+ return;
1348
+ }
1349
+ res.writeHead(200, {
1350
+ "content-type": "text/html; charset=utf-8",
1351
+ "cache-control": "no-store",
1352
+ "content-security-policy": "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'"
1353
+ });
1354
+ res.end(dashboardHtml());
1355
+ } catch (err) {
1356
+ res.writeHead(500, { "content-type": "application/json" });
1357
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1358
+ }
1359
+ })();
1360
+ });
1361
+ await new Promise((resolveListen, rejectListen) => {
1362
+ server.once("error", rejectListen);
1363
+ server.listen(port, "127.0.0.1", () => resolveListen());
1364
+ });
1365
+ const url = `http://127.0.0.1:${port}`;
1366
+ process.stderr.write(`
1367
+ ${bold("protect-mcp dashboard")}
1368
+
1369
+ `);
1370
+ process.stderr.write(` Local URL: ${url}
1371
+ `);
1372
+ process.stderr.write(` Reading: ${dir}
1373
+ `);
1374
+ process.stderr.write(` Policy: ${policyPath}
1375
+ `);
1376
+ process.stderr.write(` Network: 127.0.0.1 only; no uploads
1377
+
1378
+ `);
1379
+ if (open) {
1380
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
1381
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1382
+ execFile(opener, args, () => {
1383
+ });
1384
+ }
1385
+ }
1386
+ function draftPolicyFromSummary(summary) {
1387
+ const files = summary.files || {};
1388
+ const rows = Array.isArray(summary.tools) ? summary.tools : [];
1389
+ const tools = {
1390
+ "*": { rate_limit: "100/hour" }
1391
+ };
1392
+ for (const row of rows) {
1393
+ if (!row.tool || row.tool === "unknown") continue;
1394
+ const suggestion = row.suggestion || suggestedGuardrailFor(row.tool, row.risk || "low", row.reasons || []);
1395
+ tools[row.tool] = suggestion.policy;
1396
+ }
1397
+ return {
1398
+ tools,
1399
+ default_tier: "unknown",
1400
+ signing: files.key_exists ? {
1401
+ key_path: "./keys/gateway.json",
1402
+ issuer: "protect-mcp"
1403
+ } : void 0,
1404
+ notes: [
1405
+ "Generated from local shadow-mode inventory.",
1406
+ "Review before running with --enforce.",
1407
+ "High-risk transaction, deployment, external-send, and database tools require approval.",
1408
+ "Destructive and secret-handling tools are blocked by default."
1409
+ ]
1410
+ };
1411
+ }
1412
+ function writeToolPolicy(policyPath, tool, action) {
1413
+ const existing = loadPolicyJson(policyPath) || { tools: {}, default_tier: "unknown" };
1414
+ const tools = existing.tools && typeof existing.tools === "object" ? { ...existing.tools } : {};
1415
+ if (action === "require_approval") {
1416
+ tools[tool] = { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" };
1417
+ } else if (action === "block") {
1418
+ tools[tool] = { block: true, min_tier: "privileged" };
1419
+ } else {
1420
+ tools[tool] = { rate_limit: "100/hour" };
1421
+ }
1422
+ const next = {
1423
+ ...existing,
1424
+ tools,
1425
+ default_tier: existing.default_tier || "unknown"
1426
+ };
1427
+ writeFileSyncCli(policyPath, JSON.stringify(next, null, 2) + "\n");
1428
+ return next;
1429
+ }
1430
+ function policyPackDirectory(dir) {
1431
+ return joinCli(dir, "cedar");
1432
+ }
1433
+ function installedPolicyPackIds(dir) {
1434
+ const cedarDir = policyPackDirectory(dir);
1435
+ return POLICY_PACKS.filter(
1436
+ (pack) => pack.files.every((file) => existsSyncCli(joinCli(cedarDir, file.path)))
1437
+ ).map((pack) => pack.id);
1438
+ }
1439
+ function installPolicyPackToDir(dir, packId, force = false) {
1440
+ const packs = packId === "all" ? POLICY_PACKS : [getPolicyPack(packId)].filter(Boolean);
1441
+ if (packs.length === 0) throw new Error(`Unknown policy pack: ${packId}`);
1442
+ const outDir = policyPackDirectory(dir);
1443
+ mkdirSyncCli(outDir, { recursive: true });
1444
+ const written = [];
1445
+ for (const pack of packs) {
1446
+ for (const file of pack.files) {
1447
+ const outPath = joinCli(outDir, file.path);
1448
+ if (existsSyncCli(outPath) && !force) {
1449
+ throw new Error(`Refusing to overwrite ${outPath}. Pass force=true if intentional.`);
1450
+ }
1451
+ mkdirSyncCli(dirnameCli(outPath), { recursive: true });
1452
+ writeFileSyncCli(outPath, file.contents.endsWith("\n") ? file.contents : `${file.contents}
1453
+ `);
1454
+ written.push(outPath);
1455
+ }
1456
+ }
1457
+ return { dir: outDir, written, packs: packs.map((pack) => pack.id) };
1458
+ }
1459
+ function dashboardRegistryStatus(dir) {
1460
+ const identityPath = joinCli(dir, ".protect-mcp-org.json");
1461
+ const registryPath = joinCli(dir, ".protect-mcp-registry.json");
1462
+ const verifierPath = joinCli(dir, "scopeblind-verifier.html");
1463
+ const identity = existsSyncCli(identityPath) ? (() => {
1464
+ try {
1465
+ return JSON.parse(readFileSyncCli(identityPath, "utf-8"));
1466
+ } catch {
1467
+ return null;
1468
+ }
1469
+ })() : null;
1470
+ const registry = existsSyncCli(registryPath) ? (() => {
1471
+ try {
1472
+ return JSON.parse(readFileSyncCli(registryPath, "utf-8"));
1473
+ } catch {
1474
+ return null;
1475
+ }
1476
+ })() : null;
1477
+ const anchors = Array.isArray(registry?.anchors) ? registry.anchors : [];
1478
+ const hosted = anchors.some((anchor) => anchor.timestamp_source === "scopeblind-hosted");
1479
+ return {
1480
+ identity_exists: existsSyncCli(identityPath),
1481
+ registry_exists: existsSyncCli(registryPath),
1482
+ verifier_exists: existsSyncCli(verifierPath),
1483
+ identity_path: identityPath,
1484
+ registry_path: registryPath,
1485
+ verifier_path: verifierPath,
1486
+ org_name: identity?.org_name || (registry?.org && typeof registry.org === "object" ? registry.org.org_name : null),
1487
+ org_id: identity?.org_id || (registry?.org && typeof registry.org === "object" ? registry.org.org_id : null),
1488
+ billing_account_id: identity?.billing_account_id || (registry?.billing && typeof registry.billing === "object" ? registry.billing.billing_account_id : null),
1489
+ records: Array.isArray(registry?.records) ? registry.records.length : 0,
1490
+ anchors: anchors.length,
1491
+ hosted,
1492
+ boundary: hosted ? "hosted digest anchor" : registry ? "local preview only" : "not configured"
1493
+ };
1494
+ }
1495
+ async function readJsonBody(req) {
1496
+ const chunks = [];
1497
+ for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1498
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
1499
+ return raw ? JSON.parse(raw) : {};
1500
+ }
1501
+ async function buildAuditBundleForDir(dir) {
1502
+ const { createAuditBundle } = await import("./bundle-YUUHAG7O.mjs");
1503
+ const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
1504
+ const keyPath = joinCli(dir, "keys", "gateway.json");
1505
+ if (!existsSyncCli(receiptPath)) throw new Error("No receipt file found.");
1506
+ if (!existsSyncCli(keyPath)) throw new Error("No signing key found.");
1507
+ const receipts = parseJsonlFile(receiptPath);
1508
+ if (receipts.length === 0) throw new Error("No signed receipts found.");
1509
+ const keyData = JSON.parse(readFileSyncCli(keyPath, "utf-8"));
1510
+ return createAuditBundle({
1511
+ tenant: keyData.issuer || "protect-mcp",
1512
+ receipts,
1513
+ selectiveDisclosures: collectSelectiveDisclosurePackages(dir),
1514
+ signingKeys: [{
1515
+ kty: "OKP",
1516
+ crv: "Ed25519",
1517
+ kid: keyData.kid || "unknown",
1518
+ x: Buffer.from(keyData.publicKey || "", "hex").toString("base64url"),
1519
+ use: "sig"
1520
+ }]
1521
+ });
1522
+ }
1523
+ function collectSelectiveDisclosurePackages(dir) {
1524
+ const out = [];
1525
+ const seen = /* @__PURE__ */ new Set();
1526
+ const candidates = [];
1527
+ const receiptsDir = joinCli(dir, "receipts");
1528
+ if (existsSyncCli(receiptsDir)) {
1529
+ for (const name of readdirSyncCli(receiptsDir)) {
1530
+ if (name.includes("selective-disclosure") && name.endsWith(".json")) {
1531
+ candidates.push(joinCli(receiptsDir, name));
1532
+ }
1533
+ }
1534
+ }
1535
+ const jsonlPath = joinCli(dir, ".protect-mcp-selective-disclosures.jsonl");
1536
+ if (existsSyncCli(jsonlPath)) {
1537
+ for (const line of readFileSyncCli(jsonlPath, "utf-8").split("\n").map((s) => s.trim()).filter(Boolean)) {
1538
+ try {
1539
+ const parsed = JSON.parse(line);
1540
+ addSelectiveDisclosure(out, seen, parsed);
1541
+ } catch {
1542
+ }
1543
+ }
1544
+ }
1545
+ for (const path of candidates) {
1546
+ try {
1547
+ const parsed = JSON.parse(readFileSyncCli(path, "utf-8"));
1548
+ addSelectiveDisclosure(out, seen, parsed);
1549
+ } catch {
1550
+ }
1551
+ }
1552
+ return out;
1553
+ }
1554
+ function addSelectiveDisclosure(out, seen, parsed) {
1555
+ if (parsed?.type !== "scopeblind.selective_disclosure.v0") return;
1556
+ const key = [
1557
+ parsed.parent_receipt_hash || "",
1558
+ Array.isArray(parsed.disclosed_fields) ? parsed.disclosed_fields.slice().sort().join(",") : "",
1559
+ Array.isArray(parsed.hidden_fields) ? parsed.hidden_fields.slice().sort().join(",") : ""
1560
+ ].join("|");
1561
+ if (seen.has(key)) return;
1562
+ seen.add(key);
1563
+ out.push(parsed);
1564
+ }
1565
+ async function recordApprovalResolution(opts) {
1566
+ const resolution = String(opts.body.resolution || "deny");
1567
+ const requestId = String(opts.body.request_id || "");
1568
+ const tool = String(opts.body.tool || "unknown");
1569
+ const record = {
1570
+ type: "scopeblind.approval_resolution.v1",
1571
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1572
+ request_id: requestId,
1573
+ tool,
1574
+ resolution,
1575
+ reason: typeof opts.body.reason === "string" ? opts.body.reason.slice(0, 1e3) : "",
1576
+ edited_payload: opts.body.edited_payload || void 0,
1577
+ takeover_note: opts.body.takeover_note || void 0,
1578
+ payload_hash: opts.body.payload_hash || void 0
1579
+ };
1580
+ appendFileSyncCli(joinCli(opts.dir, ".protect-mcp-approval-resolutions.jsonl"), JSON.stringify(record) + "\n");
1581
+ let forwarded = null;
1582
+ if (resolution === "approve" && opts.approvalEndpoint && opts.approvalNonce) {
1583
+ const endpoint = opts.approvalEndpoint.replace(/\/$/, "") + "/approve";
1584
+ const response = await fetch(endpoint, {
1585
+ method: "POST",
1586
+ headers: { "content-type": "application/json" },
1587
+ body: JSON.stringify({
1588
+ request_id: requestId,
1589
+ tool,
1590
+ mode: "once",
1591
+ nonce: opts.approvalNonce
1592
+ })
1593
+ });
1594
+ forwarded = {
1595
+ ok: response.ok,
1596
+ status: response.status,
1597
+ body: await response.text().catch(() => "")
1598
+ };
1599
+ }
1600
+ return { recorded: true, resolution: record, forwarded };
1601
+ }
1602
+ async function handleRecommend(argv) {
1603
+ const { writeFileSync } = await import("fs");
1604
+ const { resolve } = await import("path");
1605
+ const dir = resolve(commandNeedsValue(argv, "--dir") ? flagValue(argv, "--dir") || process.cwd() : process.cwd());
1606
+ const outputPath = resolve(flagValue(argv, "--output") || "protect-mcp.recommended.json");
1607
+ const write = argv.includes("--write");
1608
+ const summary = buildDashboardSummary(dir);
1609
+ const totals = summary.totals;
1610
+ const policy = draftPolicyFromSummary(summary);
1611
+ const rows = Array.isArray(summary.tools) ? summary.tools : [];
1612
+ process.stdout.write(`
1613
+ ${bold("protect-mcp recommend")}
1614
+
1615
+ `);
1616
+ process.stdout.write(` Source: ${dir}
1617
+ `);
1618
+ process.stdout.write(` Decisions: ${totals.decisions || 0}
1619
+ `);
1620
+ process.stdout.write(` Tools: ${totals.tools || 0}
1621
+
1622
+ `);
1623
+ if (rows.length === 0) {
1624
+ process.stdout.write(`No tool calls found yet. First run:
1625
+
1626
+ `);
1627
+ process.stdout.write(` ${dim("npx protect-mcp wrap -- node your-mcp-server.js")}
1628
+ `);
1629
+ process.stdout.write(` ${dim("npx protect-mcp dashboard --open")}
1630
+
1631
+ `);
1632
+ return;
1633
+ }
1634
+ for (const row of rows) {
1635
+ const suggestion = row.suggestion || suggestedGuardrailFor(row.tool, row.risk, row.reasons);
1636
+ process.stdout.write(` - ${row.tool}: ${bold(suggestion.action)} (${row.risk})
1637
+ `);
1638
+ process.stdout.write(` ${dim(suggestion.reason)}
1639
+ `);
1640
+ }
1641
+ const body = JSON.stringify(policy, null, 2) + "\n";
1642
+ if (!write) {
1643
+ process.stdout.write(`
1644
+ Dry run only. Write the policy with:
1645
+ `);
1646
+ process.stdout.write(` ${dim("npx protect-mcp recommend --write")}
1647
+
1648
+ `);
1649
+ process.stdout.write(dim(body));
1650
+ return;
1651
+ }
1652
+ writeFileSync(outputPath, body);
1653
+ process.stdout.write(`
1654
+ ${green("\u2713 Wrote recommended policy")}
1655
+ `);
1656
+ process.stdout.write(` Output: ${outputPath}
1657
+ `);
1658
+ process.stdout.write(` Review it, then restart your wrapper with:
1659
+ `);
1660
+ process.stdout.write(` ${dim(shellCommand("npx", ["protect-mcp", "--policy", outputPath, "--enforce", "--", "node", "your-mcp-server.js"]))}
1661
+
1662
+ `);
1663
+ }
1664
+ async function handleWrap(argv) {
1665
+ const { existsSync, readFileSync, writeFileSync } = await import("fs");
1666
+ const { resolve } = await import("path");
1667
+ const configFlag = flagValue(argv, "--config");
1668
+ const cedarFlag = flagValue(argv, "--cedar");
1669
+ const enforce = argv.includes("--enforce");
1670
+ const write = argv.includes("--write");
1671
+ const claudeDesktop = argv.includes("--claude-desktop") || argv.includes("--claude");
1672
+ const serverName = flagValue(argv, "--server");
1673
+ const separator = argv.indexOf("--");
1674
+ const childCommand = separator >= 0 ? argv.slice(separator + 1).filter(Boolean) : [];
1675
+ const configPath = cedarFlag ? void 0 : resolve(configFlag || await ensureLocalConfig(process.cwd()));
1676
+ const cedarDir = cedarFlag ? resolve(cedarFlag) : void 0;
1677
+ if (childCommand.length > 0) {
1678
+ const args = wrapperArgsFor(childCommand, { configPath, cedarDir, enforce });
1679
+ process.stdout.write(`
1680
+ ${bold("protect-mcp wrap")}
1681
+
1682
+ `);
1683
+ process.stdout.write(`Use this command in your MCP client config:
1684
+
1685
+ `);
1686
+ process.stdout.write(` ${shellCommand("npx", args)}
1687
+
1688
+ `);
1689
+ process.stdout.write(`Claude Desktop JSON snippet:
1690
+
1691
+ `);
1692
+ process.stdout.write(dim(JSON.stringify({
1693
+ command: "npx",
1694
+ args
1695
+ }, null, 2)) + "\n\n");
1696
+ process.stdout.write(`Then inspect calls with: ${dim("npx protect-mcp dashboard --open")}
1697
+
1698
+ `);
1699
+ return;
1700
+ }
1701
+ const claudePath = resolve(flagValue(argv, "--path") || claudeDesktopConfigPath());
1702
+ if (!claudeDesktop && !existsSync(claudePath)) {
1703
+ process.stdout.write(`
1704
+ ${bold("protect-mcp wrap")}
1705
+
1706
+ `);
1707
+ process.stdout.write(`No command was passed after "--" and no Claude Desktop config was found.
1708
+
1709
+ `);
1710
+ process.stdout.write(`Examples:
1711
+ `);
1712
+ process.stdout.write(` ${dim("npx protect-mcp wrap -- node server.js")}
1713
+ `);
1714
+ process.stdout.write(` ${dim("npx protect-mcp wrap --claude-desktop --write")}
1715
+
1716
+ `);
1717
+ return;
1718
+ }
1719
+ if (!existsSync(claudePath)) {
1720
+ process.stderr.write(`protect-mcp wrap: Claude Desktop config not found at ${claudePath}
1721
+ `);
1722
+ process.exit(1);
1723
+ }
1724
+ let parsed;
1725
+ try {
1726
+ parsed = JSON.parse(readFileSync(claudePath, "utf-8"));
1727
+ } catch (err) {
1728
+ process.stderr.write(`protect-mcp wrap: could not parse ${claudePath}: ${err instanceof Error ? err.message : err}
1729
+ `);
1730
+ process.exit(1);
1731
+ }
1732
+ const servers = parsed.mcpServers || {};
1733
+ const names = Object.keys(servers).filter((name) => !serverName || name === serverName);
1734
+ if (names.length === 0) {
1735
+ process.stderr.write(`protect-mcp wrap: no MCP servers found${serverName ? ` matching "${serverName}"` : ""}.
1736
+ `);
1737
+ process.exit(1);
1738
+ }
1739
+ const next = { ...parsed, mcpServers: { ...servers } };
1740
+ const changes = [];
1741
+ for (const name of names) {
1742
+ const before = servers[name] || {};
1743
+ const originalCommand = before.command;
1744
+ const originalArgs = Array.isArray(before.args) ? before.args : [];
1745
+ if (!originalCommand) {
1746
+ changes.push({ name, before, after: before, skipped: "missing command" });
1747
+ continue;
1748
+ }
1749
+ if (originalCommand === "npx" && originalArgs.some((arg) => String(arg).includes("protect-mcp"))) {
1750
+ changes.push({ name, before, after: before, skipped: "already wrapped" });
1751
+ continue;
1752
+ }
1753
+ const wrappedArgs = wrapperArgsFor([originalCommand, ...originalArgs], { configPath, cedarDir, enforce });
1754
+ const after = { ...before, command: "npx", args: wrappedArgs };
1755
+ next.mcpServers[name] = after;
1756
+ changes.push({ name, before, after });
1757
+ }
1758
+ process.stdout.write(`
1759
+ ${bold("protect-mcp wrap: Claude Desktop")}
1760
+
1761
+ `);
1762
+ process.stdout.write(`Config: ${claudePath}
1763
+ `);
1764
+ process.stdout.write(`Mode: ${enforce ? "enforce" : "shadow"}
1765
+
1766
+ `);
1767
+ for (const change of changes) {
1768
+ if (change.skipped) {
1769
+ process.stdout.write(` - ${change.name}: ${yellow(change.skipped)}
1770
+ `);
1771
+ } else {
1772
+ process.stdout.write(` - ${change.name}: ${green("will wrap")}
1773
+ `);
1774
+ process.stdout.write(` ${dim(`${change.before.command || ""} ${(change.before.args || []).join(" ")}`)}
1775
+ `);
1776
+ process.stdout.write(` ${dim(shellCommand("npx", change.after.args || []))}
1777
+ `);
1778
+ }
1779
+ }
1780
+ if (!write) {
1781
+ process.stdout.write(`
1782
+ Dry run only. Apply with:
1783
+ `);
1784
+ process.stdout.write(` ${dim("npx protect-mcp wrap --claude-desktop --write")}
1785
+
1786
+ `);
1787
+ return;
1788
+ }
1789
+ const backupPath = `${claudePath}.bak.${Date.now()}`;
1790
+ writeFileSync(backupPath, readFileSync(claudePath, "utf-8"));
1791
+ writeFileSync(claudePath, JSON.stringify(next, null, 2) + "\n");
1792
+ process.stdout.write(`
1793
+ ${green("\u2713 Claude Desktop config updated")}
1794
+ `);
1795
+ process.stdout.write(` Backup: ${backupPath}
1796
+ `);
1797
+ process.stdout.write(` Restart Claude Desktop, then run: ${dim("npx protect-mcp dashboard --open")}
1798
+
469
1799
  `);
470
1800
  }
471
1801
  function bold(s) {
@@ -615,7 +1945,7 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
615
1945
  async function handleBundle(argv) {
616
1946
  const { readFileSync, writeFileSync, existsSync } = await import("fs");
617
1947
  const { join } = await import("path");
618
- const { createAuditBundle } = await import("./bundle-XTR3YMPO.mjs");
1948
+ const { createAuditBundle } = await import("./bundle-YUUHAG7O.mjs");
619
1949
  let dir = process.cwd();
620
1950
  const dirIdx = argv.indexOf("--dir");
621
1951
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
@@ -642,6 +1972,7 @@ No key file found at ${keyPath}
642
1972
  const bundle = createAuditBundle({
643
1973
  tenant: keyData.issuer || "protect-mcp",
644
1974
  receipts,
1975
+ selectiveDisclosures: collectSelectiveDisclosurePackages(dir),
645
1976
  signingKeys: [{
646
1977
  kty: "OKP",
647
1978
  crv: "Ed25519",
@@ -656,6 +1987,8 @@ ${bold("protect-mcp bundle")}
656
1987
 
657
1988
  `);
658
1989
  process.stdout.write(` Receipts: ${receipts.length}
1990
+ `);
1991
+ process.stdout.write(` Disclosures: ${collectSelectiveDisclosurePackages(dir).length}
659
1992
  `);
660
1993
  process.stdout.write(` Output: ${outputPath}
661
1994
  `);
@@ -857,6 +2190,648 @@ ${bold("protect-mcp quickstart")}
857
2190
  process.env.PROTECT_MCP_CONFIG = configPath;
858
2191
  await handleDemo();
859
2192
  }
2193
+ async function handleRegistry(argv) {
2194
+ const subcommand = argv[0] || "status";
2195
+ const dir = resolveCli(flagValue(argv, "--dir") || process.cwd());
2196
+ const orgName = flagValue(argv, "--org") || process.env.SCOPEBLIND_ORG;
2197
+ const orgId = flagValue(argv, "--org-id") || process.env.SCOPEBLIND_ORG_ID;
2198
+ const billingAccountId = flagValue(argv, "--billing-account") || process.env.SCOPEBLIND_BILLING_ACCOUNT;
2199
+ const endpoint = flagValue(argv, "--endpoint") || process.env.SCOPEBLIND_REGISTRY_ENDPOINT || (argv.includes("--hosted") ? "https://api.scopeblind.com" : void 0);
2200
+ const token = flagValue(argv, "--token") || process.env.SCOPEBLIND_TOKEN;
2201
+ const verifierBaseUrl = flagValue(argv, "--verifier-base") || process.env.SCOPEBLIND_VERIFIER_BASE || "https://legate.scopeblind.com";
2202
+ const registryMod = await import("./receipt-registry-6CAOY6RP.mjs");
2203
+ if (subcommand === "init") {
2204
+ const identity = registryMod.createOrgIdentity({
2205
+ dir,
2206
+ orgName,
2207
+ orgId,
2208
+ billingAccountId
2209
+ });
2210
+ const path = registryMod.writeOrgIdentity(dir, identity);
2211
+ process.stdout.write(`
2212
+ ${bold("protect-mcp registry init")}
2213
+
2214
+ `);
2215
+ process.stdout.write(` Org: ${identity.org_name}
2216
+ `);
2217
+ process.stdout.write(` Org ID: ${identity.org_id}
2218
+ `);
2219
+ process.stdout.write(` Billing account: ${identity.billing_account_id}
2220
+ `);
2221
+ process.stdout.write(` Public keys: ${identity.public_key_directory.length}
2222
+ `);
2223
+ process.stdout.write(` Wrote: ${path}
2224
+
2225
+ `);
2226
+ process.stdout.write(`${dim("No prompts, tool payloads, raw receipts, or private keys are included.")}
2227
+
2228
+ `);
2229
+ return;
2230
+ }
2231
+ if (subcommand === "anchor") {
2232
+ process.stdout.write(`
2233
+ ${bold("protect-mcp registry anchor")}
2234
+
2235
+ `);
2236
+ const result = await registryMod.createReceiptRegistry({
2237
+ dir,
2238
+ orgName,
2239
+ orgId,
2240
+ billingAccountId,
2241
+ endpoint,
2242
+ token,
2243
+ hosted: argv.includes("--hosted") || Boolean(endpoint || token),
2244
+ verifierBaseUrl,
2245
+ outPath: flagValue(argv, "--output")
2246
+ });
2247
+ process.stdout.write(` Org: ${result.registry.org.org_name}
2248
+ `);
2249
+ process.stdout.write(` Billing account: ${result.registry.billing.billing_account_id}
2250
+ `);
2251
+ process.stdout.write(` Digests: ${result.registry.records.length}
2252
+ `);
2253
+ process.stdout.write(` Anchors: ${result.registry.anchors.length}
2254
+ `);
2255
+ process.stdout.write(` Boundary: ${result.uploaded ? green("hosted digest anchor") : yellow("local preview only")}
2256
+ `);
2257
+ process.stdout.write(` Registry: ${result.registryPath}
2258
+ `);
2259
+ process.stdout.write(` Verifier page: ${result.verifierPath}
2260
+
2261
+ `);
2262
+ process.stdout.write(` Uploaded fields: ${result.registry.privacy.uploaded_fields.join(", ")}
2263
+ `);
2264
+ process.stdout.write(` Excluded fields: ${result.registry.privacy.excluded_fields.join(", ")}
2265
+
2266
+ `);
2267
+ if (!result.uploaded) {
2268
+ process.stdout.write(`${yellow(" This is not an independent timestamp yet.")}
2269
+ `);
2270
+ process.stdout.write(` Run with ${dim("--hosted --token $SCOPEBLIND_TOKEN")} to make the paid boundary real.
2271
+
2272
+ `);
2273
+ }
2274
+ return;
2275
+ }
2276
+ if (subcommand === "status") {
2277
+ const registryPath = joinCli(dir, registryMod.REGISTRY_FILE);
2278
+ const identityPath = joinCli(dir, registryMod.ORG_IDENTITY_FILE);
2279
+ process.stdout.write(`
2280
+ ${bold("protect-mcp registry status")}
2281
+
2282
+ `);
2283
+ if (existsSyncCli(identityPath)) {
2284
+ const identity = JSON.parse(readFileSyncCli(identityPath, "utf-8"));
2285
+ process.stdout.write(` Org: ${identity.org_name || "unknown"}
2286
+ `);
2287
+ process.stdout.write(` Org ID: ${identity.org_id || "unknown"}
2288
+ `);
2289
+ process.stdout.write(` Billing account: ${identity.billing_account_id || "unknown"}
2290
+ `);
2291
+ } else {
2292
+ process.stdout.write(` Org identity: ${yellow("missing")} (${identityPath})
2293
+ `);
2294
+ }
2295
+ if (existsSyncCli(registryPath)) {
2296
+ const registry = JSON.parse(readFileSyncCli(registryPath, "utf-8"));
2297
+ const hosted = Array.isArray(registry.anchors) && registry.anchors.some((a) => a.timestamp_source === "scopeblind-hosted");
2298
+ process.stdout.write(` Registry: ${registryPath}
2299
+ `);
2300
+ process.stdout.write(` Digests: ${registry.records?.length || 0}
2301
+ `);
2302
+ process.stdout.write(` Anchors: ${registry.anchors?.length || 0}
2303
+ `);
2304
+ process.stdout.write(` Boundary: ${hosted ? green("hosted digest anchor") : yellow("local preview only")}
2305
+ `);
2306
+ process.stdout.write(` Verifier page: ${joinCli(dir, registryMod.VERIFIER_PAGE_FILE)}
2307
+ `);
2308
+ } else {
2309
+ process.stdout.write(` Registry: ${yellow("missing")} (${registryPath})
2310
+ `);
2311
+ process.stdout.write(` Next: ${dim("npx protect-mcp registry anchor --hosted")}
2312
+ `);
2313
+ }
2314
+ process.stdout.write(`
2315
+ `);
2316
+ return;
2317
+ }
2318
+ process.stderr.write("Usage: protect-mcp registry init|anchor|status [--dir <path>] [--org <name>] [--hosted]\\n");
2319
+ process.exit(1);
2320
+ }
2321
+ async function handleKillerDemo(argv) {
2322
+ const { mkdtempSync } = await import("fs");
2323
+ const { tmpdir } = await import("os");
2324
+ const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
2325
+ const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
2326
+ const { randomBytes } = await import("crypto");
2327
+ const artifacts = await import("@veritasacta/artifacts");
2328
+ const {
2329
+ createSelectiveDisclosurePackage,
2330
+ signCommittedDecision,
2331
+ verifySelectiveDisclosurePackage
2332
+ } = await import("./signing-committed-MMLJ6OGG.mjs");
2333
+ const registryMod = await import("./receipt-registry-6CAOY6RP.mjs");
2334
+ const dir = resolveCli(flagValue(argv, "--dir") || mkdtempSync(joinCli(tmpdir(), "scopeblind-killer-demo-")));
2335
+ mkdirSyncCli(dir, { recursive: true });
2336
+ mkdirSyncCli(joinCli(dir, "keys"), { recursive: true });
2337
+ mkdirSyncCli(joinCli(dir, "receipts"), { recursive: true });
2338
+ const privateKeyBytes = randomBytes(32);
2339
+ const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes);
2340
+ const keypair = {
2341
+ privateKey: bytesToHex(privateKeyBytes),
2342
+ publicKey: bytesToHex(publicKeyBytes),
2343
+ kid: `killer-demo-${Date.now()}`,
2344
+ issuer: "scopeblind-killer-demo"
2345
+ };
2346
+ const keyPath = joinCli(dir, "keys", "gateway.json");
2347
+ writeFileSyncCli(keyPath, JSON.stringify({
2348
+ ...keypair,
2349
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2350
+ warning: "Demo key only. Do not use for production."
2351
+ }, null, 2) + "\n");
2352
+ const shadowConfigPath = joinCli(dir, "protect-mcp.shadow.json");
2353
+ const policyPackPath = joinCli(dir, "protect-mcp.policy-pack.json");
2354
+ const config = {
2355
+ tools: { "*": { rate_limit: "100/hour" } },
2356
+ default_tier: "signed-known",
2357
+ signing: { key_path: keyPath, issuer: keypair.issuer, enabled: true }
2358
+ };
2359
+ const policyPack = {
2360
+ tools: {
2361
+ "*": { rate_limit: "100/hour" },
2362
+ read_file: { rate_limit: "60/hour" },
2363
+ github_create_pr: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
2364
+ send_email: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
2365
+ pms_book_fill: { require_approval: true, min_tier: "signed-known", rate_limit: "10/hour" },
2366
+ delete_file: { block: true, min_tier: "privileged" }
2367
+ },
2368
+ default_tier: "signed-known",
2369
+ signing: { key_path: keyPath, issuer: keypair.issuer, enabled: true },
2370
+ notes: ["Demo policy pack: approvals for GitHub, email, and PMS booking; destructive tools blocked."]
2371
+ };
2372
+ writeFileSyncCli(shadowConfigPath, JSON.stringify(config, null, 2) + "\n");
2373
+ writeFileSyncCli(policyPackPath, JSON.stringify(policyPack, null, 2) + "\n");
2374
+ await initSigning({ enabled: true, key_path: keyPath, issuer: keypair.issuer });
2375
+ const logPath = joinCli(dir, ".protect-mcp-log.jsonl");
2376
+ const receiptPath = joinCli(dir, ".protect-mcp-receipts.jsonl");
2377
+ const shadowCalls = [
2378
+ { tool: "read_file", input: { path: "/research/macro-notes.md" }, reason: "observe_mode" },
2379
+ { tool: "github_create_pr", input: { repo: "scopeblind/legate", branch: "agent/pms-adapter", title: "Wire mock PMS adapter" }, reason: "observe_mode" },
2380
+ { tool: "send_email", input: { to: "ops@examplefund.com", subject: "Booking update", body: "Draft only", api_key: "demo-secret" }, reason: "observe_mode" },
2381
+ { 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" }
2382
+ ];
2383
+ for (const [idx, call] of shadowCalls.entries()) {
2384
+ const requestId2 = `demo-shadow-${idx + 1}`;
2385
+ appendFileSyncCli(logPath, JSON.stringify({
2386
+ v: 2,
2387
+ tool: call.tool,
2388
+ decision: "allow",
2389
+ reason_code: call.reason,
2390
+ request_id: requestId2,
2391
+ timestamp: Date.now() + idx,
2392
+ mode: "shadow",
2393
+ policy_digest: "shadow-policy",
2394
+ action_readback: buildActionReadback(call.tool, call.input)
2395
+ }) + "\n");
2396
+ }
2397
+ const sensitiveInput = {
2398
+ account: "Meridian Global Macro",
2399
+ symbol: "AAPL",
2400
+ side: "BUY",
2401
+ quantity: 50,
2402
+ price: 182.4,
2403
+ strategy: "US Large Cap Tactical",
2404
+ trader_note: "Do not reveal portfolio context outside the desk.",
2405
+ api_key: "demo-pms-secret"
2406
+ };
2407
+ const readback = buildActionReadback("pms_book_fill", sensitiveInput);
2408
+ const requestId = "demo-sensitive-pms-booking";
2409
+ const requireApprovalEntry = {
2410
+ v: 2,
2411
+ tool: "pms_book_fill",
2412
+ decision: "require_approval",
2413
+ reason_code: "requires_human_approval",
2414
+ request_id: requestId,
2415
+ timestamp: Date.now() + 10,
2416
+ mode: "enforce",
2417
+ policy_digest: createHashCli("sha256").update(JSON.stringify(policyPack)).digest("hex").slice(0, 16),
2418
+ action_readback: readback
2419
+ };
2420
+ appendFileSyncCli(logPath, JSON.stringify(requireApprovalEntry) + "\n");
2421
+ appendFileSyncCli(joinCli(dir, ".protect-mcp-approval-resolutions.jsonl"), JSON.stringify({
2422
+ type: "scopeblind.approval_resolution.v1",
2423
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2424
+ request_id: requestId,
2425
+ tool: "pms_book_fill",
2426
+ resolution: "approve",
2427
+ reason: "Matches the ticket and stays inside mandate.",
2428
+ payload_hash: readback.payload_hash
2429
+ }) + "\n");
2430
+ const executedEntry = {
2431
+ ...requireApprovalEntry,
2432
+ decision: "allow",
2433
+ reason_code: "approval_granted",
2434
+ timestamp: Date.now() + 20,
2435
+ payload_digest: {
2436
+ output_hash: createHashCli("sha256").update("mock-pms-booking-confirmed").digest("hex"),
2437
+ output_size: 26,
2438
+ truncated: false
2439
+ }
2440
+ };
2441
+ appendFileSyncCli(logPath, JSON.stringify(executedEntry) + "\n");
2442
+ const signed = signDecision(executedEntry);
2443
+ if (!signed.signed) throw new Error(`demo signing failed: ${signed.warning || signed.error || "unknown"}`);
2444
+ appendFileSyncCli(receiptPath, signed.signed + "\n");
2445
+ writeFileSyncCli(joinCli(dir, "receipts", "approved-pms-booking.receipt.json"), JSON.stringify(JSON.parse(signed.signed), null, 2) + "\n");
2446
+ const receiptArtifact = JSON.parse(signed.signed);
2447
+ const tamperedArtifact = JSON.parse(signed.signed);
2448
+ if (tamperedArtifact.payload && typeof tamperedArtifact.payload === "object") {
2449
+ tamperedArtifact.payload.decision = "deny";
2450
+ tamperedArtifact.payload.tool = "send_email";
2451
+ } else {
2452
+ tamperedArtifact.tool = "send_email";
2453
+ }
2454
+ const validOriginal = artifacts.verifyArtifact(receiptArtifact, keypair.publicKey);
2455
+ const validTampered = artifacts.verifyArtifact(tamperedArtifact, keypair.publicKey);
2456
+ writeFileSyncCli(joinCli(dir, "receipts", "tampered.receipt.json"), JSON.stringify(tamperedArtifact, null, 2) + "\n");
2457
+ const committed = signCommittedDecision(
2458
+ executedEntry,
2459
+ ["tool", "payload_digest", "swarm"],
2460
+ keypair.privateKey,
2461
+ keypair.publicKey,
2462
+ keypair.kid,
2463
+ keypair.issuer
2464
+ );
2465
+ const committedReceipt = JSON.parse(committed.signed);
2466
+ const disclosurePackage = createSelectiveDisclosurePackage(committedReceipt, ["tool"], committed.openings);
2467
+ const disclosureVerification = verifySelectiveDisclosurePackage(committedReceipt, disclosurePackage);
2468
+ appendFileSyncCli(receiptPath, committed.signed + "\n");
2469
+ writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.receipt.json"), JSON.stringify(committedReceipt, null, 2) + "\n");
2470
+ writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.package.json"), JSON.stringify(disclosurePackage, null, 2) + "\n");
2471
+ writeFileSyncCli(joinCli(dir, "receipts", "selective-disclosure.tool-only.json"), JSON.stringify(disclosurePackage, null, 2) + "\n");
2472
+ writeFileSyncCli(joinCli(dir, "verification-results.json"), JSON.stringify({
2473
+ original_receipt_valid: validOriginal,
2474
+ tampered_receipt_valid: validTampered,
2475
+ selective_disclosure_valid: disclosureVerification.valid,
2476
+ selective_disclosure_explanation: disclosureVerification.explanation
2477
+ }, null, 2) + "\n");
2478
+ const registry = await registryMod.createReceiptRegistry({
2479
+ dir,
2480
+ orgName: flagValue(argv, "--org") || "Meridian Global Macro Demo",
2481
+ billingAccountId: flagValue(argv, "--billing-account") || "demo_billing_digest_only",
2482
+ hosted: argv.includes("--hosted"),
2483
+ endpoint: flagValue(argv, "--endpoint") || process.env.SCOPEBLIND_REGISTRY_ENDPOINT || (argv.includes("--hosted") ? "https://api.scopeblind.com" : void 0),
2484
+ token: flagValue(argv, "--token") || process.env.SCOPEBLIND_TOKEN,
2485
+ verifierBaseUrl: flagValue(argv, "--verifier-base") || "https://legate.scopeblind.com"
2486
+ });
2487
+ const runbook = [
2488
+ "# ScopeBlind Killer Demo",
2489
+ "",
2490
+ "Three-minute flow, generated locally.",
2491
+ "",
2492
+ "## 1. Agent has tools",
2493
+ "",
2494
+ "Mock tools represented: filesystem `read_file`, GitHub `github_create_pr`, email `send_email`, and PMS `pms_book_fill`.",
2495
+ "",
2496
+ "## 2. Shadow mode shows risk",
2497
+ "",
2498
+ "Open the dashboard against this directory:",
2499
+ "",
2500
+ "```bash",
2501
+ `npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`,
2502
+ "```",
2503
+ "",
2504
+ "You will see GitHub, email, and PMS calls ranked as high risk.",
2505
+ "",
2506
+ "## 3. Apply policy pack",
2507
+ "",
2508
+ `Policy pack: \`${policyPackPath}\`.`,
2509
+ "",
2510
+ "It requires approval for GitHub PRs, outbound email, and PMS booking; destructive file deletion is blocked.",
2511
+ "",
2512
+ "## 4. Sensitive action requires exact approval",
2513
+ "",
2514
+ `Request id: \`${requestId}\``,
2515
+ "",
2516
+ `Exact readback summary: \`${readback.summary}\``,
2517
+ "",
2518
+ `Payload hash: \`${readback.payload_hash}\``,
2519
+ "",
2520
+ "Secret-like fields are redacted from the approval preview but still affect the hash.",
2521
+ "",
2522
+ "## 5. User approves; tool executes through gateway",
2523
+ "",
2524
+ "Approval resolution: `.protect-mcp-approval-resolutions.jsonl`",
2525
+ "",
2526
+ "Signed receipt: `receipts/approved-pms-booking.receipt.json`",
2527
+ "",
2528
+ "## 6. Offline verification and tamper failure",
2529
+ "",
2530
+ "Verification result: `verification-results.json`.",
2531
+ "",
2532
+ "Expected: original valid, tampered invalid.",
2533
+ "",
2534
+ "## 7. Selective disclosure",
2535
+ "",
2536
+ "Committed receipt: `receipts/selective-disclosure.receipt.json`",
2537
+ "",
2538
+ "Tool-only v0 disclosure package: `receipts/selective-disclosure.tool-only.json`",
2539
+ "",
2540
+ "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`.",
2541
+ "",
2542
+ "This demonstrates hiding sensitive context while revealing the minimum needed proof. It is salted commitment disclosure, not full zero-knowledge.",
2543
+ "",
2544
+ "## 8. Paid boundary MVP",
2545
+ "",
2546
+ `Registry: \`${registry.registryPath}\``,
2547
+ "",
2548
+ `Verifier page: \`${registry.verifierPath}\``,
2549
+ "",
2550
+ `Boundary: ${registry.uploaded ? "hosted digest anchor with independent timestamp" : "local preview only; hosted anchoring not used"}.`,
2551
+ "",
2552
+ "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.",
2553
+ ""
2554
+ ].join("\n");
2555
+ writeFileSyncCli(joinCli(dir, "DEMO-RUNBOOK.md"), runbook);
2556
+ writeFileSyncCli(joinCli(dir, "demo-summary.json"), JSON.stringify({
2557
+ dir,
2558
+ dashboard_command: `npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`,
2559
+ policy_pack: policyPackPath,
2560
+ receipt: joinCli(dir, "receipts", "approved-pms-booking.receipt.json"),
2561
+ tampered_receipt: joinCli(dir, "receipts", "tampered.receipt.json"),
2562
+ selective_disclosure_receipt: joinCli(dir, "receipts", "selective-disclosure.receipt.json"),
2563
+ selective_disclosure_package: joinCli(dir, "receipts", "selective-disclosure.tool-only.json"),
2564
+ verification_results: joinCli(dir, "verification-results.json"),
2565
+ registry: registry.registryPath,
2566
+ verifier_page: registry.verifierPath,
2567
+ runbook: joinCli(dir, "DEMO-RUNBOOK.md"),
2568
+ original_valid: validOriginal.valid,
2569
+ tampered_valid: validTampered.valid,
2570
+ selective_disclosure_valid: disclosureVerification.valid
2571
+ }, null, 2) + "\n");
2572
+ process.stdout.write(`
2573
+ ${bold("protect-mcp killer-demo")}
2574
+
2575
+ `);
2576
+ process.stdout.write(` Demo dir: ${dir}
2577
+ `);
2578
+ process.stdout.write(` Dashboard: ${dim(`npx protect-mcp dashboard --dir ${dir} --policy ${policyPackPath} --open`)}
2579
+ `);
2580
+ process.stdout.write(` Runbook: ${joinCli(dir, "DEMO-RUNBOOK.md")}
2581
+ `);
2582
+ process.stdout.write(` Signed receipt: ${joinCli(dir, "receipts", "approved-pms-booking.receipt.json")}
2583
+ `);
2584
+ process.stdout.write(` Tamper check: original=${validOriginal.valid ? green("valid") : red("invalid")} tampered=${validTampered.valid ? red("valid") : green("invalid")}
2585
+ `);
2586
+ process.stdout.write(` Registry: ${registry.registryPath}
2587
+ `);
2588
+ process.stdout.write(` Verifier page: ${registry.verifierPath}
2589
+ `);
2590
+ process.stdout.write(` Boundary: ${registry.uploaded ? green("hosted digest anchor") : yellow("local preview only")}
2591
+
2592
+ `);
2593
+ }
2594
+ async function handleVerifyDisclosure(argv) {
2595
+ const receiptPath = flagValue(argv, "--receipt");
2596
+ const disclosurePath = flagValue(argv, "--disclosure");
2597
+ if (!receiptPath || !disclosurePath) {
2598
+ process.stderr.write("Usage: protect-mcp verify-disclosure --receipt <committed-receipt.json> --disclosure <selective-disclosure.json>\\n");
2599
+ process.exit(1);
2600
+ }
2601
+ const { verifySelectiveDisclosurePackage } = await import("./signing-committed-MMLJ6OGG.mjs");
2602
+ const receipt = JSON.parse(readFileSyncCli(resolveCli(receiptPath), "utf-8"));
2603
+ const disclosure = JSON.parse(readFileSyncCli(resolveCli(disclosurePath), "utf-8"));
2604
+ const result = verifySelectiveDisclosurePackage(receipt, disclosure);
2605
+ process.stdout.write(`
2606
+ ${bold("protect-mcp verify-disclosure")}
2607
+
2608
+ `);
2609
+ process.stdout.write(` Result: ${result.valid ? green("valid") : red("invalid")}
2610
+ `);
2611
+ process.stdout.write(` Receipt hash: ${result.receipt_hash_valid ? green("matches") : red("mismatch")}
2612
+ `);
2613
+ process.stdout.write(` Signature: ${result.signature_valid === true ? green("valid") : result.signature_valid === null ? yellow("not checked") : red("invalid")}
2614
+ `);
2615
+ process.stdout.write(` Commitment root: ${result.commitment_root_valid ? green("matches") : red("mismatch")}
2616
+ `);
2617
+ process.stdout.write(` Disclosed fields: ${result.disclosed_fields.length ? result.disclosed_fields.join(", ") : "none"}
2618
+ `);
2619
+ process.stdout.write(` Hidden fields: ${result.hidden_fields.length ? result.hidden_fields.join(", ") : "none"}
2620
+
2621
+ `);
2622
+ for (const line of result.explanation) {
2623
+ process.stdout.write(` - ${line}
2624
+ `);
2625
+ }
2626
+ if (result.errors.length > 0) {
2627
+ process.stdout.write(`
2628
+ ${red("Errors:")}
2629
+ `);
2630
+ for (const err of result.errors) process.stdout.write(` - ${err}
2631
+ `);
2632
+ }
2633
+ process.stdout.write("\n");
2634
+ if (!result.valid) process.exit(2);
2635
+ }
2636
+ async function handlePolicyPacks(argv) {
2637
+ const subcommand = argv[0] || "list";
2638
+ const packArg = argv[1];
2639
+ const dir = resolveCli(flagValue(argv, "--dir") || "./cedar");
2640
+ const force = argv.includes("--force");
2641
+ if (subcommand === "list") {
2642
+ process.stdout.write(`
2643
+ ${bold("protect-mcp policy-packs")}
2644
+
2645
+ `);
2646
+ for (const pack of POLICY_PACKS) {
2647
+ process.stdout.write(` ${bold(pack.id.padEnd(22))} ${pack.name}
2648
+ `);
2649
+ process.stdout.write(` ${dim(" ".repeat(24) + pack.description)}
2650
+ `);
2651
+ process.stdout.write(` ${dim(" ".repeat(24) + `recommended: ${pack.recommendedMode}`)}
2652
+
2653
+ `);
2654
+ }
2655
+ process.stdout.write(`Install one: ${dim("protect-mcp policy-packs install filesystem-safe --dir ./cedar")}
2656
+ `);
2657
+ process.stdout.write(`Install all: ${dim("protect-mcp policy-packs install all --dir ./cedar")}
2658
+
2659
+ `);
2660
+ return;
2661
+ }
2662
+ if (subcommand === "show") {
2663
+ const pack = getPolicyPack(packArg || "");
2664
+ if (!pack) {
2665
+ process.stderr.write(`Unknown policy pack "${packArg || ""}". Available: ${policyPackIds().join(", ")}
2666
+ `);
2667
+ process.exit(1);
2668
+ }
2669
+ process.stdout.write(`
2670
+ ${bold(pack.name)} (${pack.id})
2671
+
2672
+ `);
2673
+ process.stdout.write(`${pack.description}
2674
+ `);
2675
+ process.stdout.write(`Recommended rollout: ${pack.recommendedMode}
2676
+
2677
+ `);
2678
+ for (const file of pack.files) {
2679
+ process.stdout.write(`${dim(`--- ${file.path} ---`)}
2680
+ `);
2681
+ process.stdout.write(file.contents.endsWith("\n") ? file.contents : `${file.contents}
2682
+ `);
2683
+ process.stdout.write("\n");
2684
+ }
2685
+ return;
2686
+ }
2687
+ if (subcommand === "install") {
2688
+ const packs = packArg === "all" ? POLICY_PACKS : [getPolicyPack(packArg || "")].filter(Boolean);
2689
+ if (packs.length === 0) {
2690
+ process.stderr.write(`Usage: protect-mcp policy-packs install <${policyPackIds().join("|")}|all> [--dir ./cedar] [--force]
2691
+ `);
2692
+ process.exit(1);
2693
+ }
2694
+ mkdirSyncCli(dir, { recursive: true });
2695
+ const written = [];
2696
+ for (const pack of packs) {
2697
+ for (const file of pack.files) {
2698
+ const outPath = joinCli(dir, file.path);
2699
+ if (existsSyncCli(outPath) && !force) {
2700
+ process.stderr.write(`Refusing to overwrite ${outPath}. Re-run with --force if intentional.
2701
+ `);
2702
+ process.exit(1);
2703
+ }
2704
+ writeFileSyncCli(outPath, file.contents.endsWith("\n") ? file.contents : `${file.contents}
2705
+ `);
2706
+ written.push(outPath);
2707
+ }
2708
+ }
2709
+ process.stdout.write(`
2710
+ ${bold("protect-mcp policy-packs install")}
2711
+
2712
+ `);
2713
+ process.stdout.write(` Directory: ${dir}
2714
+ `);
2715
+ for (const outPath of written) process.stdout.write(` Wrote: ${outPath}
2716
+ `);
2717
+ process.stdout.write(`
2718
+ Next: ${dim(`protect-mcp serve --cedar ${dir}`)} for shadow mode, then add ${dim("--enforce")} after reviewing receipts.
2719
+
2720
+ `);
2721
+ return;
2722
+ }
2723
+ process.stderr.write("Usage: protect-mcp policy-packs list|show|install [pack] [--dir ./cedar] [--force]\n");
2724
+ process.exit(1);
2725
+ }
2726
+ async function handleConnectors(argv) {
2727
+ const subcommand = argv[0] || "list";
2728
+ const pilotArg = argv[1];
2729
+ const dir = resolveCli(flagValue(argv, "--dir") || process.cwd());
2730
+ const force = argv.includes("--force");
2731
+ if (subcommand === "list") {
2732
+ process.stdout.write(`
2733
+ ${bold("protect-mcp connector pilots")}
2734
+
2735
+ `);
2736
+ for (const pilot of CONNECTOR_PILOTS) {
2737
+ process.stdout.write(` ${bold(pilot.id.padEnd(18))} ${pilot.name}
2738
+ `);
2739
+ process.stdout.write(` ${dim(" ".repeat(20) + pilot.description)}
2740
+ `);
2741
+ process.stdout.write(` ${dim(" ".repeat(20) + `tools: ${pilot.tools.join(", ")}`)}
2742
+
2743
+ `);
2744
+ }
2745
+ process.stdout.write(`Install all: ${dim("protect-mcp connectors init all --force")}
2746
+ `);
2747
+ process.stdout.write(`Check credentials: ${dim("protect-mcp connectors doctor")}
2748
+
2749
+ `);
2750
+ return;
2751
+ }
2752
+ if (subcommand === "show") {
2753
+ const pilot = getConnectorPilot(pilotArg || "");
2754
+ if (!pilot) {
2755
+ process.stderr.write(`Unknown connector pilot "${pilotArg || ""}". Available: ${CONNECTOR_PILOTS.map((p) => p.id).join(", ")}
2756
+ `);
2757
+ process.exit(1);
2758
+ }
2759
+ process.stdout.write(`
2760
+ ${bold(pilot.name)} (${pilot.id})
2761
+
2762
+ `);
2763
+ process.stdout.write(`${pilot.description}
2764
+
2765
+ `);
2766
+ process.stdout.write(`${bold("Why it matters:")} ${pilot.value}
2767
+
2768
+ `);
2769
+ process.stdout.write(`${bold("Tools:")} ${pilot.tools.join(", ")}
2770
+
2771
+ `);
2772
+ process.stdout.write(`${bold("Setup:")}
2773
+ `);
2774
+ for (const step of pilot.setup) process.stdout.write(` - ${step}
2775
+ `);
2776
+ process.stdout.write(`
2777
+ ${bold("Starter policy:")}
2778
+ ${pilot.cedar}
2779
+ `);
2780
+ return;
2781
+ }
2782
+ if (subcommand === "init") {
2783
+ const ids = pilotArg ? [pilotArg] : ["all"];
2784
+ const installed = writeConnectorPilots({ dir, ids, force });
2785
+ process.stdout.write(`
2786
+ ${bold("protect-mcp connectors init")}
2787
+
2788
+ `);
2789
+ process.stdout.write(` Directory: ${installed.directory}
2790
+ `);
2791
+ for (const outPath of installed.written) process.stdout.write(` Wrote: ${outPath}
2792
+ `);
2793
+ process.stdout.write(`
2794
+ Next: ${dim("protect-mcp connectors doctor")} then ${dim("protect-mcp dashboard --open")}.
2795
+
2796
+ `);
2797
+ return;
2798
+ }
2799
+ if (subcommand === "doctor") {
2800
+ let rows = connectorDoctor(dir);
2801
+ if (pilotArg && pilotArg !== "all") {
2802
+ const pilot = getConnectorPilot(pilotArg);
2803
+ if (!pilot) {
2804
+ process.stderr.write(`Unknown connector pilot "${pilotArg}". Available: ${CONNECTOR_PILOTS.map((p) => p.id).join(", ")}
2805
+ `);
2806
+ process.exit(1);
2807
+ }
2808
+ rows = rows.filter((row) => row.id === pilot.id);
2809
+ }
2810
+ process.stdout.write(`
2811
+ ${bold("protect-mcp connectors doctor")}
2812
+
2813
+ `);
2814
+ for (const row of rows) {
2815
+ const missing = Array.isArray(row.missing_required) && row.missing_required.length > 0 ? row.missing_required.join(", ") : "";
2816
+ const status = row.installed ? row.usable ? green("ready") : yellow("needs setup") : dim("not installed");
2817
+ process.stdout.write(` ${bold(String(row.id).padEnd(18))} ${status}
2818
+ `);
2819
+ process.stdout.write(` ${dim(" ".repeat(20) + `mode: ${String(row.mode || "unknown")}`)}
2820
+ `);
2821
+ if (missing) process.stdout.write(` ${yellow(" ".repeat(20) + `missing: ${missing}`)}
2822
+ `);
2823
+ process.stdout.write(` ${dim(" ".repeat(20) + String(row.next || ""))}
2824
+
2825
+ `);
2826
+ }
2827
+ process.stdout.write(`${dim("Secret values are never printed; only missing variable names are shown.")}
2828
+
2829
+ `);
2830
+ return;
2831
+ }
2832
+ process.stderr.write("Usage: protect-mcp connectors list|show|init|doctor [connector|all] [--dir <path>] [--force]\n");
2833
+ process.exit(1);
2834
+ }
860
2835
  async function handleTrace(argv) {
861
2836
  const receiptId = argv[0];
862
2837
  if (!receiptId) {
@@ -1375,7 +3350,7 @@ async function handleEvaluate(argv) {
1375
3350
  if (typeof input.command === "string" && context.command_pattern === void 0) {
1376
3351
  context.command_pattern = input.command;
1377
3352
  }
1378
- const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
3353
+ const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context, toolInput: input }, void 0, { failClosed: true });
1379
3354
  if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
1380
3355
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
1381
3356
  process.exit(decision.allowed ? 0 : 2);
@@ -1474,6 +3449,50 @@ async function main() {
1474
3449
  await handleQuickstart(args.slice(1));
1475
3450
  return;
1476
3451
  }
3452
+ if (args[0] === "wrap") {
3453
+ await handleWrap(args.slice(1));
3454
+ process.exit(0);
3455
+ }
3456
+ if (args[0] === "dashboard") {
3457
+ await handleDashboard(args.slice(1));
3458
+ return;
3459
+ }
3460
+ if (args[0] === "recommend") {
3461
+ await handleRecommend(args.slice(1));
3462
+ process.exit(0);
3463
+ }
3464
+ if (args[0] === "registry") {
3465
+ await handleRegistry(args.slice(1));
3466
+ process.exit(0);
3467
+ }
3468
+ if (args[0] === "trial") {
3469
+ await handleKillerDemo(args.slice(1));
3470
+ process.stdout.write(`${bold("Next: open the local dashboard")}
3471
+ `);
3472
+ process.stdout.write(` npx protect-mcp dashboard --dir ${dim(flagValue(args.slice(1), "--dir") || "<demo dir printed above>")} --open
3473
+
3474
+ `);
3475
+ process.stdout.write(`${dim("No ScopeBlind account is required for local receipts. Add --hosted with SCOPEBLIND_TOKEN when you want independent digest anchoring.")}
3476
+
3477
+ `);
3478
+ process.exit(0);
3479
+ }
3480
+ if (args[0] === "killer-demo") {
3481
+ await handleKillerDemo(args.slice(1));
3482
+ process.exit(0);
3483
+ }
3484
+ if (args[0] === "verify-disclosure") {
3485
+ await handleVerifyDisclosure(args.slice(1));
3486
+ process.exit(0);
3487
+ }
3488
+ if (args[0] === "policy-packs") {
3489
+ await handlePolicyPacks(args.slice(1));
3490
+ process.exit(0);
3491
+ }
3492
+ if (args[0] === "connectors") {
3493
+ await handleConnectors(args.slice(1));
3494
+ process.exit(0);
3495
+ }
1477
3496
  if (args[0] === "connect") {
1478
3497
  await handleConnect();
1479
3498
  process.exit(0);
@@ -1610,7 +3629,7 @@ async function main() {
1610
3629
  if (useHttp) {
1611
3630
  const portIdx = args.indexOf("--port");
1612
3631
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
1613
- const { startHttpTransport } = await import("./http-transport-R5AO7X6D.mjs");
3632
+ const { startHttpTransport } = await import("./http-transport-4GMMRPW7.mjs");
1614
3633
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
1615
3634
  return;
1616
3635
  }