protect-mcp 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1140,7 +1140,7 @@ function handleHealth(res, startTime, config) {
1140
1140
  status: "ok",
1141
1141
  uptime_ms: Date.now() - startTime,
1142
1142
  mode: config.mode,
1143
- version: "0.3.1"
1143
+ version: process.env.PROTECT_MCP_VERSION || "unknown"
1144
1144
  }));
1145
1145
  }
1146
1146
  function handleStatus(res, logDir) {
@@ -6651,7 +6651,7 @@ async function startHookServer(options = {}) {
6651
6651
  res.end(JSON.stringify({
6652
6652
  status: "ok",
6653
6653
  server: "protect-mcp-hooks",
6654
- version: "0.5.0",
6654
+ version: process.env.PROTECT_MCP_VERSION || "unknown",
6655
6655
  uptime_ms: Date.now() - state.startTime,
6656
6656
  mode: enforce ? "enforce" : "shadow",
6657
6657
  policy_digest: policyDigest,
@@ -6738,9 +6738,10 @@ async function startHookServer(options = {}) {
6738
6738
  const pad = (s, n = 46) => s.padEnd(n);
6739
6739
  w(`
6740
6740
  `);
6741
- w(` protect-mcp v0.5.4
6741
+ w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
6742
+ ` : ` protect-mcp
6742
6743
  `);
6743
- w(` ScopeBlind \u2014 https://scopeblind.com
6744
+ w(` ScopeBlind \xB7 https://scopeblind.com
6744
6745
  `);
6745
6746
  w(`
6746
6747
  `);
@@ -6768,7 +6769,13 @@ async function startHookServer(options = {}) {
6768
6769
  `);
6769
6770
  w(`
6770
6771
  `);
6771
- w(` deny is authoritative \u2014 cannot be overridden.
6772
+ w(` deny is authoritative: it cannot be overridden.
6773
+ `);
6774
+ w(`
6775
+ `);
6776
+ w(` See your record npx protect-mcp record
6777
+ `);
6778
+ w(` a searchable view of every decision, all on this machine
6772
6779
  `);
6773
6780
  w(`
6774
6781
  `);
@@ -6918,7 +6925,7 @@ async function startHttpTransport(options) {
6918
6925
  res.end(JSON.stringify({
6919
6926
  status: "ok",
6920
6927
  server: "protect-mcp",
6921
- version: "0.4.0",
6928
+ version: process.env.PROTECT_MCP_VERSION || "unknown",
6922
6929
  transport: "streamable-http",
6923
6930
  mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow",
6924
6931
  wrapping: serverCommand.join(" ")
@@ -7561,6 +7568,292 @@ var defaultPermit2 = `
7561
7568
  // Default posture: observe all non-matching tools so the connector can be piloted in shadow mode.
7562
7569
  permit(principal, action == Action::"MCP::Tool::call", resource);
7563
7570
  `;
7571
+ var nautilusBridgePy = String.raw`#!/usr/bin/env python3
7572
+ """
7573
+ ScopeBlind external bridge for NautilusTrader-compatible pilots.
7574
+
7575
+ This file is intentionally outside NautilusTrader. It gives protect-mcp a stable
7576
+ JSONL command boundary for staging, approval-gated submission, cancellation, and
7577
+ event export while keeping the trading engine customer-owned.
7578
+
7579
+ Mock mode runs without NautilusTrader installed. Real mode is enabled by setting
7580
+ NAUTILUS_BRIDGE_MODULE to "module.path:ClassName"; the class may implement:
7581
+ submit_order(order), modify_order(order), cancel_order(order), reconcile(order),
7582
+ export_events(since=None)
7583
+ """
7584
+
7585
+ from __future__ import annotations
7586
+
7587
+ import hashlib
7588
+ import importlib
7589
+ import json
7590
+ import os
7591
+ import sys
7592
+ import time
7593
+ from dataclasses import dataclass, field
7594
+ from pathlib import Path
7595
+ from typing import Any, Callable
7596
+
7597
+
7598
+ def canonical_json(value: Any) -> str:
7599
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
7600
+
7601
+
7602
+ def sha256_json(value: Any) -> str:
7603
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
7604
+
7605
+
7606
+ def now_ms() -> int:
7607
+ return int(time.time() * 1000)
7608
+
7609
+
7610
+ @dataclass
7611
+ class BridgeState:
7612
+ root: Path = field(default_factory=lambda: Path(os.environ.get("SCOPEBLIND_NAUTILUS_STATE_DIR", ".protect-mcp/nautilus")))
7613
+
7614
+ def __post_init__(self) -> None:
7615
+ self.root.mkdir(parents=True, exist_ok=True)
7616
+ self.orders_path.touch(exist_ok=True)
7617
+ self.events_path.touch(exist_ok=True)
7618
+
7619
+ @property
7620
+ def orders_path(self) -> Path:
7621
+ return self.root / "orders.jsonl"
7622
+
7623
+ @property
7624
+ def events_path(self) -> Path:
7625
+ return self.root / "events.jsonl"
7626
+
7627
+ def append_order(self, order: dict[str, Any]) -> None:
7628
+ with self.orders_path.open("a", encoding="utf-8") as handle:
7629
+ handle.write(canonical_json(order) + "\n")
7630
+
7631
+ def append_event(self, event: dict[str, Any]) -> dict[str, Any]:
7632
+ enriched = {
7633
+ "event_id": event.get("event_id") or f"nt-{now_ms()}-{len(event)}",
7634
+ "observed_at_ms": now_ms(),
7635
+ **event,
7636
+ }
7637
+ enriched["event_digest"] = sha256_json(enriched)
7638
+ with self.events_path.open("a", encoding="utf-8") as handle:
7639
+ handle.write(canonical_json(enriched) + "\n")
7640
+ return enriched
7641
+
7642
+ def events(self) -> list[dict[str, Any]]:
7643
+ rows: list[dict[str, Any]] = []
7644
+ with self.events_path.open("r", encoding="utf-8") as handle:
7645
+ for line in handle:
7646
+ if line.strip():
7647
+ rows.append(json.loads(line))
7648
+ return rows
7649
+
7650
+
7651
+ class ScopeBlindNautilusBridge:
7652
+ def __init__(self) -> None:
7653
+ self.state = BridgeState()
7654
+ self.real = self._load_real_bridge()
7655
+
7656
+ def _load_real_bridge(self) -> Any | None:
7657
+ target = os.environ.get("NAUTILUS_BRIDGE_MODULE")
7658
+ if not target:
7659
+ return None
7660
+ module_name, _, class_name = target.partition(":")
7661
+ if not module_name or not class_name:
7662
+ raise ValueError("NAUTILUS_BRIDGE_MODULE must be module.path:ClassName")
7663
+ module = importlib.import_module(module_name)
7664
+ return getattr(module, class_name)()
7665
+
7666
+ def handle(self, command: dict[str, Any]) -> dict[str, Any]:
7667
+ action = command.get("action")
7668
+ handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
7669
+ "stage_order": self.stage_order,
7670
+ "submit_order": self.submit_order,
7671
+ "modify_order": self.modify_order,
7672
+ "cancel_order": self.cancel_order,
7673
+ "reconcile": self.reconcile,
7674
+ "export_events": self.export_events,
7675
+ }
7676
+ if action not in handlers:
7677
+ return self.error(command, "unknown_action", f"Unsupported action: {action}")
7678
+ try:
7679
+ return handlers[action](command)
7680
+ except Exception as exc:
7681
+ return self.error(command, "bridge_error", str(exc))
7682
+
7683
+ def require(self, command: dict[str, Any], *fields: str) -> None:
7684
+ missing = [field for field in fields if command.get(field) in (None, "")]
7685
+ if missing:
7686
+ raise ValueError(f"missing required field(s): {', '.join(missing)}")
7687
+
7688
+ def require_approved(self, command: dict[str, Any]) -> None:
7689
+ self.require(command, "approval_receipt")
7690
+ if command.get("mandate_passed") is not True:
7691
+ raise ValueError("mandate_passed must be true before live order mutation")
7692
+
7693
+ def stage_order(self, command: dict[str, Any]) -> dict[str, Any]:
7694
+ self.require(command, "client_order_id", "instrument_id", "side", "quantity")
7695
+ order = self.order_projection(command, status="staged")
7696
+ self.state.append_order(order)
7697
+ event = self.state.append_event({
7698
+ "type": "scopeblind.nautilus.order_staged.v1",
7699
+ "client_order_id": order["client_order_id"],
7700
+ "order_digest": sha256_json(order),
7701
+ "disclosure": "position_blind",
7702
+ })
7703
+ return self.ok(command, {"status": "staged", "order": order, "event": event})
7704
+
7705
+ def submit_order(self, command: dict[str, Any]) -> dict[str, Any]:
7706
+ self.require_approved(command)
7707
+ order = self.order_projection(command, status="submitted")
7708
+ if self.real and hasattr(self.real, "submit_order"):
7709
+ external = self.real.submit_order(order)
7710
+ else:
7711
+ external = {"mode": "mock", "external_order_id": f"MOCK-{order['client_order_id']}"}
7712
+ event = self.state.append_event({
7713
+ "type": "scopeblind.nautilus.order_submitted.v1",
7714
+ "client_order_id": order["client_order_id"],
7715
+ "order_digest": sha256_json(order),
7716
+ "external_digest": sha256_json(external),
7717
+ "disclosure": "position_blind",
7718
+ })
7719
+ return self.ok(command, {"status": "submitted", "order": order, "external": external, "event": event})
7720
+
7721
+ def modify_order(self, command: dict[str, Any]) -> dict[str, Any]:
7722
+ self.require_approved(command)
7723
+ self.require(command, "client_order_id")
7724
+ if self.real and hasattr(self.real, "modify_order"):
7725
+ external = self.real.modify_order(command)
7726
+ else:
7727
+ external = {"mode": "mock", "modified": command["client_order_id"]}
7728
+ event = self.state.append_event({
7729
+ "type": "scopeblind.nautilus.order_modified.v1",
7730
+ "client_order_id": command["client_order_id"],
7731
+ "command_digest": sha256_json(command),
7732
+ "external_digest": sha256_json(external),
7733
+ "disclosure": "position_blind",
7734
+ })
7735
+ return self.ok(command, {"status": "modified", "external": external, "event": event})
7736
+
7737
+ def cancel_order(self, command: dict[str, Any]) -> dict[str, Any]:
7738
+ self.require_approved(command)
7739
+ self.require(command, "client_order_id")
7740
+ if self.real and hasattr(self.real, "cancel_order"):
7741
+ external = self.real.cancel_order(command)
7742
+ else:
7743
+ external = {"mode": "mock", "cancelled": command["client_order_id"]}
7744
+ event = self.state.append_event({
7745
+ "type": "scopeblind.nautilus.order_cancelled.v1",
7746
+ "client_order_id": command["client_order_id"],
7747
+ "command_digest": sha256_json(command),
7748
+ "external_digest": sha256_json(external),
7749
+ "disclosure": "position_blind",
7750
+ })
7751
+ return self.ok(command, {"status": "cancelled", "external": external, "event": event})
7752
+
7753
+ def reconcile(self, command: dict[str, Any]) -> dict[str, Any]:
7754
+ self.require(command, "client_order_id")
7755
+ if self.real and hasattr(self.real, "reconcile"):
7756
+ external = self.real.reconcile(command)
7757
+ else:
7758
+ external = {"mode": "mock", "client_order_id": command["client_order_id"], "state": "accepted"}
7759
+ event = self.state.append_event({
7760
+ "type": "scopeblind.nautilus.reconciled.v1",
7761
+ "client_order_id": command["client_order_id"],
7762
+ "external_digest": sha256_json(external),
7763
+ "disclosure": "position_blind",
7764
+ })
7765
+ return self.ok(command, {"status": "reconciled", "external": external, "event": event})
7766
+
7767
+ def export_events(self, command: dict[str, Any]) -> dict[str, Any]:
7768
+ if self.real and hasattr(self.real, "export_events"):
7769
+ external_events = self.real.export_events(command.get("since"))
7770
+ else:
7771
+ external_events = self.state.events()
7772
+ return self.ok(command, {
7773
+ "status": "exported",
7774
+ "event_count": len(external_events),
7775
+ "commitment_root": sha256_json(external_events),
7776
+ "events": external_events,
7777
+ })
7778
+
7779
+ def order_projection(self, command: dict[str, Any], status: str) -> dict[str, Any]:
7780
+ return {
7781
+ "client_order_id": command["client_order_id"],
7782
+ "instrument_id": command["instrument_id"],
7783
+ "side": command["side"],
7784
+ "quantity": command["quantity"],
7785
+ "price": command.get("price"),
7786
+ "time_in_force": command.get("time_in_force", "GTC"),
7787
+ "strategy_id": command.get("strategy_id"),
7788
+ "mandate_digest": command.get("mandate_digest"),
7789
+ "approval_receipt": command.get("approval_receipt"),
7790
+ "status": status,
7791
+ "created_at_ms": now_ms(),
7792
+ }
7793
+
7794
+ def ok(self, command: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
7795
+ return {
7796
+ "ok": True,
7797
+ "bridge": "scopeblind.nautilus.external.v1",
7798
+ "mode": "real" if self.real else "mock",
7799
+ "request_digest": sha256_json(command),
7800
+ **result,
7801
+ }
7802
+
7803
+ def error(self, command: dict[str, Any], code: str, message: str) -> dict[str, Any]:
7804
+ return {
7805
+ "ok": False,
7806
+ "bridge": "scopeblind.nautilus.external.v1",
7807
+ "mode": "real" if self.real else "mock",
7808
+ "error": {"code": code, "message": message},
7809
+ "request_digest": sha256_json(command),
7810
+ }
7811
+
7812
+
7813
+ def main() -> int:
7814
+ bridge = ScopeBlindNautilusBridge()
7815
+ for line in sys.stdin:
7816
+ if not line.strip():
7817
+ continue
7818
+ command = json.loads(line)
7819
+ print(canonical_json(bridge.handle(command)), flush=True)
7820
+ return 0
7821
+
7822
+
7823
+ if __name__ == "__main__":
7824
+ raise SystemExit(main())
7825
+ `;
7826
+ var nautilusAdapterReadme = `# NautilusTrader-compatible external bridge
7827
+
7828
+ This connector is intentionally external to NautilusTrader. It lets protect-mcp
7829
+ control and receipt high-risk order actions while a customer-owned Nautilus
7830
+ process remains the trading engine.
7831
+
7832
+ ## Local mock run
7833
+
7834
+ \`\`\`bash
7835
+ python3 .protect-mcp/connectors/nautilus-trader/bridge.py <<'JSONL'
7836
+ {"action":"stage_order","client_order_id":"SB-1","instrument_id":"AAPL.NASDAQ","side":"BUY","quantity":"50","price":"182.40","mandate_digest":"demo"}
7837
+ {"action":"submit_order","client_order_id":"SB-1","instrument_id":"AAPL.NASDAQ","side":"BUY","quantity":"50","price":"182.40","mandate_digest":"demo","mandate_passed":true,"approval_receipt":"receipt-demo"}
7838
+ {"action":"export_events"}
7839
+ JSONL
7840
+ \`\`\`
7841
+
7842
+ ## Real mode
7843
+
7844
+ Set \`NAUTILUS_BRIDGE_MODULE=customer_module:BridgeClass\`. The class can
7845
+ implement \`submit_order\`, \`modify_order\`, \`cancel_order\`, \`reconcile\`,
7846
+ and \`export_events\`. Keep that glue in the customer's repository so Nautilus
7847
+ licensing, credentials, and trading logic stay outside ScopeBlind.
7848
+
7849
+ ## Upstream contribution posture
7850
+
7851
+ The best NautilusTrader contribution is not this bridge or a UI. It is a small,
7852
+ vendor-neutral audit/event sink RFC: a documented way to export normalized order
7853
+ commands, execution reports, fills, cancels, and reconciliation events so
7854
+ external compliance wrappers can prove what happened without mutating the
7855
+ engine.
7856
+ `;
7564
7857
  var CONNECTOR_PILOTS = [
7565
7858
  {
7566
7859
  id: "github",
@@ -7764,6 +8057,93 @@ when { ["pms.order.stage", "pms.order.book", "pms.order.cancel"].contains(contex
7764
8057
 
7765
8058
  forbid(principal, action == Action::"MCP::Tool::call", resource)
7766
8059
  when { context.tool == "pms.order.book" && context.mandate_passed != true };
8060
+ `
8061
+ },
8062
+ {
8063
+ id: "nautilus-trader",
8064
+ category: "finance",
8065
+ name: "NautilusTrader-compatible external bridge",
8066
+ status: "usable-pilot",
8067
+ description: "Controls NautilusTrader-compatible staged orders through an external JSONL bridge, with local mock mode and customer-owned real mode.",
8068
+ value: "Turns Nautilus into a strong Legate demo target: mandate-check, exact approval, external order event, position-blind audit bundle, and later reconciliation.",
8069
+ env: [
8070
+ { name: "NAUTILUS_BRIDGE_MODULE", required: false, description: "Optional customer glue in module.path:ClassName form for real Nautilus submission." },
8071
+ { name: "SCOPEBLIND_NAUTILUS_STATE_DIR", required: false, description: "Optional state directory for local mock events. Defaults to .protect-mcp/nautilus." },
8072
+ { name: "NAUTILUS_TRADER_PROJECT", required: false, description: "Optional path to the customer Nautilus project when running real mode." }
8073
+ ],
8074
+ tools: [
8075
+ "nautilus.order.stage",
8076
+ "nautilus.order.submit",
8077
+ "nautilus.order.modify",
8078
+ "nautilus.order.cancel",
8079
+ "nautilus.strategy.deploy",
8080
+ "nautilus.event.export",
8081
+ "nautilus.reconcile"
8082
+ ],
8083
+ actions: [
8084
+ { name: "Stage order", tool: "nautilus.order.stage", risk: "medium", mode: "require_approval", description: "Creates a position-blind booking intent and event commitment." },
8085
+ { name: "Submit order", tool: "nautilus.order.submit", risk: "high", mode: "require_approval", description: "Requires mandate pass plus exact approval before live order mutation." },
8086
+ { name: "Modify or cancel order", tool: "nautilus.order.modify", risk: "high", mode: "require_approval", description: "Mutates live order state and must carry a fresh approval receipt." },
8087
+ { name: "Deploy strategy", tool: "nautilus.strategy.deploy", risk: "high", mode: "require_approval", description: "Requires signed strategy pack, mandate scope, and operator approval." },
8088
+ { name: "Export event log", tool: "nautilus.event.export", risk: "low", mode: "observe", description: "Exports normalized event commitments for receipt corroboration." }
8089
+ ],
8090
+ setup: [
8091
+ "Run mock mode first: protect-mcp connectors init nautilus-trader --force.",
8092
+ "Pipe stage/submit/reconcile JSONL through .protect-mcp/connectors/nautilus-trader/bridge.py.",
8093
+ "For real mode, set NAUTILUS_BRIDGE_MODULE to customer-owned glue that calls NautilusTrader APIs.",
8094
+ "Open an upstream NautilusTrader RFC for a neutral audit/event sink before proposing any PR."
8095
+ ],
8096
+ config: {
8097
+ type: "scopeblind.connector_pilot.v1",
8098
+ provider: "nautilus-trader-compatible",
8099
+ mode: "external-bridge-mock-first",
8100
+ license_boundary: "No NautilusTrader code is bundled. Real mode calls a customer-owned process/module.",
8101
+ adapter_contract: {
8102
+ protocol: "stdin/stdout JSONL",
8103
+ bridge: ".protect-mcp/connectors/nautilus-trader/bridge.py",
8104
+ real_mode_env: "NAUTILUS_BRIDGE_MODULE=module.path:ClassName",
8105
+ actions: ["stage_order", "submit_order", "modify_order", "cancel_order", "reconcile", "export_events"]
8106
+ },
8107
+ controlled_tools: [
8108
+ "nautilus.order.stage",
8109
+ "nautilus.order.submit",
8110
+ "nautilus.order.modify",
8111
+ "nautilus.order.cancel",
8112
+ "nautilus.strategy.deploy",
8113
+ "nautilus.event.export",
8114
+ "nautilus.reconcile"
8115
+ ],
8116
+ approval_required_for: ["submit_order", "modify_order", "cancel_order", "strategy_deploy"],
8117
+ receipt_fields: [
8118
+ "client_order_id",
8119
+ "instrument_id_hash",
8120
+ "side",
8121
+ "quantity",
8122
+ "price",
8123
+ "mandate_digest",
8124
+ "approval_receipt",
8125
+ "external_event_digest",
8126
+ "commitment_root"
8127
+ ],
8128
+ upstream_rfc: {
8129
+ title: "[RFC] Add a vendor-neutral order/execution audit event sink",
8130
+ non_goals: ["ScopeBlind dependency", "UI dashboard", "AI tooling", "new venue adapter"]
8131
+ }
8132
+ },
8133
+ artifacts: [
8134
+ { path: "nautilus-trader/bridge.py", contents: nautilusBridgePy, executable: true },
8135
+ { path: "nautilus-trader/README.md", contents: nautilusAdapterReadme }
8136
+ ],
8137
+ cedar: `${defaultPermit2}
8138
+ // NautilusTrader-compatible pilot: stage can be observed, but any live mutation requires exact approval.
8139
+ forbid(principal, action == Action::"MCP::Tool::call", resource)
8140
+ when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel", "nautilus.strategy.deploy"].contains(context.tool) && !context.approved };
8141
+
8142
+ forbid(principal, action == Action::"MCP::Tool::call", resource)
8143
+ when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel"].contains(context.tool) && context.mandate_passed != true };
8144
+
8145
+ forbid(principal, action == Action::"MCP::Tool::call", resource)
8146
+ when { context.tool == "nautilus.strategy.deploy" && context.strategy_pack_signed != true };
7767
8147
  `
7768
8148
  }
7769
8149
  ];
@@ -7792,11 +8172,26 @@ function writeConnectorPilots(opts) {
7792
8172
  (0, import_node_fs8.writeFileSync)(policyPath, pilot.cedar.endsWith("\n") ? pilot.cedar : `${pilot.cedar}
7793
8173
  `);
7794
8174
  written.push(configPath, policyPath);
8175
+ for (const artifact of pilot.artifacts || []) {
8176
+ const artifactPath = connectorArtifactPath(directory, artifact.path);
8177
+ (0, import_node_fs8.mkdirSync)((0, import_node_path5.dirname)(artifactPath), { recursive: true });
8178
+ (0, import_node_fs8.writeFileSync)(artifactPath, artifact.contents.endsWith("\n") ? artifact.contents : `${artifact.contents}
8179
+ `);
8180
+ if (artifact.executable) (0, import_node_fs8.chmodSync)(artifactPath, 493);
8181
+ written.push(artifactPath);
8182
+ }
7795
8183
  }
7796
8184
  (0, import_node_fs8.writeFileSync)((0, import_node_path5.join)(directory, "README.md"), renderConnectorReadme(selected));
7797
8185
  written.push((0, import_node_path5.join)(directory, "README.md"));
7798
8186
  return { written, pilots: selected, directory };
7799
8187
  }
8188
+ function connectorArtifactPath(directory, relativePath) {
8189
+ const clean2 = (0, import_node_path5.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
8190
+ if (clean2.startsWith("/") || clean2.includes("..")) {
8191
+ throw new Error(`Unsafe connector artifact path: ${relativePath}`);
8192
+ }
8193
+ return (0, import_node_path5.join)(directory, clean2);
8194
+ }
7800
8195
  function readInstalledConnectorPilots(dir) {
7801
8196
  const directory = connectorDirectory(dir);
7802
8197
  if (!(0, import_node_fs8.existsSync)(directory)) return [];
@@ -7830,15 +8225,15 @@ function connectorDoctor(dir, env = process.env) {
7830
8225
  }));
7831
8226
  const missingRequired = envRows.filter((item) => item.required && !item.present).map((item) => item.name);
7832
8227
  const optionalPresent = envRows.filter((item) => !item.required && item.present).map((item) => item.name);
7833
- const optionalProviderReady = pilot.id === "slack-teams" ? Boolean(env.SLACK_BOT_TOKEN || env.TEAMS_WEBHOOK_URL) : pilot.id === "finance-pms" ? Boolean(env.PMS_ADAPTER_URL) : false;
7834
- const mockModeReady = pilot.id === "finance-pms";
8228
+ const optionalProviderReady = pilot.id === "slack-teams" ? Boolean(env.SLACK_BOT_TOKEN || env.TEAMS_WEBHOOK_URL) : pilot.id === "finance-pms" ? Boolean(env.PMS_ADAPTER_URL) : pilot.id === "nautilus-trader" ? Boolean(env.NAUTILUS_BRIDGE_MODULE || env.NAUTILUS_TRADER_PROJECT) : false;
8229
+ const mockModeReady = pilot.id === "finance-pms" || pilot.id === "nautilus-trader";
7835
8230
  return {
7836
8231
  id: pilot.id,
7837
8232
  name: pilot.name,
7838
8233
  category: pilot.category,
7839
8234
  installed: installed.has(pilot.id),
7840
8235
  usable: missingRequired.length === 0 && (pilot.env.some((item) => item.required) || pilot.env.length === 0 || optionalProviderReady || mockModeReady),
7841
- mode: pilot.id === "finance-pms" && !env.PMS_ADAPTER_URL ? "mock" : pilot.id === "slack-teams" && !env.SLACK_BOT_TOKEN && !env.TEAMS_WEBHOOK_URL ? "needs_provider_choice" : "configured_or_local",
8236
+ mode: pilot.id === "finance-pms" && !env.PMS_ADAPTER_URL ? "mock" : pilot.id === "nautilus-trader" && !env.NAUTILUS_BRIDGE_MODULE ? "mock_bridge" : pilot.id === "slack-teams" && !env.SLACK_BOT_TOKEN && !env.TEAMS_WEBHOOK_URL ? "needs_provider_choice" : "configured_or_local",
7842
8237
  missing_required: missingRequired,
7843
8238
  optional_present: optionalPresent,
7844
8239
  tools: pilot.tools,
@@ -7861,7 +8256,10 @@ Tools: ${pilot.tools.map((tool) => `\`${tool}\``).join(", ")}
7861
8256
 
7862
8257
  Setup:
7863
8258
  ${pilot.setup.map((step) => `- ${step}`).join("\n")}
7864
- `).join("\n")}
8259
+ ${pilot.artifacts?.length ? `
8260
+ Generated files:
8261
+ ${pilot.artifacts.map((artifact) => `- \`${artifact.path}\``).join("\n")}
8262
+ ` : ""}`).join("\n")}
7865
8263
  Next: run \`npx protect-mcp dashboard --open\` and review tool inventory, policy coverage, approvals, and receipts.
7866
8264
  `;
7867
8265
  }
@@ -7871,10 +8269,9 @@ var import_node_crypto7 = require("crypto");
7871
8269
  var import_node_fs12 = require("fs");
7872
8270
  var import_node_path8 = require("path");
7873
8271
  var import_node_os = require("os");
7874
- var import_meta = {};
7875
8272
  function printHelp() {
7876
8273
  process.stderr.write(`
7877
- protect-mcp \u2014 Enterprise security gateway for MCP servers & Claude Code hooks
8274
+ protect-mcp: Enterprise security gateway for MCP servers & Claude Code hooks
7878
8275
 
7879
8276
  Usage:
7880
8277
  protect-mcp [options] -- <command> [args...]
@@ -7897,6 +8294,7 @@ Usage:
7897
8294
  protect-mcp status [--dir <path>]
7898
8295
  protect-mcp digest [--today] [--dir <path>]
7899
8296
  protect-mcp receipts [--last <n>] [--dir <path>]
8297
+ protect-mcp record [--dir <path>] [--no-open]
7900
8298
  protect-mcp bundle [--output <path>] [--dir <path>]
7901
8299
  protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
7902
8300
  protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
@@ -7934,6 +8332,7 @@ Commands:
7934
8332
  status Show tool call statistics from the local decision log
7935
8333
  digest Generate a human-readable summary of agent activity
7936
8334
  receipts Show recent persisted signed receipts
8335
+ record Open a local, searchable view of your record in the browser
7937
8336
  bundle Export an offline-verifiable audit bundle
7938
8337
 
7939
8338
  Examples:
@@ -8127,14 +8526,14 @@ Add --enforce when ready to block policy violations.
8127
8526
  }
8128
8527
  async function handleDemo() {
8129
8528
  const { existsSync: existsSync9 } = await import("fs");
8130
- const { join: join8, dirname: dirname2, resolve } = await import("path");
8529
+ const { join: join8, dirname: dirname3, resolve } = await import("path");
8131
8530
  const { realpathSync } = await import("fs");
8132
8531
  const cliPath = resolve(process.argv[1] || "dist/cli.js");
8133
8532
  let cliDir;
8134
8533
  try {
8135
- cliDir = dirname2(realpathSync(cliPath));
8534
+ cliDir = dirname3(realpathSync(cliPath));
8136
8535
  } catch {
8137
- cliDir = dirname2(cliPath);
8536
+ cliDir = dirname3(cliPath);
8138
8537
  }
8139
8538
  const demoServerPath = join8(cliDir, "demo-server.js");
8140
8539
  const configPath = join8(process.cwd(), "protect-mcp.json");
@@ -9780,6 +10179,162 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
9780
10179
  process.stdout.write(`
9781
10180
  `);
9782
10181
  }
10182
+ var _pkgV = null;
10183
+ async function pkgVersion() {
10184
+ if (_pkgV) return _pkgV;
10185
+ let v = "0.0.0";
10186
+ try {
10187
+ const { readFileSync: readFileSync11, existsSync: existsSync9, realpathSync } = await import("fs");
10188
+ const { dirname: dirname3, join: join8, resolve } = await import("path");
10189
+ let base = "";
10190
+ try {
10191
+ base = dirname3(realpathSync(resolve(process.argv[1] || "")));
10192
+ } catch {
10193
+ }
10194
+ const candidates = [
10195
+ base ? join8(base, "..", "package.json") : "",
10196
+ base ? join8(base, "package.json") : ""
10197
+ ].filter(Boolean);
10198
+ for (const p of candidates) {
10199
+ if (existsSync9(p)) {
10200
+ const parsed = JSON.parse(readFileSync11(p, "utf-8"));
10201
+ if (parsed && parsed.name === "protect-mcp" && parsed.version) {
10202
+ v = parsed.version;
10203
+ break;
10204
+ }
10205
+ }
10206
+ }
10207
+ } catch {
10208
+ }
10209
+ _pkgV = v;
10210
+ return v;
10211
+ }
10212
+ function mapRecordEntry(e) {
10213
+ const p = e && e.payload && typeof e.payload === "object" ? e.payload : e;
10214
+ const dec = String(p.decision || e.decision || "").toLowerCase();
10215
+ const verdict = /den|block|reject|refus/.test(dec) ? "blocked" : /ask|approv|hold|escal|review|pending/.test(dec) ? "held" : "allowed";
10216
+ const tsRaw = e.issued_at || e.timestamp || p.timestamp || p.issued_at;
10217
+ const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
10218
+ const ts = isFinite(ms) ? new Date(ms).toISOString() : "";
10219
+ const tool = String(p.tool || e.tool || "action");
10220
+ const reason = String(p.reason_code || e.reason_code || p.policy_engine || "signed");
10221
+ const hook = String(p.hook_event || e.hook_event || "");
10222
+ const signed = !!(e.signature || e.sig || e.receipt_hash || typeof e.type === "string" && e.type.indexOf("receipt") >= 0);
10223
+ let digest = "";
10224
+ if (e.receipt_hash) digest = String(e.receipt_hash);
10225
+ else if (e.digest) digest = String(e.digest);
10226
+ else if (p.payload_digest && p.payload_digest.output_hash) digest = String(p.payload_digest.output_hash);
10227
+ return { ts, tool, verdict, reason, hook, signed, id: String(e.request_id || p.request_id || ""), digest };
10228
+ }
10229
+ async function handleRecord(argv) {
10230
+ const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
10231
+ const { join: join8 } = await import("path");
10232
+ const osMod = await import("os");
10233
+ const cp = await import("child_process");
10234
+ let dir = process.cwd();
10235
+ const di = argv.indexOf("--dir");
10236
+ if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
10237
+ const recPath = join8(dir, ".protect-mcp-receipts.jsonl");
10238
+ const logPath = join8(dir, ".protect-mcp-log.jsonl");
10239
+ const chosen = existsSync9(recPath) ? recPath : existsSync9(logPath) ? logPath : null;
10240
+ if (!chosen) {
10241
+ process.stderr.write(`
10242
+ ${bold("protect-mcp record")}
10243
+
10244
+ No record found in ${dir}.
10245
+ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
10246
+
10247
+ `);
10248
+ process.exit(0);
10249
+ return;
10250
+ }
10251
+ const raw = readFileSync11(chosen, "utf-8");
10252
+ const recs = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
10253
+ try {
10254
+ return JSON.parse(l);
10255
+ } catch {
10256
+ return null;
10257
+ }
10258
+ }).filter((x) => x !== null).map(mapRecordEntry);
10259
+ const meta = { file: chosen, signed: chosen === recPath, count: recs.length };
10260
+ const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
10261
+ const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
10262
+ writeFileSync4(out, html);
10263
+ if (!argv.includes("--no-open")) {
10264
+ const platform = process.platform;
10265
+ const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
10266
+ const openArgs = platform === "win32" ? ["/c", "start", "", out] : [out];
10267
+ try {
10268
+ const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
10269
+ child.unref();
10270
+ } catch {
10271
+ }
10272
+ }
10273
+ process.stdout.write(`
10274
+ ${bold("\u{1F6E1}\uFE0F Your record")} ${recs.length} decision${recs.length === 1 ? "" : "s"}
10275
+ `);
10276
+ process.stdout.write(` Opened a searchable view in your browser. Everything stayed on this machine.
10277
+ `);
10278
+ if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
10279
+ `);
10280
+ process.stdout.write(` ${dim("If it did not open, open this file: " + out)}
10281
+
10282
+ `);
10283
+ process.exit(0);
10284
+ }
10285
+ var RECORD_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>protect-mcp record</title>
10286
+ <style>
10287
+ :root{--paper:#f6f4ef;--ink:#1b1815;--soft:#524d46;--faint:#8a837a;--line:#ddd7c9;--g:#3f6146;--gb:#e7eee3;--a:#8f6216;--ab:#f2e8d3;--r:#7d3535;--rb:#f2e0dc}
10288
+ *{box-sizing:border-box}
10289
+ body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
10290
+ .wrap{max-width:1000px;margin:0 auto;padding:26px 22px 60px}
10291
+ h1{font-size:24px;margin:0 0 4px;letter-spacing:-.012em}
10292
+ .meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace}
10293
+ .bar{margin:18px 0 12px}
10294
+ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit}
10295
+ .chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
10296
+ .chip{cursor:pointer;font-size:12px;padding:3px 10px;border-radius:100px;border:1px solid var(--line);background:#fff;color:var(--soft)}
10297
+ .chip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
10298
+ .count{color:var(--faint);font-size:12px;font-family:ui-monospace,Menlo,monospace;margin-bottom:8px}
10299
+ .row{border:1px solid var(--line);border-radius:9px;background:#fcfbf7;padding:11px 13px;margin-bottom:8px;cursor:pointer}
10300
+ .top{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
10301
+ .pill{font-size:11px;font-weight:600;padding:2px 9px;border-radius:100px}
10302
+ .pill.allowed{background:var(--gb);color:var(--g)}.pill.held{background:var(--ab);color:var(--a)}.pill.blocked{background:var(--rb);color:var(--r)}
10303
+ .tag{font-size:11px;padding:1px 7px;border-radius:100px;background:var(--paper);border:1px solid var(--line);color:var(--faint)}
10304
+ .when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
10305
+ .det{margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:12px;color:var(--soft);font-family:ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all;display:none}
10306
+ .row.open .det{display:block}
10307
+ .foot{margin-top:22px;color:var(--faint);font-size:12px;line-height:1.6;border-top:1px solid var(--line);padding-top:14px}
10308
+ .foot b{color:var(--soft)}
10309
+ </style></head><body><div class="wrap">
10310
+ <h1>Your record</h1>
10311
+ <div class="meta" id="meta"></div>
10312
+ <div class="bar"><input id="q" placeholder="Search your record: tool, reason, verdict"></div>
10313
+ <div class="chips" id="chips"></div>
10314
+ <div class="count" id="count"></div>
10315
+ <div id="list"></div>
10316
+ <div class="foot">Signed decisions from your own gate. This page is local and nothing was uploaded. Verify authoritatively with <b>npx protect-mcp receipts</b> or the open verifier. protect-mcp governs proposed actions before they run.</div>
10317
+ </div>
10318
+ <script>
10319
+ var RECORDS=__DATA__;var META=__META__;var Q="",ACT={};
10320
+ function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]})}
10321
+ function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
10322
+ function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+d.toLocaleTimeString(undefined,{hour:"2-digit",minute:"2-digit"})}
10323
+ function fvals(key){var m={};RECORDS.forEach(function(r){var v=key==="Decision"?vlabel(r.verdict):r[key.toLowerCase()];if(v){m[v]=(m[v]||0)+1}});return Object.keys(m).sort(function(a,b){return m[b]-m[a]}).slice(0,8).map(function(v){return[v,m[v]]})}
10324
+ function match(r){if(ACT.Decision&&vlabel(r.verdict)!==ACT.Decision)return false;if(ACT.Tool&&r.tool!==ACT.Tool)return false;if(ACT.Reason&&r.reason!==ACT.Reason)return false;if(Q){var h=(r.tool+" "+r.reason+" "+vlabel(r.verdict)+" "+r.hook).toLowerCase();if(h.indexOf(Q)<0)return false}return true}
10325
+ function render(){
10326
+ document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local";
10327
+ var chips="";["Decision","Tool","Reason"].forEach(function(key){fvals(key).forEach(function(p){var on=ACT[key]===p[0];chips+='<span class="chip'+(on?" on":"")+'" data-k="'+key+'" data-v="'+esc(p[0])+'">'+esc(p[0])+" "+p[1]+"</span>"})});
10328
+ document.getElementById("chips").innerHTML=chips;
10329
+ var rows=RECORDS.filter(match);
10330
+ document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records";
10331
+ var html="";rows.slice(0,800).forEach(function(r){html+='<div class="row"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+'<span class="tag">'+(r.signed?"signed":"log")+'</span><span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r,null,2))+"</div></div>"});
10332
+ document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>'}
10333
+ document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
10334
+ document.getElementById("chips").addEventListener("click",function(e){var c=e.target.closest(".chip");if(!c)return;var k=c.getAttribute("data-k"),v=c.getAttribute("data-v");ACT[k]=ACT[k]===v?undefined:v;render()});
10335
+ document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
10336
+ render();
10337
+ </script></body></html>`;
9783
10338
  async function handleBundle(argv) {
9784
10339
  const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
9785
10340
  const { join: join8 } = await import("path");
@@ -9901,7 +10456,7 @@ ${bold("protect-mcp connect")}
9901
10456
  `));
9902
10457
  process.stderr.write(` Receipts will be uploaded automatically.
9903
10458
  `);
9904
- process.stderr.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
10459
+ process.stderr.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
9905
10460
  `));
9906
10461
  process.stderr.write(`
9907
10462
  ${"\u2500".repeat(50)}
@@ -9999,7 +10554,7 @@ ${bold("protect-mcp quickstart")}
9999
10554
  `));
10000
10555
  process.stdout.write(` Receipts will be uploaded automatically.
10001
10556
  `);
10002
- process.stdout.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
10557
+ process.stdout.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
10003
10558
  `));
10004
10559
  process.stdout.write(`
10005
10560
  `);
@@ -10748,7 +11303,7 @@ ${bold("protect-mcp trace")}
10748
11303
  process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
10749
11304
  `);
10750
11305
  if (rendered.has(id)) {
10751
- process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
11306
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle: already rendered)")}
10752
11307
  `);
10753
11308
  return;
10754
11309
  }
@@ -10951,7 +11506,7 @@ ${bold("protect-mcp init-hooks")}
10951
11506
 
10952
11507
  `);
10953
11508
  } catch {
10954
- process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys \u2014 signing disabled
11509
+ process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys, signing disabled
10955
11510
 
10956
11511
  `);
10957
11512
  }
@@ -11022,13 +11577,14 @@ ${bold("protect-mcp init-hooks")}
11022
11577
  process.stdout.write(` Every tool call will be receipted automatically.
11023
11578
 
11024
11579
  `);
11025
- process.stdout.write(` 3. Check receipts:
11580
+ process.stdout.write(` 3. See your record: a searchable view of every decision.
11026
11581
  `);
11027
- process.stdout.write(` ${dim(`curl http://127.0.0.1:${port}/receipts/latest`)}
11582
+ process.stdout.write(` ${dim(`npx protect-mcp record`)}
11028
11583
  `);
11029
- process.stdout.write(` ${dim(`npx protect-mcp receipts`)}
11584
+ process.stdout.write(` ${dim(`Everything stays on this machine. Nothing is uploaded.`)}
11585
+
11030
11586
  `);
11031
- process.stdout.write(` Or use ${dim("/verify-receipt")} inside Claude Code.
11587
+ process.stdout.write(` Prefer the terminal? ${dim(`npx protect-mcp receipts`)}, or ${dim("/verify-receipt")} in Claude Code.
11032
11588
 
11033
11589
  `);
11034
11590
  process.stdout.write(` 4. View policy suggestions:
@@ -11038,9 +11594,9 @@ ${bold("protect-mcp init-hooks")}
11038
11594
  `);
11039
11595
  process.stdout.write(`${bold("Key facts:")}
11040
11596
  `);
11041
- process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")} \u2014 cannot be overridden
11597
+ process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}: they cannot be overridden
11042
11598
  `);
11043
- process.stdout.write(` \u2022 PostToolUse runs ${bold("async")} \u2014 zero latency impact on tool execution
11599
+ process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}, so there is zero latency impact on tool execution
11044
11600
  `);
11045
11601
  process.stdout.write(` \u2022 Receipts are Ed25519-signed and append-only
11046
11602
  `);
@@ -11051,7 +11607,7 @@ ${bold("protect-mcp init-hooks")}
11051
11607
  async function sendInstallTelemetry() {
11052
11608
  try {
11053
11609
  const { existsSync: existsSync9, mkdirSync: mkdirSync3, writeFileSync: writeFileSync4, readFileSync: readFileSync11 } = await import("fs");
11054
- const { join: join8, dirname: dirname2 } = await import("path");
11610
+ const { join: join8, dirname: dirname3 } = await import("path");
11055
11611
  const { homedir } = await import("os");
11056
11612
  const { fileURLToPath } = await import("url");
11057
11613
  const markerDir = join8(homedir(), ".protect-mcp");
@@ -11059,15 +11615,7 @@ async function sendInstallTelemetry() {
11059
11615
  if (existsSync9(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
11060
11616
  return;
11061
11617
  }
11062
- let version = "unknown";
11063
- try {
11064
- const thisDir = dirname2(fileURLToPath(import_meta.url));
11065
- const pkgPath = join8(thisDir, "..", "package.json");
11066
- if (existsSync9(pkgPath)) {
11067
- version = JSON.parse(readFileSync11(pkgPath, "utf-8")).version;
11068
- }
11069
- } catch {
11070
- }
11618
+ const version = await pkgVersion();
11071
11619
  const controller = new AbortController();
11072
11620
  const timeout = setTimeout(() => controller.abort(), 3e3);
11073
11621
  fetch("https://api.scopeblind.com/telemetry/install", {
@@ -11241,6 +11789,7 @@ async function main() {
11241
11789
  sendInstallTelemetry().catch(() => {
11242
11790
  });
11243
11791
  const args = process.argv.slice(2);
11792
+ process.env.PROTECT_MCP_VERSION = process.env.PROTECT_MCP_VERSION || await pkgVersion();
11244
11793
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
11245
11794
  printHelp();
11246
11795
  process.exit(0);
@@ -11279,6 +11828,10 @@ async function main() {
11279
11828
  await startHookServer2({ port, policyPath: policyPath2, cedarDir: cedarDir2, enforce: enforce2, verbose: verbose2 });
11280
11829
  return;
11281
11830
  }
11831
+ if (args[0] === "record") {
11832
+ await handleRecord(args.slice(1));
11833
+ process.exit(0);
11834
+ }
11282
11835
  if (args[0] === "init-hooks") {
11283
11836
  await handleInitHooks(args.slice(1));
11284
11837
  process.exit(0);
@@ -11536,7 +12089,7 @@ async function handleDoctor() {
11536
12089
  process.stdout.write(green2(`Node.js ${nodeVersion}
11537
12090
  `));
11538
12091
  } else {
11539
- process.stdout.write(red2(`Node.js ${nodeVersion} \u2014 requires >= 18
12092
+ process.stdout.write(red2(`Node.js ${nodeVersion}, requires >= 18
11540
12093
  `));
11541
12094
  issues++;
11542
12095
  }
@@ -11547,7 +12100,7 @@ async function handleDoctor() {
11547
12100
  if (config.signing?.private_key || config.signing?.key_file) {
11548
12101
  process.stdout.write(green2("Signing keys configured\n"));
11549
12102
  } else {
11550
- process.stdout.write(yellow2("Config found but no signing keys \u2014 run: protect-mcp init\n"));
12103
+ process.stdout.write(yellow2("Config found but no signing keys. Run: protect-mcp init\n"));
11551
12104
  issues++;
11552
12105
  }
11553
12106
  } catch {
@@ -11555,7 +12108,7 @@ async function handleDoctor() {
11555
12108
  issues++;
11556
12109
  }
11557
12110
  } else {
11558
- process.stdout.write(yellow2("No scopeblind.config.json \u2014 run: protect-mcp init\n"));
12111
+ process.stdout.write(yellow2("No scopeblind.config.json. Run: protect-mcp init\n"));
11559
12112
  }
11560
12113
  let policyFound = false;
11561
12114
  for (const dir of ["cedar", "policies", "."]) {
@@ -11580,14 +12133,14 @@ async function handleDoctor() {
11580
12133
  }
11581
12134
  }
11582
12135
  if (!policyFound) {
11583
- process.stdout.write(yellow2("No policy files found \u2014 running in shadow mode (allow all)\n"));
12136
+ process.stdout.write(yellow2("No policy files found, running in shadow mode (allow all)\n"));
11584
12137
  }
11585
12138
  try {
11586
12139
  const cedarAvailable = await isCedarAvailable();
11587
12140
  if (cedarAvailable) {
11588
12141
  process.stdout.write(green2("Cedar WASM engine available\n"));
11589
12142
  } else {
11590
- process.stdout.write(dim2(" Cedar WASM not installed \u2014 install: npm install @cedar-policy/cedar-wasm\n"));
12143
+ process.stdout.write(dim2(" Cedar WASM not installed. Install: npm install @cedar-policy/cedar-wasm\n"));
11591
12144
  }
11592
12145
  } catch {
11593
12146
  process.stdout.write(dim2(" Cedar WASM not installed\n"));
@@ -11603,7 +12156,7 @@ async function handleDoctor() {
11603
12156
  process.stdout.write(green2("Decision log exists\n"));
11604
12157
  }
11605
12158
  } else {
11606
- process.stdout.write(dim2(" No decision log yet \u2014 will be created on first tool call\n"));
12159
+ process.stdout.write(dim2(" No decision log yet, will be created on first tool call\n"));
11607
12160
  }
11608
12161
  if (existsSync9(receiptFile)) {
11609
12162
  try {
@@ -11618,17 +12171,17 @@ async function handleDoctor() {
11618
12171
  execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
11619
12172
  process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
11620
12173
  } catch {
11621
- process.stdout.write(dim2(" Verifier not cached \u2014 install: npm install -g @veritasacta/verify\n"));
12174
+ process.stdout.write(dim2(" Verifier not cached. Install: npm install -g @veritasacta/verify\n"));
11622
12175
  }
11623
12176
  try {
11624
12177
  const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
11625
12178
  if (res.ok) {
11626
12179
  process.stdout.write(green2("ScopeBlind API reachable\n"));
11627
12180
  } else {
11628
- process.stdout.write(yellow2("ScopeBlind API returned non-200 \u2014 receipts will be stored locally\n"));
12181
+ process.stdout.write(yellow2("ScopeBlind API returned non-200, receipts will be stored locally\n"));
11629
12182
  }
11630
12183
  } catch {
11631
- process.stdout.write(dim2(" ScopeBlind API not reachable \u2014 offline mode (receipts stored locally)\n"));
12184
+ process.stdout.write(dim2(" ScopeBlind API not reachable, offline mode (receipts stored locally)\n"));
11632
12185
  }
11633
12186
  process.stdout.write("\nRestraint self-test:\n");
11634
12187
  try {