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/{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 +597 -44
- package/dist/cli.mjs +195 -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
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
loadPolicy,
|
|
13
13
|
parseRateLimit,
|
|
14
14
|
signDecision
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-WIPWNWMJ.mjs";
|
|
16
16
|
|
|
17
17
|
// src/hook-server.ts
|
|
18
18
|
import { createServer } from "http";
|
|
@@ -829,7 +829,7 @@ async function startHookServer(options = {}) {
|
|
|
829
829
|
res.end(JSON.stringify({
|
|
830
830
|
status: "ok",
|
|
831
831
|
server: "protect-mcp-hooks",
|
|
832
|
-
version:
|
|
832
|
+
version: process.env.PROTECT_MCP_VERSION || "unknown",
|
|
833
833
|
uptime_ms: Date.now() - state.startTime,
|
|
834
834
|
mode: enforce ? "enforce" : "shadow",
|
|
835
835
|
policy_digest: policyDigest,
|
|
@@ -916,9 +916,10 @@ async function startHookServer(options = {}) {
|
|
|
916
916
|
const pad = (s, n = 46) => s.padEnd(n);
|
|
917
917
|
w(`
|
|
918
918
|
`);
|
|
919
|
-
w(` protect-mcp
|
|
919
|
+
w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
|
|
920
|
+
` : ` protect-mcp
|
|
920
921
|
`);
|
|
921
|
-
w(` ScopeBlind \
|
|
922
|
+
w(` ScopeBlind \xB7 https://scopeblind.com
|
|
922
923
|
`);
|
|
923
924
|
w(`
|
|
924
925
|
`);
|
|
@@ -946,7 +947,13 @@ async function startHookServer(options = {}) {
|
|
|
946
947
|
`);
|
|
947
948
|
w(`
|
|
948
949
|
`);
|
|
949
|
-
w(` deny is authoritative
|
|
950
|
+
w(` deny is authoritative: it cannot be overridden.
|
|
951
|
+
`);
|
|
952
|
+
w(`
|
|
953
|
+
`);
|
|
954
|
+
w(` See your record npx protect-mcp record
|
|
955
|
+
`);
|
|
956
|
+
w(` a searchable view of every decision, all on this machine
|
|
950
957
|
`);
|
|
951
958
|
w(`
|
|
952
959
|
`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
meetsMinTier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VTPZ4G5I.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-WIPWNWMJ.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
|
@@ -331,12 +331,298 @@ function policyPackIds() {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
// src/connector-pilots.ts
|
|
334
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
335
|
-
import { join } from "path";
|
|
334
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
335
|
+
import { dirname, join, normalize } from "path";
|
|
336
336
|
var defaultPermit2 = `
|
|
337
337
|
// Default posture: observe all non-matching tools so the connector can be piloted in shadow mode.
|
|
338
338
|
permit(principal, action == Action::"MCP::Tool::call", resource);
|
|
339
339
|
`;
|
|
340
|
+
var nautilusBridgePy = String.raw`#!/usr/bin/env python3
|
|
341
|
+
"""
|
|
342
|
+
ScopeBlind external bridge for NautilusTrader-compatible pilots.
|
|
343
|
+
|
|
344
|
+
This file is intentionally outside NautilusTrader. It gives protect-mcp a stable
|
|
345
|
+
JSONL command boundary for staging, approval-gated submission, cancellation, and
|
|
346
|
+
event export while keeping the trading engine customer-owned.
|
|
347
|
+
|
|
348
|
+
Mock mode runs without NautilusTrader installed. Real mode is enabled by setting
|
|
349
|
+
NAUTILUS_BRIDGE_MODULE to "module.path:ClassName"; the class may implement:
|
|
350
|
+
submit_order(order), modify_order(order), cancel_order(order), reconcile(order),
|
|
351
|
+
export_events(since=None)
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
from __future__ import annotations
|
|
355
|
+
|
|
356
|
+
import hashlib
|
|
357
|
+
import importlib
|
|
358
|
+
import json
|
|
359
|
+
import os
|
|
360
|
+
import sys
|
|
361
|
+
import time
|
|
362
|
+
from dataclasses import dataclass, field
|
|
363
|
+
from pathlib import Path
|
|
364
|
+
from typing import Any, Callable
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def canonical_json(value: Any) -> str:
|
|
368
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def sha256_json(value: Any) -> str:
|
|
372
|
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def now_ms() -> int:
|
|
376
|
+
return int(time.time() * 1000)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass
|
|
380
|
+
class BridgeState:
|
|
381
|
+
root: Path = field(default_factory=lambda: Path(os.environ.get("SCOPEBLIND_NAUTILUS_STATE_DIR", ".protect-mcp/nautilus")))
|
|
382
|
+
|
|
383
|
+
def __post_init__(self) -> None:
|
|
384
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
self.orders_path.touch(exist_ok=True)
|
|
386
|
+
self.events_path.touch(exist_ok=True)
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def orders_path(self) -> Path:
|
|
390
|
+
return self.root / "orders.jsonl"
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def events_path(self) -> Path:
|
|
394
|
+
return self.root / "events.jsonl"
|
|
395
|
+
|
|
396
|
+
def append_order(self, order: dict[str, Any]) -> None:
|
|
397
|
+
with self.orders_path.open("a", encoding="utf-8") as handle:
|
|
398
|
+
handle.write(canonical_json(order) + "\n")
|
|
399
|
+
|
|
400
|
+
def append_event(self, event: dict[str, Any]) -> dict[str, Any]:
|
|
401
|
+
enriched = {
|
|
402
|
+
"event_id": event.get("event_id") or f"nt-{now_ms()}-{len(event)}",
|
|
403
|
+
"observed_at_ms": now_ms(),
|
|
404
|
+
**event,
|
|
405
|
+
}
|
|
406
|
+
enriched["event_digest"] = sha256_json(enriched)
|
|
407
|
+
with self.events_path.open("a", encoding="utf-8") as handle:
|
|
408
|
+
handle.write(canonical_json(enriched) + "\n")
|
|
409
|
+
return enriched
|
|
410
|
+
|
|
411
|
+
def events(self) -> list[dict[str, Any]]:
|
|
412
|
+
rows: list[dict[str, Any]] = []
|
|
413
|
+
with self.events_path.open("r", encoding="utf-8") as handle:
|
|
414
|
+
for line in handle:
|
|
415
|
+
if line.strip():
|
|
416
|
+
rows.append(json.loads(line))
|
|
417
|
+
return rows
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class ScopeBlindNautilusBridge:
|
|
421
|
+
def __init__(self) -> None:
|
|
422
|
+
self.state = BridgeState()
|
|
423
|
+
self.real = self._load_real_bridge()
|
|
424
|
+
|
|
425
|
+
def _load_real_bridge(self) -> Any | None:
|
|
426
|
+
target = os.environ.get("NAUTILUS_BRIDGE_MODULE")
|
|
427
|
+
if not target:
|
|
428
|
+
return None
|
|
429
|
+
module_name, _, class_name = target.partition(":")
|
|
430
|
+
if not module_name or not class_name:
|
|
431
|
+
raise ValueError("NAUTILUS_BRIDGE_MODULE must be module.path:ClassName")
|
|
432
|
+
module = importlib.import_module(module_name)
|
|
433
|
+
return getattr(module, class_name)()
|
|
434
|
+
|
|
435
|
+
def handle(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
436
|
+
action = command.get("action")
|
|
437
|
+
handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
438
|
+
"stage_order": self.stage_order,
|
|
439
|
+
"submit_order": self.submit_order,
|
|
440
|
+
"modify_order": self.modify_order,
|
|
441
|
+
"cancel_order": self.cancel_order,
|
|
442
|
+
"reconcile": self.reconcile,
|
|
443
|
+
"export_events": self.export_events,
|
|
444
|
+
}
|
|
445
|
+
if action not in handlers:
|
|
446
|
+
return self.error(command, "unknown_action", f"Unsupported action: {action}")
|
|
447
|
+
try:
|
|
448
|
+
return handlers[action](command)
|
|
449
|
+
except Exception as exc:
|
|
450
|
+
return self.error(command, "bridge_error", str(exc))
|
|
451
|
+
|
|
452
|
+
def require(self, command: dict[str, Any], *fields: str) -> None:
|
|
453
|
+
missing = [field for field in fields if command.get(field) in (None, "")]
|
|
454
|
+
if missing:
|
|
455
|
+
raise ValueError(f"missing required field(s): {', '.join(missing)}")
|
|
456
|
+
|
|
457
|
+
def require_approved(self, command: dict[str, Any]) -> None:
|
|
458
|
+
self.require(command, "approval_receipt")
|
|
459
|
+
if command.get("mandate_passed") is not True:
|
|
460
|
+
raise ValueError("mandate_passed must be true before live order mutation")
|
|
461
|
+
|
|
462
|
+
def stage_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
463
|
+
self.require(command, "client_order_id", "instrument_id", "side", "quantity")
|
|
464
|
+
order = self.order_projection(command, status="staged")
|
|
465
|
+
self.state.append_order(order)
|
|
466
|
+
event = self.state.append_event({
|
|
467
|
+
"type": "scopeblind.nautilus.order_staged.v1",
|
|
468
|
+
"client_order_id": order["client_order_id"],
|
|
469
|
+
"order_digest": sha256_json(order),
|
|
470
|
+
"disclosure": "position_blind",
|
|
471
|
+
})
|
|
472
|
+
return self.ok(command, {"status": "staged", "order": order, "event": event})
|
|
473
|
+
|
|
474
|
+
def submit_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
475
|
+
self.require_approved(command)
|
|
476
|
+
order = self.order_projection(command, status="submitted")
|
|
477
|
+
if self.real and hasattr(self.real, "submit_order"):
|
|
478
|
+
external = self.real.submit_order(order)
|
|
479
|
+
else:
|
|
480
|
+
external = {"mode": "mock", "external_order_id": f"MOCK-{order['client_order_id']}"}
|
|
481
|
+
event = self.state.append_event({
|
|
482
|
+
"type": "scopeblind.nautilus.order_submitted.v1",
|
|
483
|
+
"client_order_id": order["client_order_id"],
|
|
484
|
+
"order_digest": sha256_json(order),
|
|
485
|
+
"external_digest": sha256_json(external),
|
|
486
|
+
"disclosure": "position_blind",
|
|
487
|
+
})
|
|
488
|
+
return self.ok(command, {"status": "submitted", "order": order, "external": external, "event": event})
|
|
489
|
+
|
|
490
|
+
def modify_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
491
|
+
self.require_approved(command)
|
|
492
|
+
self.require(command, "client_order_id")
|
|
493
|
+
if self.real and hasattr(self.real, "modify_order"):
|
|
494
|
+
external = self.real.modify_order(command)
|
|
495
|
+
else:
|
|
496
|
+
external = {"mode": "mock", "modified": command["client_order_id"]}
|
|
497
|
+
event = self.state.append_event({
|
|
498
|
+
"type": "scopeblind.nautilus.order_modified.v1",
|
|
499
|
+
"client_order_id": command["client_order_id"],
|
|
500
|
+
"command_digest": sha256_json(command),
|
|
501
|
+
"external_digest": sha256_json(external),
|
|
502
|
+
"disclosure": "position_blind",
|
|
503
|
+
})
|
|
504
|
+
return self.ok(command, {"status": "modified", "external": external, "event": event})
|
|
505
|
+
|
|
506
|
+
def cancel_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
507
|
+
self.require_approved(command)
|
|
508
|
+
self.require(command, "client_order_id")
|
|
509
|
+
if self.real and hasattr(self.real, "cancel_order"):
|
|
510
|
+
external = self.real.cancel_order(command)
|
|
511
|
+
else:
|
|
512
|
+
external = {"mode": "mock", "cancelled": command["client_order_id"]}
|
|
513
|
+
event = self.state.append_event({
|
|
514
|
+
"type": "scopeblind.nautilus.order_cancelled.v1",
|
|
515
|
+
"client_order_id": command["client_order_id"],
|
|
516
|
+
"command_digest": sha256_json(command),
|
|
517
|
+
"external_digest": sha256_json(external),
|
|
518
|
+
"disclosure": "position_blind",
|
|
519
|
+
})
|
|
520
|
+
return self.ok(command, {"status": "cancelled", "external": external, "event": event})
|
|
521
|
+
|
|
522
|
+
def reconcile(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
523
|
+
self.require(command, "client_order_id")
|
|
524
|
+
if self.real and hasattr(self.real, "reconcile"):
|
|
525
|
+
external = self.real.reconcile(command)
|
|
526
|
+
else:
|
|
527
|
+
external = {"mode": "mock", "client_order_id": command["client_order_id"], "state": "accepted"}
|
|
528
|
+
event = self.state.append_event({
|
|
529
|
+
"type": "scopeblind.nautilus.reconciled.v1",
|
|
530
|
+
"client_order_id": command["client_order_id"],
|
|
531
|
+
"external_digest": sha256_json(external),
|
|
532
|
+
"disclosure": "position_blind",
|
|
533
|
+
})
|
|
534
|
+
return self.ok(command, {"status": "reconciled", "external": external, "event": event})
|
|
535
|
+
|
|
536
|
+
def export_events(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
537
|
+
if self.real and hasattr(self.real, "export_events"):
|
|
538
|
+
external_events = self.real.export_events(command.get("since"))
|
|
539
|
+
else:
|
|
540
|
+
external_events = self.state.events()
|
|
541
|
+
return self.ok(command, {
|
|
542
|
+
"status": "exported",
|
|
543
|
+
"event_count": len(external_events),
|
|
544
|
+
"commitment_root": sha256_json(external_events),
|
|
545
|
+
"events": external_events,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
def order_projection(self, command: dict[str, Any], status: str) -> dict[str, Any]:
|
|
549
|
+
return {
|
|
550
|
+
"client_order_id": command["client_order_id"],
|
|
551
|
+
"instrument_id": command["instrument_id"],
|
|
552
|
+
"side": command["side"],
|
|
553
|
+
"quantity": command["quantity"],
|
|
554
|
+
"price": command.get("price"),
|
|
555
|
+
"time_in_force": command.get("time_in_force", "GTC"),
|
|
556
|
+
"strategy_id": command.get("strategy_id"),
|
|
557
|
+
"mandate_digest": command.get("mandate_digest"),
|
|
558
|
+
"approval_receipt": command.get("approval_receipt"),
|
|
559
|
+
"status": status,
|
|
560
|
+
"created_at_ms": now_ms(),
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
def ok(self, command: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
|
|
564
|
+
return {
|
|
565
|
+
"ok": True,
|
|
566
|
+
"bridge": "scopeblind.nautilus.external.v1",
|
|
567
|
+
"mode": "real" if self.real else "mock",
|
|
568
|
+
"request_digest": sha256_json(command),
|
|
569
|
+
**result,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
def error(self, command: dict[str, Any], code: str, message: str) -> dict[str, Any]:
|
|
573
|
+
return {
|
|
574
|
+
"ok": False,
|
|
575
|
+
"bridge": "scopeblind.nautilus.external.v1",
|
|
576
|
+
"mode": "real" if self.real else "mock",
|
|
577
|
+
"error": {"code": code, "message": message},
|
|
578
|
+
"request_digest": sha256_json(command),
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def main() -> int:
|
|
583
|
+
bridge = ScopeBlindNautilusBridge()
|
|
584
|
+
for line in sys.stdin:
|
|
585
|
+
if not line.strip():
|
|
586
|
+
continue
|
|
587
|
+
command = json.loads(line)
|
|
588
|
+
print(canonical_json(bridge.handle(command)), flush=True)
|
|
589
|
+
return 0
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
if __name__ == "__main__":
|
|
593
|
+
raise SystemExit(main())
|
|
594
|
+
`;
|
|
595
|
+
var nautilusAdapterReadme = `# NautilusTrader-compatible external bridge
|
|
596
|
+
|
|
597
|
+
This connector is intentionally external to NautilusTrader. It lets protect-mcp
|
|
598
|
+
control and receipt high-risk order actions while a customer-owned Nautilus
|
|
599
|
+
process remains the trading engine.
|
|
600
|
+
|
|
601
|
+
## Local mock run
|
|
602
|
+
|
|
603
|
+
\`\`\`bash
|
|
604
|
+
python3 .protect-mcp/connectors/nautilus-trader/bridge.py <<'JSONL'
|
|
605
|
+
{"action":"stage_order","client_order_id":"SB-1","instrument_id":"AAPL.NASDAQ","side":"BUY","quantity":"50","price":"182.40","mandate_digest":"demo"}
|
|
606
|
+
{"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"}
|
|
607
|
+
{"action":"export_events"}
|
|
608
|
+
JSONL
|
|
609
|
+
\`\`\`
|
|
610
|
+
|
|
611
|
+
## Real mode
|
|
612
|
+
|
|
613
|
+
Set \`NAUTILUS_BRIDGE_MODULE=customer_module:BridgeClass\`. The class can
|
|
614
|
+
implement \`submit_order\`, \`modify_order\`, \`cancel_order\`, \`reconcile\`,
|
|
615
|
+
and \`export_events\`. Keep that glue in the customer's repository so Nautilus
|
|
616
|
+
licensing, credentials, and trading logic stay outside ScopeBlind.
|
|
617
|
+
|
|
618
|
+
## Upstream contribution posture
|
|
619
|
+
|
|
620
|
+
The best NautilusTrader contribution is not this bridge or a UI. It is a small,
|
|
621
|
+
vendor-neutral audit/event sink RFC: a documented way to export normalized order
|
|
622
|
+
commands, execution reports, fills, cancels, and reconciliation events so
|
|
623
|
+
external compliance wrappers can prove what happened without mutating the
|
|
624
|
+
engine.
|
|
625
|
+
`;
|
|
340
626
|
var CONNECTOR_PILOTS = [
|
|
341
627
|
{
|
|
342
628
|
id: "github",
|
|
@@ -540,6 +826,93 @@ when { ["pms.order.stage", "pms.order.book", "pms.order.cancel"].contains(contex
|
|
|
540
826
|
|
|
541
827
|
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
542
828
|
when { context.tool == "pms.order.book" && context.mandate_passed != true };
|
|
829
|
+
`
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
id: "nautilus-trader",
|
|
833
|
+
category: "finance",
|
|
834
|
+
name: "NautilusTrader-compatible external bridge",
|
|
835
|
+
status: "usable-pilot",
|
|
836
|
+
description: "Controls NautilusTrader-compatible staged orders through an external JSONL bridge, with local mock mode and customer-owned real mode.",
|
|
837
|
+
value: "Turns Nautilus into a strong Legate demo target: mandate-check, exact approval, external order event, position-blind audit bundle, and later reconciliation.",
|
|
838
|
+
env: [
|
|
839
|
+
{ name: "NAUTILUS_BRIDGE_MODULE", required: false, description: "Optional customer glue in module.path:ClassName form for real Nautilus submission." },
|
|
840
|
+
{ name: "SCOPEBLIND_NAUTILUS_STATE_DIR", required: false, description: "Optional state directory for local mock events. Defaults to .protect-mcp/nautilus." },
|
|
841
|
+
{ name: "NAUTILUS_TRADER_PROJECT", required: false, description: "Optional path to the customer Nautilus project when running real mode." }
|
|
842
|
+
],
|
|
843
|
+
tools: [
|
|
844
|
+
"nautilus.order.stage",
|
|
845
|
+
"nautilus.order.submit",
|
|
846
|
+
"nautilus.order.modify",
|
|
847
|
+
"nautilus.order.cancel",
|
|
848
|
+
"nautilus.strategy.deploy",
|
|
849
|
+
"nautilus.event.export",
|
|
850
|
+
"nautilus.reconcile"
|
|
851
|
+
],
|
|
852
|
+
actions: [
|
|
853
|
+
{ name: "Stage order", tool: "nautilus.order.stage", risk: "medium", mode: "require_approval", description: "Creates a position-blind booking intent and event commitment." },
|
|
854
|
+
{ name: "Submit order", tool: "nautilus.order.submit", risk: "high", mode: "require_approval", description: "Requires mandate pass plus exact approval before live order mutation." },
|
|
855
|
+
{ 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." },
|
|
856
|
+
{ name: "Deploy strategy", tool: "nautilus.strategy.deploy", risk: "high", mode: "require_approval", description: "Requires signed strategy pack, mandate scope, and operator approval." },
|
|
857
|
+
{ name: "Export event log", tool: "nautilus.event.export", risk: "low", mode: "observe", description: "Exports normalized event commitments for receipt corroboration." }
|
|
858
|
+
],
|
|
859
|
+
setup: [
|
|
860
|
+
"Run mock mode first: protect-mcp connectors init nautilus-trader --force.",
|
|
861
|
+
"Pipe stage/submit/reconcile JSONL through .protect-mcp/connectors/nautilus-trader/bridge.py.",
|
|
862
|
+
"For real mode, set NAUTILUS_BRIDGE_MODULE to customer-owned glue that calls NautilusTrader APIs.",
|
|
863
|
+
"Open an upstream NautilusTrader RFC for a neutral audit/event sink before proposing any PR."
|
|
864
|
+
],
|
|
865
|
+
config: {
|
|
866
|
+
type: "scopeblind.connector_pilot.v1",
|
|
867
|
+
provider: "nautilus-trader-compatible",
|
|
868
|
+
mode: "external-bridge-mock-first",
|
|
869
|
+
license_boundary: "No NautilusTrader code is bundled. Real mode calls a customer-owned process/module.",
|
|
870
|
+
adapter_contract: {
|
|
871
|
+
protocol: "stdin/stdout JSONL",
|
|
872
|
+
bridge: ".protect-mcp/connectors/nautilus-trader/bridge.py",
|
|
873
|
+
real_mode_env: "NAUTILUS_BRIDGE_MODULE=module.path:ClassName",
|
|
874
|
+
actions: ["stage_order", "submit_order", "modify_order", "cancel_order", "reconcile", "export_events"]
|
|
875
|
+
},
|
|
876
|
+
controlled_tools: [
|
|
877
|
+
"nautilus.order.stage",
|
|
878
|
+
"nautilus.order.submit",
|
|
879
|
+
"nautilus.order.modify",
|
|
880
|
+
"nautilus.order.cancel",
|
|
881
|
+
"nautilus.strategy.deploy",
|
|
882
|
+
"nautilus.event.export",
|
|
883
|
+
"nautilus.reconcile"
|
|
884
|
+
],
|
|
885
|
+
approval_required_for: ["submit_order", "modify_order", "cancel_order", "strategy_deploy"],
|
|
886
|
+
receipt_fields: [
|
|
887
|
+
"client_order_id",
|
|
888
|
+
"instrument_id_hash",
|
|
889
|
+
"side",
|
|
890
|
+
"quantity",
|
|
891
|
+
"price",
|
|
892
|
+
"mandate_digest",
|
|
893
|
+
"approval_receipt",
|
|
894
|
+
"external_event_digest",
|
|
895
|
+
"commitment_root"
|
|
896
|
+
],
|
|
897
|
+
upstream_rfc: {
|
|
898
|
+
title: "[RFC] Add a vendor-neutral order/execution audit event sink",
|
|
899
|
+
non_goals: ["ScopeBlind dependency", "UI dashboard", "AI tooling", "new venue adapter"]
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
artifacts: [
|
|
903
|
+
{ path: "nautilus-trader/bridge.py", contents: nautilusBridgePy, executable: true },
|
|
904
|
+
{ path: "nautilus-trader/README.md", contents: nautilusAdapterReadme }
|
|
905
|
+
],
|
|
906
|
+
cedar: `${defaultPermit2}
|
|
907
|
+
// NautilusTrader-compatible pilot: stage can be observed, but any live mutation requires exact approval.
|
|
908
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
909
|
+
when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel", "nautilus.strategy.deploy"].contains(context.tool) && !context.approved };
|
|
910
|
+
|
|
911
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
912
|
+
when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel"].contains(context.tool) && context.mandate_passed != true };
|
|
913
|
+
|
|
914
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
915
|
+
when { context.tool == "nautilus.strategy.deploy" && context.strategy_pack_signed != true };
|
|
543
916
|
`
|
|
544
917
|
}
|
|
545
918
|
];
|
|
@@ -571,11 +944,26 @@ function writeConnectorPilots(opts) {
|
|
|
571
944
|
writeFileSync(policyPath, pilot.cedar.endsWith("\n") ? pilot.cedar : `${pilot.cedar}
|
|
572
945
|
`);
|
|
573
946
|
written.push(configPath, policyPath);
|
|
947
|
+
for (const artifact of pilot.artifacts || []) {
|
|
948
|
+
const artifactPath = connectorArtifactPath(directory, artifact.path);
|
|
949
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
950
|
+
writeFileSync(artifactPath, artifact.contents.endsWith("\n") ? artifact.contents : `${artifact.contents}
|
|
951
|
+
`);
|
|
952
|
+
if (artifact.executable) chmodSync(artifactPath, 493);
|
|
953
|
+
written.push(artifactPath);
|
|
954
|
+
}
|
|
574
955
|
}
|
|
575
956
|
writeFileSync(join(directory, "README.md"), renderConnectorReadme(selected));
|
|
576
957
|
written.push(join(directory, "README.md"));
|
|
577
958
|
return { written, pilots: selected, directory };
|
|
578
959
|
}
|
|
960
|
+
function connectorArtifactPath(directory, relativePath) {
|
|
961
|
+
const clean = normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
962
|
+
if (clean.startsWith("/") || clean.includes("..")) {
|
|
963
|
+
throw new Error(`Unsafe connector artifact path: ${relativePath}`);
|
|
964
|
+
}
|
|
965
|
+
return join(directory, clean);
|
|
966
|
+
}
|
|
579
967
|
function readInstalledConnectorPilots(dir) {
|
|
580
968
|
const directory = connectorDirectory(dir);
|
|
581
969
|
if (!existsSync(directory)) return [];
|
|
@@ -609,15 +997,15 @@ function connectorDoctor(dir, env = process.env) {
|
|
|
609
997
|
}));
|
|
610
998
|
const missingRequired = envRows.filter((item) => item.required && !item.present).map((item) => item.name);
|
|
611
999
|
const optionalPresent = envRows.filter((item) => !item.required && item.present).map((item) => item.name);
|
|
612
|
-
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;
|
|
613
|
-
const mockModeReady = pilot.id === "finance-pms";
|
|
1000
|
+
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;
|
|
1001
|
+
const mockModeReady = pilot.id === "finance-pms" || pilot.id === "nautilus-trader";
|
|
614
1002
|
return {
|
|
615
1003
|
id: pilot.id,
|
|
616
1004
|
name: pilot.name,
|
|
617
1005
|
category: pilot.category,
|
|
618
1006
|
installed: installed.has(pilot.id),
|
|
619
1007
|
usable: missingRequired.length === 0 && (pilot.env.some((item) => item.required) || pilot.env.length === 0 || optionalProviderReady || mockModeReady),
|
|
620
|
-
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",
|
|
1008
|
+
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",
|
|
621
1009
|
missing_required: missingRequired,
|
|
622
1010
|
optional_present: optionalPresent,
|
|
623
1011
|
tools: pilot.tools,
|
|
@@ -640,7 +1028,10 @@ Tools: ${pilot.tools.map((tool) => `\`${tool}\``).join(", ")}
|
|
|
640
1028
|
|
|
641
1029
|
Setup:
|
|
642
1030
|
${pilot.setup.map((step) => `- ${step}`).join("\n")}
|
|
643
|
-
`
|
|
1031
|
+
${pilot.artifacts?.length ? `
|
|
1032
|
+
Generated files:
|
|
1033
|
+
${pilot.artifacts.map((artifact) => `- \`${artifact.path}\``).join("\n")}
|
|
1034
|
+
` : ""}`).join("\n")}
|
|
644
1035
|
Next: run \`npx protect-mcp dashboard --open\` and review tool inventory, policy coverage, approvals, and receipts.
|
|
645
1036
|
`;
|
|
646
1037
|
}
|