protect-mcp 0.7.4 → 0.7.6
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/{chunk-NVJHGXXG.mjs → chunk-6E2DHBAR.mjs} +12 -5
- package/dist/{chunk-36UID5WY.mjs → chunk-JCMDLN5I.mjs} +399 -8
- package/dist/{chunk-LYKNULYU.mjs → chunk-LJQOALYR.mjs} +152 -0
- package/dist/{chunk-KPSICBAJ.mjs → chunk-SETXVE2K.mjs} +2 -2
- package/dist/{chunk-UBZJ3VI2.mjs → chunk-VTPZ4G5I.mjs} +1 -1
- package/dist/{chunk-D2RDY2JR.mjs → chunk-WIPWNWMJ.mjs} +1 -1
- package/dist/{chunk-F2FKQ4XN.mjs → chunk-WV4DKYE4.mjs} +78 -2
- package/dist/cli.js +649 -44
- package/dist/cli.mjs +247 -39
- package/dist/demo-server.js +2 -2
- package/dist/demo-server.mjs +1 -1
- package/dist/{ed25519-DZMMNNVE.mjs → ed25519-BSHMMVNX.mjs} +1 -1
- package/dist/hook-server.js +11 -4
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-4GMMRPW7.mjs → http-transport-JBORN27G.mjs} +3 -3
- package/dist/index.d.mts +46 -12
- package/dist/index.d.ts +46 -12
- package/dist/index.js +1747 -37
- package/dist/index.mjs +1182 -34
- package/dist/{signing-committed-MMLJ6OGG.mjs → signing-committed-QXCW24RF.mjs} +2 -2
- package/package.json +2 -2
- package/policies/agent.cedar +50 -0
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:
|
|
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:
|
|
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
|
|
6741
|
+
w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
|
|
6742
|
+
` : ` protect-mcp
|
|
6742
6743
|
`);
|
|
6743
|
-
w(` ScopeBlind \
|
|
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
|
|
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:
|
|
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
|
-
`
|
|
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
|
|
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>] [--live] [--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:
|
|
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 =
|
|
8534
|
+
cliDir = dirname3(realpathSync(cliPath));
|
|
8136
8535
|
} catch {
|
|
8137
|
-
cliDir =
|
|
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,214 @@ ${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 pick = () => existsSync9(recPath) ? recPath : existsSync9(logPath) ? logPath : null;
|
|
10240
|
+
const chosen = pick();
|
|
10241
|
+
if (!chosen) {
|
|
10242
|
+
process.stderr.write(`
|
|
10243
|
+
${bold("protect-mcp record")}
|
|
10244
|
+
|
|
10245
|
+
No record found in ${dir}.
|
|
10246
|
+
Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run this again.
|
|
10247
|
+
`);
|
|
10248
|
+
process.stderr.write(`Tip: run this in the directory where your gate is signing (where .protect-mcp-receipts.jsonl lives), or pass ${bold("--dir <path>")}.
|
|
10249
|
+
|
|
10250
|
+
`);
|
|
10251
|
+
process.exit(0);
|
|
10252
|
+
return;
|
|
10253
|
+
}
|
|
10254
|
+
const readRecs = (file) => readFileSync11(file, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
10255
|
+
try {
|
|
10256
|
+
return JSON.parse(l);
|
|
10257
|
+
} catch {
|
|
10258
|
+
return null;
|
|
10259
|
+
}
|
|
10260
|
+
}).filter((x) => x !== null).map(mapRecordEntry);
|
|
10261
|
+
const openTarget = (target) => {
|
|
10262
|
+
if (argv.includes("--no-open")) return;
|
|
10263
|
+
const platform = process.platform;
|
|
10264
|
+
const opener = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
10265
|
+
const openArgs = platform === "win32" ? ["/c", "start", "", target] : [target];
|
|
10266
|
+
try {
|
|
10267
|
+
const child = cp.spawn(opener, openArgs, { stdio: "ignore", detached: true });
|
|
10268
|
+
child.unref();
|
|
10269
|
+
} catch {
|
|
10270
|
+
}
|
|
10271
|
+
};
|
|
10272
|
+
if (argv.includes("--live") || argv.includes("--watch")) {
|
|
10273
|
+
const http = await import("http");
|
|
10274
|
+
const pi = argv.indexOf("--port");
|
|
10275
|
+
const port = pi !== -1 && argv[pi + 1] ? parseInt(argv[pi + 1], 10) : 9378;
|
|
10276
|
+
const server = http.createServer((req, res) => {
|
|
10277
|
+
if (req.url && req.url.indexOf("/data") === 0) {
|
|
10278
|
+
let recs2 = [];
|
|
10279
|
+
const f = pick();
|
|
10280
|
+
try {
|
|
10281
|
+
if (f) recs2 = readRecs(f);
|
|
10282
|
+
} catch {
|
|
10283
|
+
}
|
|
10284
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
10285
|
+
res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
|
|
10286
|
+
return;
|
|
10287
|
+
}
|
|
10288
|
+
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
|
|
10289
|
+
const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
|
|
10290
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
10291
|
+
res.end(page);
|
|
10292
|
+
});
|
|
10293
|
+
server.on("error", (e) => {
|
|
10294
|
+
process.stderr.write(`
|
|
10295
|
+
protect-mcp record --live: could not start on port ${port}${e && e.code ? ` (${e.code})` : ""}. Try ${bold("--port <n>")}.
|
|
10296
|
+
|
|
10297
|
+
`);
|
|
10298
|
+
process.exit(1);
|
|
10299
|
+
});
|
|
10300
|
+
server.listen(port, "127.0.0.1", () => {
|
|
10301
|
+
const url = `http://127.0.0.1:${port}`;
|
|
10302
|
+
openTarget(url);
|
|
10303
|
+
process.stdout.write(`
|
|
10304
|
+
${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
|
|
10305
|
+
`);
|
|
10306
|
+
process.stdout.write(` Updates as your agent runs. All local, nothing uploaded. ${dim("Ctrl-C to stop.")}
|
|
10307
|
+
|
|
10308
|
+
`);
|
|
10309
|
+
});
|
|
10310
|
+
return;
|
|
10311
|
+
}
|
|
10312
|
+
const recs = readRecs(chosen);
|
|
10313
|
+
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
|
|
10314
|
+
const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
|
|
10315
|
+
const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
|
|
10316
|
+
writeFileSync4(out, html);
|
|
10317
|
+
openTarget(out);
|
|
10318
|
+
process.stdout.write(`
|
|
10319
|
+
${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} ${recs.length} decision${recs.length === 1 ? "" : "s"}, all on this machine
|
|
10320
|
+
`);
|
|
10321
|
+
if (!meta.signed) process.stdout.write(` ${dim("(decision log; signed receipts appear in .protect-mcp-receipts.jsonl once signing is on)")}
|
|
10322
|
+
`);
|
|
10323
|
+
const fileUrl = "file://" + encodeURI(out);
|
|
10324
|
+
if (process.stdout.isTTY) {
|
|
10325
|
+
process.stdout.write(` Opened in your browser. If it did not open, click: \x1B]8;;${fileUrl}\x1B\\${bold("your record")}\x1B]8;;\x1B\\
|
|
10326
|
+
`);
|
|
10327
|
+
} else {
|
|
10328
|
+
process.stdout.write(` Opened in your browser. If it did not open, open: ${out}
|
|
10329
|
+
`);
|
|
10330
|
+
}
|
|
10331
|
+
process.stdout.write(` ${dim("Want it to update live as your agent runs? npx protect-mcp record --live")}
|
|
10332
|
+
|
|
10333
|
+
`);
|
|
10334
|
+
process.exit(0);
|
|
10335
|
+
}
|
|
10336
|
+
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>
|
|
10337
|
+
<style>
|
|
10338
|
+
:root{--paper:#f6f4ef;--ink:#1b1815;--soft:#524d46;--faint:#8a837a;--line:#ddd7c9;--g:#3f6146;--gb:#e7eee3;--a:#8f6216;--ab:#f2e8d3;--r:#7d3535;--rb:#f2e0dc}
|
|
10339
|
+
*{box-sizing:border-box}
|
|
10340
|
+
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}
|
|
10341
|
+
.wrap{max-width:1000px;margin:0 auto;padding:26px 22px 60px}
|
|
10342
|
+
h1{font-size:24px;margin:0 0 4px;letter-spacing:-.012em}
|
|
10343
|
+
.meta{color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
10344
|
+
.bar{margin:18px 0 12px}
|
|
10345
|
+
input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit}
|
|
10346
|
+
.chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
|
10347
|
+
.chip{cursor:pointer;font-size:12px;padding:3px 10px;border-radius:100px;border:1px solid var(--line);background:#fff;color:var(--soft)}
|
|
10348
|
+
.chip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
|
|
10349
|
+
.count{color:var(--faint);font-size:12px;font-family:ui-monospace,Menlo,monospace;margin-bottom:8px}
|
|
10350
|
+
.row{border:1px solid var(--line);border-radius:9px;background:#fcfbf7;padding:11px 13px;margin-bottom:8px;cursor:pointer}
|
|
10351
|
+
.top{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
|
|
10352
|
+
.pill{font-size:11px;font-weight:600;padding:2px 9px;border-radius:100px}
|
|
10353
|
+
.pill.allowed{background:var(--gb);color:var(--g)}.pill.held{background:var(--ab);color:var(--a)}.pill.blocked{background:var(--rb);color:var(--r)}
|
|
10354
|
+
.tag{font-size:11px;padding:1px 7px;border-radius:100px;background:var(--paper);border:1px solid var(--line);color:var(--faint)}
|
|
10355
|
+
.when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
10356
|
+
.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}
|
|
10357
|
+
.row.open .det{display:block}
|
|
10358
|
+
.foot{margin-top:22px;color:var(--faint);font-size:12px;line-height:1.6;border-top:1px solid var(--line);padding-top:14px}
|
|
10359
|
+
.foot b{color:var(--soft)}
|
|
10360
|
+
</style></head><body><div class="wrap">
|
|
10361
|
+
<h1>Your record</h1>
|
|
10362
|
+
<div class="meta" id="meta"></div>
|
|
10363
|
+
<div class="bar"><input id="q" placeholder="Search your record: tool, reason, verdict"></div>
|
|
10364
|
+
<div class="chips" id="chips"></div>
|
|
10365
|
+
<div class="count" id="count"></div>
|
|
10366
|
+
<div id="list"></div>
|
|
10367
|
+
<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>
|
|
10368
|
+
</div>
|
|
10369
|
+
<script>
|
|
10370
|
+
var RECORDS=__DATA__;var META=__META__;var Q="",ACT={};
|
|
10371
|
+
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
10372
|
+
function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
|
|
10373
|
+
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"})}
|
|
10374
|
+
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]]})}
|
|
10375
|
+
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}
|
|
10376
|
+
function render(){
|
|
10377
|
+
document.getElementById("meta").textContent=META.count+" decisions from "+META.file+(META.signed?" (signed)":" (decision log)")+" - all local"+(META.live?" \xB7 live, updating":"");
|
|
10378
|
+
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>"})});
|
|
10379
|
+
document.getElementById("chips").innerHTML=chips;
|
|
10380
|
+
var rows=RECORDS.filter(match);
|
|
10381
|
+
document.getElementById("count").textContent=rows.length+" of "+RECORDS.length+" records";
|
|
10382
|
+
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>"});
|
|
10383
|
+
document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>'}
|
|
10384
|
+
document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
|
|
10385
|
+
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()});
|
|
10386
|
+
document.getElementById("list").addEventListener("click",function(e){var row=e.target.closest(".row");if(row)row.classList.toggle("open")});
|
|
10387
|
+
render();
|
|
10388
|
+
if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){RECORDS=d.recs||[];META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;render()}).catch(function(){})};poll();setInterval(poll,2000);}
|
|
10389
|
+
</script></body></html>`;
|
|
9783
10390
|
async function handleBundle(argv) {
|
|
9784
10391
|
const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
|
|
9785
10392
|
const { join: join8 } = await import("path");
|
|
@@ -9901,7 +10508,7 @@ ${bold("protect-mcp connect")}
|
|
|
9901
10508
|
`));
|
|
9902
10509
|
process.stderr.write(` Receipts will be uploaded automatically.
|
|
9903
10510
|
`);
|
|
9904
|
-
process.stderr.write(dim(` Free tier: 20,000 receipts/month
|
|
10511
|
+
process.stderr.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
|
|
9905
10512
|
`));
|
|
9906
10513
|
process.stderr.write(`
|
|
9907
10514
|
${"\u2500".repeat(50)}
|
|
@@ -9999,7 +10606,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
9999
10606
|
`));
|
|
10000
10607
|
process.stdout.write(` Receipts will be uploaded automatically.
|
|
10001
10608
|
`);
|
|
10002
|
-
process.stdout.write(dim(` Free tier: 20,000 receipts/month
|
|
10609
|
+
process.stdout.write(dim(` Free tier: 20,000 receipts/month, no credit card required.
|
|
10003
10610
|
`));
|
|
10004
10611
|
process.stdout.write(`
|
|
10005
10612
|
`);
|
|
@@ -10748,7 +11355,7 @@ ${bold("protect-mcp trace")}
|
|
|
10748
11355
|
process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
|
|
10749
11356
|
`);
|
|
10750
11357
|
if (rendered.has(id)) {
|
|
10751
|
-
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle
|
|
11358
|
+
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle: already rendered)")}
|
|
10752
11359
|
`);
|
|
10753
11360
|
return;
|
|
10754
11361
|
}
|
|
@@ -10951,7 +11558,7 @@ ${bold("protect-mcp init-hooks")}
|
|
|
10951
11558
|
|
|
10952
11559
|
`);
|
|
10953
11560
|
} catch {
|
|
10954
|
-
process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys
|
|
11561
|
+
process.stdout.write(` ${yellow("\u26A0")} Could not generate Ed25519 keys, signing disabled
|
|
10955
11562
|
|
|
10956
11563
|
`);
|
|
10957
11564
|
}
|
|
@@ -11022,13 +11629,14 @@ ${bold("protect-mcp init-hooks")}
|
|
|
11022
11629
|
process.stdout.write(` Every tool call will be receipted automatically.
|
|
11023
11630
|
|
|
11024
11631
|
`);
|
|
11025
|
-
process.stdout.write(` 3.
|
|
11632
|
+
process.stdout.write(` 3. See your record: a searchable view of every decision.
|
|
11026
11633
|
`);
|
|
11027
|
-
process.stdout.write(` ${dim(`
|
|
11634
|
+
process.stdout.write(` ${dim(`npx protect-mcp record`)}
|
|
11028
11635
|
`);
|
|
11029
|
-
process.stdout.write(` ${dim(`
|
|
11636
|
+
process.stdout.write(` ${dim(`Everything stays on this machine. Nothing is uploaded.`)}
|
|
11637
|
+
|
|
11030
11638
|
`);
|
|
11031
|
-
process.stdout.write(`
|
|
11639
|
+
process.stdout.write(` Prefer the terminal? ${dim(`npx protect-mcp receipts`)}, or ${dim("/verify-receipt")} in Claude Code.
|
|
11032
11640
|
|
|
11033
11641
|
`);
|
|
11034
11642
|
process.stdout.write(` 4. View policy suggestions:
|
|
@@ -11038,9 +11646,9 @@ ${bold("protect-mcp init-hooks")}
|
|
|
11038
11646
|
`);
|
|
11039
11647
|
process.stdout.write(`${bold("Key facts:")}
|
|
11040
11648
|
`);
|
|
11041
|
-
process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}
|
|
11649
|
+
process.stdout.write(` \u2022 deny decisions are ${bold("AUTHORITATIVE")}: they cannot be overridden
|
|
11042
11650
|
`);
|
|
11043
|
-
process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}
|
|
11651
|
+
process.stdout.write(` \u2022 PostToolUse runs ${bold("async")}, so there is zero latency impact on tool execution
|
|
11044
11652
|
`);
|
|
11045
11653
|
process.stdout.write(` \u2022 Receipts are Ed25519-signed and append-only
|
|
11046
11654
|
`);
|
|
@@ -11051,7 +11659,7 @@ ${bold("protect-mcp init-hooks")}
|
|
|
11051
11659
|
async function sendInstallTelemetry() {
|
|
11052
11660
|
try {
|
|
11053
11661
|
const { existsSync: existsSync9, mkdirSync: mkdirSync3, writeFileSync: writeFileSync4, readFileSync: readFileSync11 } = await import("fs");
|
|
11054
|
-
const { join: join8, dirname:
|
|
11662
|
+
const { join: join8, dirname: dirname3 } = await import("path");
|
|
11055
11663
|
const { homedir } = await import("os");
|
|
11056
11664
|
const { fileURLToPath } = await import("url");
|
|
11057
11665
|
const markerDir = join8(homedir(), ".protect-mcp");
|
|
@@ -11059,15 +11667,7 @@ async function sendInstallTelemetry() {
|
|
|
11059
11667
|
if (existsSync9(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
|
|
11060
11668
|
return;
|
|
11061
11669
|
}
|
|
11062
|
-
|
|
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
|
-
}
|
|
11670
|
+
const version = await pkgVersion();
|
|
11071
11671
|
const controller = new AbortController();
|
|
11072
11672
|
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
11073
11673
|
fetch("https://api.scopeblind.com/telemetry/install", {
|
|
@@ -11241,6 +11841,7 @@ async function main() {
|
|
|
11241
11841
|
sendInstallTelemetry().catch(() => {
|
|
11242
11842
|
});
|
|
11243
11843
|
const args = process.argv.slice(2);
|
|
11844
|
+
process.env.PROTECT_MCP_VERSION = process.env.PROTECT_MCP_VERSION || await pkgVersion();
|
|
11244
11845
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
11245
11846
|
printHelp();
|
|
11246
11847
|
process.exit(0);
|
|
@@ -11279,6 +11880,10 @@ async function main() {
|
|
|
11279
11880
|
await startHookServer2({ port, policyPath: policyPath2, cedarDir: cedarDir2, enforce: enforce2, verbose: verbose2 });
|
|
11280
11881
|
return;
|
|
11281
11882
|
}
|
|
11883
|
+
if (args[0] === "record") {
|
|
11884
|
+
await handleRecord(args.slice(1));
|
|
11885
|
+
return;
|
|
11886
|
+
}
|
|
11282
11887
|
if (args[0] === "init-hooks") {
|
|
11283
11888
|
await handleInitHooks(args.slice(1));
|
|
11284
11889
|
process.exit(0);
|
|
@@ -11536,7 +12141,7 @@ async function handleDoctor() {
|
|
|
11536
12141
|
process.stdout.write(green2(`Node.js ${nodeVersion}
|
|
11537
12142
|
`));
|
|
11538
12143
|
} else {
|
|
11539
|
-
process.stdout.write(red2(`Node.js ${nodeVersion}
|
|
12144
|
+
process.stdout.write(red2(`Node.js ${nodeVersion}, requires >= 18
|
|
11540
12145
|
`));
|
|
11541
12146
|
issues++;
|
|
11542
12147
|
}
|
|
@@ -11547,7 +12152,7 @@ async function handleDoctor() {
|
|
|
11547
12152
|
if (config.signing?.private_key || config.signing?.key_file) {
|
|
11548
12153
|
process.stdout.write(green2("Signing keys configured\n"));
|
|
11549
12154
|
} else {
|
|
11550
|
-
process.stdout.write(yellow2("Config found but no signing keys
|
|
12155
|
+
process.stdout.write(yellow2("Config found but no signing keys. Run: protect-mcp init\n"));
|
|
11551
12156
|
issues++;
|
|
11552
12157
|
}
|
|
11553
12158
|
} catch {
|
|
@@ -11555,7 +12160,7 @@ async function handleDoctor() {
|
|
|
11555
12160
|
issues++;
|
|
11556
12161
|
}
|
|
11557
12162
|
} else {
|
|
11558
|
-
process.stdout.write(yellow2("No scopeblind.config.json
|
|
12163
|
+
process.stdout.write(yellow2("No scopeblind.config.json. Run: protect-mcp init\n"));
|
|
11559
12164
|
}
|
|
11560
12165
|
let policyFound = false;
|
|
11561
12166
|
for (const dir of ["cedar", "policies", "."]) {
|
|
@@ -11580,14 +12185,14 @@ async function handleDoctor() {
|
|
|
11580
12185
|
}
|
|
11581
12186
|
}
|
|
11582
12187
|
if (!policyFound) {
|
|
11583
|
-
process.stdout.write(yellow2("No policy files found
|
|
12188
|
+
process.stdout.write(yellow2("No policy files found, running in shadow mode (allow all)\n"));
|
|
11584
12189
|
}
|
|
11585
12190
|
try {
|
|
11586
12191
|
const cedarAvailable = await isCedarAvailable();
|
|
11587
12192
|
if (cedarAvailable) {
|
|
11588
12193
|
process.stdout.write(green2("Cedar WASM engine available\n"));
|
|
11589
12194
|
} else {
|
|
11590
|
-
process.stdout.write(dim2(" Cedar WASM not installed
|
|
12195
|
+
process.stdout.write(dim2(" Cedar WASM not installed. Install: npm install @cedar-policy/cedar-wasm\n"));
|
|
11591
12196
|
}
|
|
11592
12197
|
} catch {
|
|
11593
12198
|
process.stdout.write(dim2(" Cedar WASM not installed\n"));
|
|
@@ -11603,7 +12208,7 @@ async function handleDoctor() {
|
|
|
11603
12208
|
process.stdout.write(green2("Decision log exists\n"));
|
|
11604
12209
|
}
|
|
11605
12210
|
} else {
|
|
11606
|
-
process.stdout.write(dim2(" No decision log yet
|
|
12211
|
+
process.stdout.write(dim2(" No decision log yet, will be created on first tool call\n"));
|
|
11607
12212
|
}
|
|
11608
12213
|
if (existsSync9(receiptFile)) {
|
|
11609
12214
|
try {
|
|
@@ -11618,17 +12223,17 @@ async function handleDoctor() {
|
|
|
11618
12223
|
execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
|
|
11619
12224
|
process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
|
|
11620
12225
|
} catch {
|
|
11621
|
-
process.stdout.write(dim2(" Verifier not cached
|
|
12226
|
+
process.stdout.write(dim2(" Verifier not cached. Install: npm install -g @veritasacta/verify\n"));
|
|
11622
12227
|
}
|
|
11623
12228
|
try {
|
|
11624
12229
|
const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
|
|
11625
12230
|
if (res.ok) {
|
|
11626
12231
|
process.stdout.write(green2("ScopeBlind API reachable\n"));
|
|
11627
12232
|
} else {
|
|
11628
|
-
process.stdout.write(yellow2("ScopeBlind API returned non-200
|
|
12233
|
+
process.stdout.write(yellow2("ScopeBlind API returned non-200, receipts will be stored locally\n"));
|
|
11629
12234
|
}
|
|
11630
12235
|
} catch {
|
|
11631
|
-
process.stdout.write(dim2(" ScopeBlind API not reachable
|
|
12236
|
+
process.stdout.write(dim2(" ScopeBlind API not reachable, offline mode (receipts stored locally)\n"));
|
|
11632
12237
|
}
|
|
11633
12238
|
process.stdout.write("\nRestraint self-test:\n");
|
|
11634
12239
|
try {
|