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.
@@ -12,7 +12,7 @@ import {
12
12
  loadPolicy,
13
13
  parseRateLimit,
14
14
  signDecision
15
- } from "./chunk-D2RDY2JR.mjs";
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: "0.5.0",
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 v0.5.4
919
+ w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
920
+ ` : ` protect-mcp
920
921
  `);
921
- w(` ScopeBlind \u2014 https://scopeblind.com
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 \u2014 cannot be overridden.
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-UBZJ3VI2.mjs";
3
+ } from "./chunk-VTPZ4G5I.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-D2RDY2JR.mjs";
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
- `).join("\n")}
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
  }