protect-mcp 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -618,6 +618,366 @@ var init_external_pdp = __esm({
618
618
  }
619
619
  });
620
620
 
621
+ // src/cedar-evaluator.ts
622
+ async function ensureCedarWasm() {
623
+ if (cedarWasm) return true;
624
+ if (loadAttempted) return false;
625
+ loadAttempted = true;
626
+ try {
627
+ const moduleName = "@cedar-policy/cedar-wasm";
628
+ cedarWasm = await import(
629
+ /* @vite-ignore */
630
+ moduleName
631
+ );
632
+ return true;
633
+ } catch {
634
+ return false;
635
+ }
636
+ }
637
+ function loadCedarPolicies(dirPath) {
638
+ if (!(0, import_node_fs4.existsSync)(dirPath)) {
639
+ throw new Error(`Cedar policy directory not found: ${dirPath}`);
640
+ }
641
+ const entries = (0, import_node_fs4.readdirSync)(dirPath).filter((f) => (0, import_node_path2.extname)(f) === ".cedar").sort();
642
+ if (entries.length === 0) {
643
+ throw new Error(`No .cedar files found in: ${dirPath}`);
644
+ }
645
+ const sources = [];
646
+ for (const file of entries) {
647
+ const content = (0, import_node_fs4.readFileSync)((0, import_node_path2.join)(dirPath, file), "utf-8");
648
+ sources.push(content);
649
+ }
650
+ const concatenated = sources.join("\n\n");
651
+ const digest = (0, import_node_crypto2.createHash)("sha256").update(concatenated).digest("hex").slice(0, 16);
652
+ return {
653
+ source: concatenated,
654
+ digest,
655
+ fileCount: entries.length,
656
+ files: entries
657
+ };
658
+ }
659
+ function buildEntities(req) {
660
+ const agentId = req.agentId || req.tier;
661
+ return [
662
+ {
663
+ uid: { type: "Agent", id: agentId },
664
+ attrs: {
665
+ tier: req.tier,
666
+ ...req.agentId ? { agent_id: req.agentId } : {}
667
+ },
668
+ parents: []
669
+ },
670
+ {
671
+ uid: { type: "Tool", id: req.tool },
672
+ attrs: {},
673
+ parents: []
674
+ }
675
+ ];
676
+ }
677
+ async function evaluateCedar(policySet, req) {
678
+ const available = await ensureCedarWasm();
679
+ if (!available) {
680
+ return {
681
+ allowed: true,
682
+ reason: "cedar_wasm_not_available",
683
+ metadata: { fallback: true }
684
+ };
685
+ }
686
+ try {
687
+ const agentId = req.agentId || req.tier;
688
+ const authRequest = {
689
+ principal: { type: "Agent", id: agentId },
690
+ action: { type: "Action", id: "MCP::Tool::call" },
691
+ resource: { type: "Tool", id: req.tool },
692
+ context: {
693
+ tier: req.tier,
694
+ ...req.context || {}
695
+ }
696
+ };
697
+ const entities = buildEntities(req);
698
+ let result;
699
+ if (typeof cedarWasm.isAuthorized === "function") {
700
+ result = cedarWasm.isAuthorized({
701
+ policies: policySet.source,
702
+ entities,
703
+ principal: authRequest.principal,
704
+ action: authRequest.action,
705
+ resource: authRequest.resource,
706
+ context: authRequest.context,
707
+ schema: null
708
+ // No schema enforcement — Cedar still evaluates correctly
709
+ });
710
+ } else if (typeof cedarWasm.checkAuthorization === "function") {
711
+ result = cedarWasm.checkAuthorization(
712
+ policySet.source,
713
+ JSON.stringify(entities),
714
+ JSON.stringify(authRequest)
715
+ );
716
+ } else {
717
+ const cedarEngine = cedarWasm.default || cedarWasm;
718
+ if (typeof cedarEngine.isAuthorized === "function") {
719
+ result = cedarEngine.isAuthorized({
720
+ policies: policySet.source,
721
+ entities,
722
+ principal: authRequest.principal,
723
+ action: authRequest.action,
724
+ resource: authRequest.resource,
725
+ context: authRequest.context,
726
+ schema: null
727
+ });
728
+ } else {
729
+ return {
730
+ allowed: true,
731
+ reason: "cedar_wasm_api_unsupported",
732
+ metadata: { fallback: true, exports: Object.keys(cedarWasm) }
733
+ };
734
+ }
735
+ }
736
+ const decision = parseWasmResult(result);
737
+ return {
738
+ allowed: decision.allowed,
739
+ reason: decision.allowed ? void 0 : `cedar_deny${decision.diagnostics ? ": " + decision.diagnostics : ""}`,
740
+ metadata: {
741
+ policy_digest: policySet.digest,
742
+ ...decision.matchedPolicies ? { matched_policies: decision.matchedPolicies } : {}
743
+ }
744
+ };
745
+ } catch (err) {
746
+ return {
747
+ allowed: true,
748
+ reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
749
+ metadata: { fallback: true, error: true }
750
+ };
751
+ }
752
+ }
753
+ function parseWasmResult(result) {
754
+ if (!result) {
755
+ return { allowed: true, diagnostics: "null result from Cedar WASM" };
756
+ }
757
+ if (result.type === "allow" || result.type === "Allow") {
758
+ return { allowed: true };
759
+ }
760
+ if (result.type === "deny" || result.type === "Deny") {
761
+ return {
762
+ allowed: false,
763
+ diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
764
+ matchedPolicies: result.diagnostics?.reasons
765
+ };
766
+ }
767
+ if (result.decision === "Allow") {
768
+ return { allowed: true };
769
+ }
770
+ if (result.decision === "Deny") {
771
+ return {
772
+ allowed: false,
773
+ diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
774
+ };
775
+ }
776
+ if (typeof result === "boolean") {
777
+ return { allowed: result };
778
+ }
779
+ return { allowed: true, diagnostics: `unknown result format: ${JSON.stringify(result)}` };
780
+ }
781
+ async function isCedarAvailable() {
782
+ return ensureCedarWasm();
783
+ }
784
+ var import_node_crypto2, import_node_fs4, import_node_path2, cedarWasm, loadAttempted;
785
+ var init_cedar_evaluator = __esm({
786
+ "src/cedar-evaluator.ts"() {
787
+ "use strict";
788
+ import_node_crypto2 = require("crypto");
789
+ import_node_fs4 = require("fs");
790
+ import_node_path2 = require("path");
791
+ cedarWasm = null;
792
+ loadAttempted = false;
793
+ }
794
+ });
795
+
796
+ // src/notifications.ts
797
+ async function sendApprovalNotification(config, notification) {
798
+ const promises = [];
799
+ if (config.sms) {
800
+ promises.push(sendSms(config.sms, notification));
801
+ }
802
+ if (config.webhook) {
803
+ promises.push(sendWebhook(config.webhook, notification));
804
+ }
805
+ if (config.email) {
806
+ promises.push(sendEmail(config.email, notification));
807
+ }
808
+ const results = await Promise.allSettled(promises);
809
+ for (const result of results) {
810
+ if (result.status === "rejected") {
811
+ console.error(`[protect-mcp] Notification failed: ${result.reason}`);
812
+ }
813
+ }
814
+ }
815
+ async function sendSms(config, notification) {
816
+ const body = [
817
+ `\u{1F512} Approval Required`,
818
+ `Tool: ${notification.toolName}`,
819
+ notification.agentId ? `Agent: ${notification.agentId}` : null,
820
+ `Reason: ${notification.reason}`,
821
+ notification.approveUrl ? `Approve: ${notification.approveUrl}` : null,
822
+ notification.traceUrl ? `Trace: ${notification.traceUrl}` : null
823
+ ].filter(Boolean).join("\n");
824
+ const params = new URLSearchParams({
825
+ To: config.to,
826
+ From: config.from,
827
+ Body: body
828
+ });
829
+ const response = await fetch(
830
+ `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
831
+ {
832
+ method: "POST",
833
+ headers: {
834
+ Authorization: `Basic ${Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64")}`,
835
+ "Content-Type": "application/x-www-form-urlencoded"
836
+ },
837
+ body: params.toString()
838
+ }
839
+ );
840
+ if (!response.ok) {
841
+ throw new Error(`Twilio SMS failed: ${response.status} ${await response.text()}`);
842
+ }
843
+ }
844
+ async function sendWebhook(config, notification) {
845
+ let payload;
846
+ if (config.template === "slack") {
847
+ payload = {
848
+ blocks: [
849
+ {
850
+ type: "header",
851
+ text: { type: "plain_text", text: "\u{1F512} Agent Approval Required" }
852
+ },
853
+ {
854
+ type: "section",
855
+ fields: [
856
+ { type: "mrkdwn", text: `*Tool:*
857
+ \`${notification.toolName}\`` },
858
+ { type: "mrkdwn", text: `*Agent:*
859
+ ${notification.agentId || "unknown"}` },
860
+ { type: "mrkdwn", text: `*Policy:*
861
+ ${notification.policyName || "default"}` },
862
+ { type: "mrkdwn", text: `*Time:*
863
+ ${notification.timestamp}` }
864
+ ]
865
+ },
866
+ {
867
+ type: "section",
868
+ text: { type: "mrkdwn", text: `*Reason:* ${notification.reason}` }
869
+ },
870
+ ...notification.approveUrl || notification.traceUrl ? [
871
+ {
872
+ type: "actions",
873
+ elements: [
874
+ ...notification.approveUrl ? [{ type: "button", text: { type: "plain_text", text: "\u2705 Approve" }, url: notification.approveUrl, style: "primary" }] : [],
875
+ ...notification.traceUrl ? [{ type: "button", text: { type: "plain_text", text: "\u{1F50D} View Trace" }, url: notification.traceUrl }] : []
876
+ ]
877
+ }
878
+ ] : []
879
+ ]
880
+ };
881
+ } else if (config.template === "pagerduty") {
882
+ payload = {
883
+ routing_key: config.headers?.["X-Routing-Key"] || "",
884
+ event_action: "trigger",
885
+ payload: {
886
+ summary: `Agent approval required: ${notification.toolName}`,
887
+ source: "protect-mcp",
888
+ severity: "warning",
889
+ custom_details: {
890
+ tool: notification.toolName,
891
+ agent: notification.agentId,
892
+ policy: notification.policyName,
893
+ reason: notification.reason,
894
+ trace_url: notification.traceUrl,
895
+ approve_url: notification.approveUrl
896
+ }
897
+ }
898
+ };
899
+ } else {
900
+ payload = notification;
901
+ }
902
+ const response = await fetch(config.url, {
903
+ method: config.method || "POST",
904
+ headers: {
905
+ "Content-Type": "application/json",
906
+ ...config.headers
907
+ },
908
+ body: JSON.stringify(payload)
909
+ });
910
+ if (!response.ok) {
911
+ throw new Error(`Webhook failed: ${response.status}`);
912
+ }
913
+ }
914
+ async function sendEmail(config, notification) {
915
+ if (!config.resendApiKey) {
916
+ console.warn("[protect-mcp] Email notification skipped: no resendApiKey configured");
917
+ return;
918
+ }
919
+ const html = `
920
+ <div style="font-family: monospace; padding: 20px; background: #0d1117; color: #c9d1d9; border-radius: 8px;">
921
+ <h2 style="color: #10b981;">\u{1F512} Agent Approval Required</h2>
922
+ <table style="font-size: 14px; margin: 16px 0;">
923
+ <tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Tool:</td><td>${notification.toolName}</td></tr>
924
+ <tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Agent:</td><td>${notification.agentId || "unknown"}</td></tr>
925
+ <tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Reason:</td><td>${notification.reason}</td></tr>
926
+ <tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Time:</td><td>${notification.timestamp}</td></tr>
927
+ </table>
928
+ ${notification.approveUrl ? `<a href="${notification.approveUrl}" style="background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; margin-right: 8px;">\u2705 Approve</a>` : ""}
929
+ ${notification.traceUrl ? `<a href="${notification.traceUrl}" style="background: #1f2937; color: #c9d1d9; padding: 8px 16px; border-radius: 6px; text-decoration: none; border: 1px solid #374151;">\u{1F50D} View Trace</a>` : ""}
930
+ </div>
931
+ `;
932
+ const response = await fetch("https://api.resend.com/emails", {
933
+ method: "POST",
934
+ headers: {
935
+ Authorization: `Bearer ${config.resendApiKey}`,
936
+ "Content-Type": "application/json"
937
+ },
938
+ body: JSON.stringify({
939
+ from: "ScopeBlind <noreply@scopeblind.com>",
940
+ to: config.to,
941
+ subject: `\u{1F512} Approval required: ${notification.toolName}`,
942
+ html
943
+ })
944
+ });
945
+ if (!response.ok) {
946
+ throw new Error(`Resend email failed: ${response.status}`);
947
+ }
948
+ }
949
+ function parseNotificationConfigFromEnv() {
950
+ const config = {};
951
+ let hasConfig = false;
952
+ const smsTo = process.env.SCOPEBLIND_SMS_TO;
953
+ const twilioSid = process.env.TWILIO_ACCOUNT_SID;
954
+ const twilioToken = process.env.TWILIO_AUTH_TOKEN;
955
+ const twilioFrom = process.env.TWILIO_FROM_NUMBER;
956
+ if (smsTo && twilioSid && twilioToken && twilioFrom) {
957
+ config.sms = { accountSid: twilioSid, authToken: twilioToken, from: twilioFrom, to: smsTo };
958
+ hasConfig = true;
959
+ }
960
+ const webhookUrl = process.env.SCOPEBLIND_WEBHOOK_URL;
961
+ if (webhookUrl) {
962
+ config.webhook = {
963
+ url: webhookUrl,
964
+ template: process.env.SCOPEBLIND_WEBHOOK_TEMPLATE || "custom"
965
+ };
966
+ hasConfig = true;
967
+ }
968
+ const emailTo = process.env.SCOPEBLIND_EMAIL_TO;
969
+ if (emailTo) {
970
+ config.email = { to: emailTo, resendApiKey: process.env.RESEND_API_KEY };
971
+ hasConfig = true;
972
+ }
973
+ return hasConfig ? config : null;
974
+ }
975
+ var init_notifications = __esm({
976
+ "src/notifications.ts"() {
977
+ "use strict";
978
+ }
979
+ });
980
+
621
981
  // src/http-server.ts
622
982
  function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
623
983
  const startTime = Date.now();
@@ -684,13 +1044,13 @@ function handleHealth(res, startTime, config) {
684
1044
  }));
685
1045
  }
686
1046
  function handleStatus(res, logDir) {
687
- const logPath = (0, import_node_path2.join)(logDir, LOG_FILE);
688
- if (!(0, import_node_fs4.existsSync)(logPath)) {
1047
+ const logPath = (0, import_node_path3.join)(logDir, LOG_FILE);
1048
+ if (!(0, import_node_fs5.existsSync)(logPath)) {
689
1049
  res.writeHead(200);
690
1050
  res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
691
1051
  return;
692
1052
  }
693
- const raw = (0, import_node_fs4.readFileSync)(logPath, "utf-8");
1053
+ const raw = (0, import_node_fs5.readFileSync)(logPath, "utf-8");
694
1054
  const lines = raw.trim().split("\n").filter(Boolean);
695
1055
  const entries = [];
696
1056
  for (const line of lines) {
@@ -812,13 +1172,13 @@ function handleListApprovals(res, approvalStore) {
812
1172
  res.writeHead(200);
813
1173
  res.end(JSON.stringify({ grants }));
814
1174
  }
815
- var import_node_http, import_node_fs4, import_node_path2, LOG_FILE, MAX_RECEIPTS, ReceiptBuffer;
1175
+ var import_node_http, import_node_fs5, import_node_path3, LOG_FILE, MAX_RECEIPTS, ReceiptBuffer;
816
1176
  var init_http_server = __esm({
817
1177
  "src/http-server.ts"() {
818
1178
  "use strict";
819
1179
  import_node_http = require("http");
820
- import_node_fs4 = require("fs");
821
- import_node_path2 = require("path");
1180
+ import_node_fs5 = require("fs");
1181
+ import_node_path3 = require("path");
822
1182
  LOG_FILE = ".protect-mcp-log.jsonl";
823
1183
  MAX_RECEIPTS = 100;
824
1184
  ReceiptBuffer = class {
@@ -850,21 +1210,23 @@ var init_http_server = __esm({
850
1210
  });
851
1211
 
852
1212
  // src/gateway.ts
853
- var import_node_child_process, import_node_crypto2, import_node_readline, import_node_fs5, import_node_path3, LOG_FILE2, RECEIPTS_FILE, ProtectGateway;
1213
+ var import_node_child_process, import_node_crypto3, import_node_readline, import_node_fs6, import_node_path4, LOG_FILE2, RECEIPTS_FILE, ProtectGateway;
854
1214
  var init_gateway = __esm({
855
1215
  "src/gateway.ts"() {
856
1216
  "use strict";
857
1217
  import_node_child_process = require("child_process");
858
- import_node_crypto2 = require("crypto");
1218
+ import_node_crypto3 = require("crypto");
859
1219
  import_node_readline = require("readline");
860
- import_node_fs5 = require("fs");
861
- import_node_path3 = require("path");
1220
+ import_node_fs6 = require("fs");
1221
+ import_node_path4 = require("path");
862
1222
  init_policy();
863
1223
  init_admission();
864
1224
  init_credentials();
865
1225
  init_signing();
866
1226
  init_external_pdp();
1227
+ init_cedar_evaluator();
867
1228
  init_evidence_store();
1229
+ init_notifications();
868
1230
  init_http_server();
869
1231
  LOG_FILE2 = ".protect-mcp-log.jsonl";
870
1232
  RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
@@ -880,18 +1242,30 @@ var init_gateway = __esm({
880
1242
  /** Approval grants keyed by request_id (scoped to the specific action that was requested) */
881
1243
  approvalStore = /* @__PURE__ */ new Map();
882
1244
  /** Random nonce generated at startup — required for approval endpoint authentication */
883
- approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
1245
+ approvalNonce = (0, import_node_crypto3.randomBytes)(16).toString("hex");
884
1246
  currentTier = "unknown";
885
1247
  admissionResult = null;
1248
+ /** Notification config for approval gates (SMS, webhook, email) */
1249
+ notificationConfig = null;
886
1250
  /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
887
1251
  pendingResponses = /* @__PURE__ */ new Map();
888
1252
  httpMode = false;
1253
+ /** Loaded Cedar policy set (when policy_engine is "cedar") */
1254
+ cedarPolicySet = null;
889
1255
  constructor(config) {
890
1256
  this.config = config;
891
- this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
892
- this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
1257
+ this.logFilePath = (0, import_node_path4.join)(process.cwd(), LOG_FILE2);
1258
+ this.receiptFilePath = (0, import_node_path4.join)(process.cwd(), RECEIPTS_FILE);
893
1259
  this.evidenceStore = new EvidenceStore();
894
1260
  this.receiptBuffer = new ReceiptBuffer();
1261
+ this.notificationConfig = parseNotificationConfigFromEnv();
1262
+ }
1263
+ /**
1264
+ * Set the Cedar policy set for local evaluation.
1265
+ * Called during CLI startup when --cedar flag is used.
1266
+ */
1267
+ setCedarPolicies(policySet) {
1268
+ this.cedarPolicySet = policySet;
895
1269
  }
896
1270
  async start() {
897
1271
  const { command, args, verbose } = this.config;
@@ -1022,7 +1396,7 @@ var init_gateway = __esm({
1022
1396
  }
1023
1397
  async interceptToolCall(request) {
1024
1398
  const toolName = request.params?.name || "unknown";
1025
- const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
1399
+ const requestId = (0, import_node_crypto3.randomUUID)().slice(0, 12);
1026
1400
  const mode = this.config.enforce ? "enforce" : "shadow";
1027
1401
  let resolvedAgentKid = this.admissionResult?.agent_id;
1028
1402
  let effectiveToolPolicy;
@@ -1060,6 +1434,27 @@ var init_gateway = __esm({
1060
1434
  }
1061
1435
  }
1062
1436
  }
1437
+ if (this.config.policy?.policy_engine === "cedar" && this.cedarPolicySet) {
1438
+ try {
1439
+ const cedarDecision = await evaluateCedar(this.cedarPolicySet, {
1440
+ tool: toolName,
1441
+ tier: this.currentTier,
1442
+ agentId: this.admissionResult?.agent_id
1443
+ });
1444
+ if (!cedarDecision.allowed) {
1445
+ const reason = cedarDecision.reason || "cedar_deny";
1446
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
1447
+ if (this.config.enforce) {
1448
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by Cedar policy`);
1449
+ }
1450
+ return null;
1451
+ }
1452
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
1453
+ return null;
1454
+ } catch (err) {
1455
+ if (this.config.verbose) this.log(`Cedar evaluation error: ${err instanceof Error ? err.message : err}`);
1456
+ }
1457
+ }
1063
1458
  if (this.config.policy?.external && (this.config.policy.policy_engine === "external" || this.config.policy.policy_engine === "hybrid")) {
1064
1459
  try {
1065
1460
  const ctx = buildDecisionContext(toolName, this.currentTier, {
@@ -1107,6 +1502,20 @@ var init_gateway = __esm({
1107
1502
  return null;
1108
1503
  }
1109
1504
  this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
1505
+ if (this.notificationConfig) {
1506
+ sendApprovalNotification(this.notificationConfig, {
1507
+ requestId,
1508
+ toolName,
1509
+ agentId: this.admissionResult?.agent_id,
1510
+ policyName: "default",
1511
+ reason: `Policy requires human approval for "${toolName}"`,
1512
+ traceUrl: `https://scopeblind.com/trace`,
1513
+ approveUrl: void 0,
1514
+ // Approve URL provided when HTTP transport is active
1515
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1516
+ }).catch(() => {
1517
+ });
1518
+ }
1110
1519
  if (this.config.enforce) {
1111
1520
  return {
1112
1521
  jsonrpc: "2.0",
@@ -1154,8 +1563,19 @@ var init_gateway = __esm({
1154
1563
  }
1155
1564
  return policy.rate_limit;
1156
1565
  }
1566
+ /**
1567
+ * Emit a decision log entry with OTel-compatible trace IDs and optional
1568
+ * signed receipt generation.
1569
+ *
1570
+ * @patent Patent-protected construction — decision receipts with configurable
1571
+ * disclosure and issuer-blind properties. Covered by Apache 2.0 patent grant
1572
+ * for users of this code. Clean-room reimplementation requires a patent license.
1573
+ * @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
1574
+ */
1157
1575
  emitDecisionLog(entry) {
1158
1576
  const mode = this.config.enforce ? "enforce" : "shadow";
1577
+ const otelTraceId = entry.otel_trace_id || (0, import_node_crypto3.randomBytes)(16).toString("hex");
1578
+ const otelSpanId = entry.otel_span_id || (0, import_node_crypto3.randomBytes)(8).toString("hex");
1159
1579
  const log = {
1160
1580
  v: 2,
1161
1581
  tool: entry.tool || "unknown",
@@ -1163,17 +1583,19 @@ var init_gateway = __esm({
1163
1583
  reason_code: entry.reason_code || "default_allow",
1164
1584
  policy_digest: this.config.policyDigest,
1165
1585
  policy_engine: this.config.policy?.policy_engine || "built-in",
1166
- request_id: entry.request_id || (0, import_node_crypto2.randomUUID)().slice(0, 12),
1586
+ request_id: entry.request_id || (0, import_node_crypto3.randomUUID)().slice(0, 12),
1167
1587
  timestamp: Date.now(),
1168
1588
  mode,
1169
1589
  ...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining },
1170
1590
  ...entry.tier && { tier: entry.tier },
1171
- ...entry.credential_ref && { credential_ref: entry.credential_ref }
1591
+ ...entry.credential_ref && { credential_ref: entry.credential_ref },
1592
+ otel_trace_id: otelTraceId,
1593
+ otel_span_id: otelSpanId
1172
1594
  };
1173
1595
  process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
1174
1596
  `);
1175
1597
  try {
1176
- (0, import_node_fs5.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
1598
+ (0, import_node_fs6.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
1177
1599
  } catch {
1178
1600
  }
1179
1601
  if (isSigningEnabled()) {
@@ -1182,7 +1604,7 @@ var init_gateway = __esm({
1182
1604
  process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
1183
1605
  `);
1184
1606
  try {
1185
- (0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
1607
+ (0, import_node_fs6.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
1186
1608
  } catch {
1187
1609
  }
1188
1610
  this.receiptBuffer.add(log.request_id, signed.signed);
@@ -4163,8 +4585,8 @@ function generateReport(logPath, receiptPath, periodDays) {
4163
4585
  const now = /* @__PURE__ */ new Date();
4164
4586
  const from = new Date(now.getTime() - periodDays * 864e5);
4165
4587
  const entries = [];
4166
- if ((0, import_node_fs7.existsSync)(logPath)) {
4167
- const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
4588
+ if ((0, import_node_fs8.existsSync)(logPath)) {
4589
+ const raw = (0, import_node_fs8.readFileSync)(logPath, "utf-8");
4168
4590
  for (const line of raw.split("\n")) {
4169
4591
  const trimmed = line.trim();
4170
4592
  if (!trimmed) continue;
@@ -4184,8 +4606,8 @@ function generateReport(logPath, receiptPath, periodDays) {
4184
4606
  let receiptsSigned = 0;
4185
4607
  let signerKid = "";
4186
4608
  let signerIssuer = "";
4187
- if ((0, import_node_fs7.existsSync)(receiptPath)) {
4188
- const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
4609
+ if ((0, import_node_fs8.existsSync)(receiptPath)) {
4610
+ const raw = (0, import_node_fs8.readFileSync)(receiptPath, "utf-8");
4189
4611
  for (const line of raw.split("\n")) {
4190
4612
  const trimmed = line.trim();
4191
4613
  if (!trimmed) continue;
@@ -4317,11 +4739,11 @@ function formatReportMarkdown(report) {
4317
4739
  lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
4318
4740
  return lines.join("\n");
4319
4741
  }
4320
- var import_node_fs7;
4742
+ var import_node_fs8;
4321
4743
  var init_report = __esm({
4322
4744
  "src/report.ts"() {
4323
4745
  "use strict";
4324
- import_node_fs7 = require("fs");
4746
+ import_node_fs8 = require("fs");
4325
4747
  }
4326
4748
  });
4327
4749
 
@@ -4332,11 +4754,11 @@ init_signing();
4332
4754
  init_credentials();
4333
4755
 
4334
4756
  // src/simulate.ts
4335
- var import_node_fs6 = require("fs");
4757
+ var import_node_fs7 = require("fs");
4336
4758
  init_policy();
4337
4759
  init_admission();
4338
4760
  function parseLogFile(path) {
4339
- const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
4761
+ const raw = (0, import_node_fs7.readFileSync)(path, "utf-8");
4340
4762
  const entries = [];
4341
4763
  for (const line of raw.split("\n")) {
4342
4764
  const trimmed = line.trim();
@@ -4465,6 +4887,7 @@ function formatSimulation(summary) {
4465
4887
  }
4466
4888
 
4467
4889
  // src/cli.ts
4890
+ init_cedar_evaluator();
4468
4891
  function printHelp() {
4469
4892
  process.stderr.write(`
4470
4893
  protect-mcp \u2014 Shadow-mode security gateway for MCP servers
@@ -4484,6 +4907,7 @@ Usage:
4484
4907
 
4485
4908
  Options:
4486
4909
  --policy <path> Policy/config JSON file (default: allow-all)
4910
+ --cedar <dir> Cedar policy directory (alternative to --policy, evaluates locally via WASM)
4487
4911
  --slug <slug> ScopeBlind tenant slug (optional)
4488
4912
  --enforce Enable enforcement mode (default: shadow mode)
4489
4913
  --http Start HTTP/SSE server instead of stdio proxy
@@ -4495,6 +4919,7 @@ Commands:
4495
4919
  quickstart Zero-config onboarding: init + demo + show receipts in one command
4496
4920
  init Generate config template, Ed25519 keypair, and sample policy
4497
4921
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
4922
+ doctor Check your setup: keys, policies, verifier, API connectivity
4498
4923
  trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
4499
4924
  status Show tool call statistics from the local decision log
4500
4925
  digest Generate a human-readable summary of agent activity
@@ -4517,6 +4942,7 @@ Examples:
4517
4942
  }
4518
4943
  function parseArgs(argv) {
4519
4944
  let policyPath;
4945
+ let cedarDir;
4520
4946
  let slug;
4521
4947
  let enforce = false;
4522
4948
  let verbose = false;
@@ -4541,6 +4967,8 @@ function parseArgs(argv) {
4541
4967
  process.exit(0);
4542
4968
  } else if (arg === "--policy" && i + 1 < options.length) {
4543
4969
  policyPath = options[++i];
4970
+ } else if (arg === "--cedar" && i + 1 < options.length) {
4971
+ cedarDir = options[++i];
4544
4972
  } else if (arg === "--slug" && i + 1 < options.length) {
4545
4973
  slug = options[++i];
4546
4974
  } else if (arg === "--enforce") {
@@ -4552,20 +4980,20 @@ function parseArgs(argv) {
4552
4980
  `);
4553
4981
  }
4554
4982
  }
4555
- return { policyPath, slug, enforce, verbose, childCommand };
4983
+ return { policyPath, cedarDir, slug, enforce, verbose, childCommand };
4556
4984
  }
4557
4985
  async function handleInit(argv) {
4558
- const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
4559
- const { join: join4 } = await import("path");
4986
+ const { writeFileSync: writeFileSync2, existsSync: existsSync6, mkdirSync } = await import("fs");
4987
+ const { join: join5 } = await import("path");
4560
4988
  let dir = process.cwd();
4561
4989
  const dirIdx = argv.indexOf("--dir");
4562
4990
  if (dirIdx !== -1 && argv[dirIdx + 1]) {
4563
4991
  dir = argv[dirIdx + 1];
4564
4992
  }
4565
- const configPath = join4(dir, "protect-mcp.json");
4566
- const keysDir = join4(dir, "keys");
4567
- const keyPath = join4(keysDir, "gateway.json");
4568
- if (existsSync5(configPath)) {
4993
+ const configPath = join5(dir, "protect-mcp.json");
4994
+ const keysDir = join5(dir, "keys");
4995
+ const keyPath = join5(keysDir, "gateway.json");
4996
+ if (existsSync6(configPath)) {
4569
4997
  process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
4570
4998
  `);
4571
4999
  process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
@@ -4584,7 +5012,7 @@ async function handleInit(argv) {
4584
5012
  kid: "generated"
4585
5013
  };
4586
5014
  }
4587
- if (!existsSync5(keysDir)) {
5015
+ if (!existsSync6(keysDir)) {
4588
5016
  mkdirSync(keysDir, { recursive: true });
4589
5017
  }
4590
5018
  writeFileSync2(keyPath, JSON.stringify({
@@ -4594,8 +5022,8 @@ async function handleInit(argv) {
4594
5022
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
4595
5023
  warning: "KEEP THIS FILE SECRET. Never commit to version control."
4596
5024
  }, null, 2) + "\n");
4597
- const gitignorePath = join4(keysDir, ".gitignore");
4598
- if (!existsSync5(gitignorePath)) {
5025
+ const gitignorePath = join5(keysDir, ".gitignore");
5026
+ if (!existsSync6(gitignorePath)) {
4599
5027
  writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
4600
5028
  }
4601
5029
  const config = {
@@ -4668,8 +5096,8 @@ Add --enforce when ready to block policy violations.
4668
5096
  `);
4669
5097
  }
4670
5098
  async function handleDemo() {
4671
- const { existsSync: existsSync5 } = await import("fs");
4672
- const { join: join4, dirname, resolve } = await import("path");
5099
+ const { existsSync: existsSync6 } = await import("fs");
5100
+ const { join: join5, dirname, resolve } = await import("path");
4673
5101
  const { realpathSync } = await import("fs");
4674
5102
  const cliPath = resolve(process.argv[1] || "dist/cli.js");
4675
5103
  let cliDir;
@@ -4678,9 +5106,9 @@ async function handleDemo() {
4678
5106
  } catch {
4679
5107
  cliDir = dirname(cliPath);
4680
5108
  }
4681
- const demoServerPath = join4(cliDir, "demo-server.js");
4682
- const configPath = join4(process.cwd(), "protect-mcp.json");
4683
- const hasConfig = existsSync5(configPath);
5109
+ const demoServerPath = join5(cliDir, "demo-server.js");
5110
+ const configPath = join5(process.cwd(), "protect-mcp.json");
5111
+ const hasConfig = existsSync6(configPath);
4684
5112
  if (!hasConfig) {
4685
5113
  process.stderr.write(`
4686
5114
  ${bold("protect-mcp demo")}
@@ -4754,15 +5182,15 @@ Starting demo server with 5 tools...
4754
5182
  await gateway.start();
4755
5183
  }
4756
5184
  async function handleStatus2(argv) {
4757
- const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4758
- const { join: join4 } = await import("path");
5185
+ const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
5186
+ const { join: join5 } = await import("path");
4759
5187
  let dir = process.cwd();
4760
5188
  const dirIdx = argv.indexOf("--dir");
4761
5189
  if (dirIdx !== -1 && argv[dirIdx + 1]) {
4762
5190
  dir = argv[dirIdx + 1];
4763
5191
  }
4764
- const logPath = join4(dir, ".protect-mcp-log.jsonl");
4765
- if (!existsSync5(logPath)) {
5192
+ const logPath = join5(dir, ".protect-mcp-log.jsonl");
5193
+ if (!existsSync6(logPath)) {
4766
5194
  process.stderr.write(`${bold("protect-mcp status")}
4767
5195
 
4768
5196
  `);
@@ -4772,7 +5200,7 @@ async function handleStatus2(argv) {
4772
5200
  `);
4773
5201
  process.exit(0);
4774
5202
  }
4775
- const raw = readFileSync7(logPath, "utf-8");
5203
+ const raw = readFileSync8(logPath, "utf-8");
4776
5204
  const lines = raw.trim().split("\n").filter(Boolean);
4777
5205
  if (lines.length === 0) {
4778
5206
  process.stderr.write(`${bold("protect-mcp status")}
@@ -4851,10 +5279,10 @@ ${bold("protect-mcp status")}
4851
5279
  process.stdout.write(` ${reason.padEnd(25)} ${count}
4852
5280
  `);
4853
5281
  }
4854
- const evidencePath = join4(dir, ".protect-mcp-evidence.json");
4855
- if (existsSync5(evidencePath)) {
5282
+ const evidencePath = join5(dir, ".protect-mcp-evidence.json");
5283
+ if (existsSync6(evidencePath)) {
4856
5284
  try {
4857
- const evidenceRaw = readFileSync7(evidencePath, "utf-8");
5285
+ const evidenceRaw = readFileSync8(evidencePath, "utf-8");
4858
5286
  const evidence = JSON.parse(evidenceRaw);
4859
5287
  const agentCount = Object.keys(evidence.agents || {}).length;
4860
5288
  process.stdout.write(`
@@ -4863,10 +5291,10 @@ ${bold("protect-mcp status")}
4863
5291
  } catch {
4864
5292
  }
4865
5293
  }
4866
- const keyPath = join4(dir, "keys", "gateway.json");
4867
- if (existsSync5(keyPath)) {
5294
+ const keyPath = join5(dir, "keys", "gateway.json");
5295
+ if (existsSync6(keyPath)) {
4868
5296
  try {
4869
- const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
5297
+ const keyData = JSON.parse(readFileSync8(keyPath, "utf-8"));
4870
5298
  if (keyData.publicKey) {
4871
5299
  const fingerprint = keyData.publicKey.slice(0, 16) + "...";
4872
5300
  process.stdout.write(`
@@ -4905,21 +5333,21 @@ function yellow(s) {
4905
5333
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
4906
5334
  }
4907
5335
  async function handleDigest(argv) {
4908
- const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4909
- const { join: join4 } = await import("path");
5336
+ const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
5337
+ const { join: join5 } = await import("path");
4910
5338
  let dir = process.cwd();
4911
5339
  const dirIdx = argv.indexOf("--dir");
4912
5340
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4913
5341
  const today = argv.includes("--today");
4914
- const logPath = join4(dir, ".protect-mcp-log.jsonl");
4915
- if (!existsSync5(logPath)) {
5342
+ const logPath = join5(dir, ".protect-mcp-log.jsonl");
5343
+ if (!existsSync6(logPath)) {
4916
5344
  process.stderr.write(`${bold("protect-mcp digest")}
4917
5345
 
4918
5346
  No log file found. Run protect-mcp first.
4919
5347
  `);
4920
5348
  process.exit(0);
4921
5349
  }
4922
- const raw = readFileSync7(logPath, "utf-8");
5350
+ const raw = readFileSync8(logPath, "utf-8");
4923
5351
  const lines = raw.trim().split("\n").filter(Boolean);
4924
5352
  let entries = [];
4925
5353
  for (const line of lines) {
@@ -4996,22 +5424,22 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
4996
5424
  `);
4997
5425
  }
4998
5426
  async function handleReceipts2(argv) {
4999
- const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
5000
- const { join: join4 } = await import("path");
5427
+ const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
5428
+ const { join: join5 } = await import("path");
5001
5429
  let dir = process.cwd();
5002
5430
  const dirIdx = argv.indexOf("--dir");
5003
5431
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
5004
5432
  const lastIdx = argv.indexOf("--last");
5005
5433
  const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
5006
- const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
5007
- if (!existsSync5(receiptsPath)) {
5434
+ const receiptsPath = join5(dir, ".protect-mcp-receipts.jsonl");
5435
+ if (!existsSync6(receiptsPath)) {
5008
5436
  process.stderr.write(`${bold("protect-mcp receipts")}
5009
5437
 
5010
5438
  No signed receipt file found. Run protect-mcp with signing enabled first.
5011
5439
  `);
5012
5440
  process.exit(0);
5013
5441
  }
5014
- const raw = readFileSync7(receiptsPath, "utf-8");
5442
+ const raw = readFileSync8(receiptsPath, "utf-8");
5015
5443
  const lines = raw.trim().split("\n").filter(Boolean);
5016
5444
  const recent = lines.slice(-count);
5017
5445
  process.stdout.write(`
@@ -5034,32 +5462,32 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
5034
5462
  `);
5035
5463
  }
5036
5464
  async function handleBundle(argv) {
5037
- const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
5038
- const { join: join4 } = await import("path");
5465
+ const { readFileSync: readFileSync8, writeFileSync: writeFileSync2, existsSync: existsSync6 } = await import("fs");
5466
+ const { join: join5 } = await import("path");
5039
5467
  const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
5040
5468
  let dir = process.cwd();
5041
5469
  const dirIdx = argv.indexOf("--dir");
5042
5470
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
5043
5471
  const outputIdx = argv.indexOf("--output");
5044
- const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
5045
- const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
5046
- const keyPath = join4(dir, "keys", "gateway.json");
5047
- if (!existsSync5(receiptsPath)) {
5472
+ const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join5(dir, "audit-bundle.json");
5473
+ const receiptsPath = join5(dir, ".protect-mcp-receipts.jsonl");
5474
+ const keyPath = join5(dir, "keys", "gateway.json");
5475
+ if (!existsSync6(receiptsPath)) {
5048
5476
  process.stderr.write(`${bold("protect-mcp bundle")}
5049
5477
 
5050
5478
  No signed receipt file found. Run protect-mcp with signing enabled first.
5051
5479
  `);
5052
5480
  process.exit(0);
5053
5481
  }
5054
- if (!existsSync5(keyPath)) {
5482
+ if (!existsSync6(keyPath)) {
5055
5483
  process.stderr.write(`${bold("protect-mcp bundle")}
5056
5484
 
5057
5485
  No key file found at ${keyPath}
5058
5486
  `);
5059
5487
  process.exit(1);
5060
5488
  }
5061
- const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5062
- const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
5489
+ const receipts = readFileSync8(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5490
+ const keyData = JSON.parse(readFileSync8(keyPath, "utf-8"));
5063
5491
  const bundle = createAuditBundle2({
5064
5492
  tenant: keyData.issuer || "protect-mcp",
5065
5493
  receipts,
@@ -5085,10 +5513,10 @@ ${bold("protect-mcp bundle")}
5085
5513
  `);
5086
5514
  }
5087
5515
  async function handleQuickstart() {
5088
- const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
5089
- const { join: join4 } = await import("path");
5516
+ const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync6, mkdirSync, readFileSync: readFileSync8 } = await import("fs");
5517
+ const { join: join5 } = await import("path");
5090
5518
  const { tmpdir } = await import("os");
5091
- const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
5519
+ const dir = mkdtempSync(join5(tmpdir(), "protect-mcp-quickstart-"));
5092
5520
  process.stdout.write(`
5093
5521
  ${bold("protect-mcp quickstart")}
5094
5522
  `);
@@ -5109,7 +5537,7 @@ ${bold("protect-mcp quickstart")}
5109
5537
  process.stdout.write(` Working dir: ${dir}
5110
5538
 
5111
5539
  `);
5112
- const keysDir = join4(dir, "keys");
5540
+ const keysDir = join5(dir, "keys");
5113
5541
  mkdirSync(keysDir, { recursive: true });
5114
5542
  const { randomBytes: randomBytes3 } = await import("crypto");
5115
5543
  let keypair;
@@ -5130,13 +5558,13 @@ ${bold("protect-mcp quickstart")}
5130
5558
  kid: `quickstart-${Date.now()}`
5131
5559
  };
5132
5560
  }
5133
- writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
5561
+ writeFileSync2(join5(keysDir, "gateway.json"), JSON.stringify({
5134
5562
  privateKey: keypair.privateKey,
5135
5563
  publicKey: keypair.publicKey,
5136
5564
  kid: keypair.kid,
5137
5565
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
5138
5566
  }, null, 2) + "\n");
5139
- const configPath = join4(dir, "protect-mcp.json");
5567
+ const configPath = join5(dir, "protect-mcp.json");
5140
5568
  const config = {
5141
5569
  tools: {
5142
5570
  "*": { rate_limit: "100/hour" },
@@ -5144,7 +5572,7 @@ ${bold("protect-mcp quickstart")}
5144
5572
  },
5145
5573
  default_tier: "unknown",
5146
5574
  signing: {
5147
- key_path: join4(keysDir, "gateway.json"),
5575
+ key_path: join5(keysDir, "gateway.json"),
5148
5576
  issuer: "protect-mcp-quickstart",
5149
5577
  enabled: true
5150
5578
  }
@@ -5186,7 +5614,7 @@ async function handleTrace(argv) {
5186
5614
  process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
5187
5615
  process.exit(1);
5188
5616
  }
5189
- let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
5617
+ let endpoint = "https://api.scopeblind.com/evidence";
5190
5618
  let depth = 3;
5191
5619
  for (let i = 1; i < argv.length; i++) {
5192
5620
  if (argv[i] === "--endpoint" && argv[i + 1]) {
@@ -5316,25 +5744,25 @@ ${"\u2500".repeat(60)}
5316
5744
  `);
5317
5745
  }
5318
5746
  async function traceLocal(receiptId) {
5319
- const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
5320
- const { join: join4 } = await import("path");
5747
+ const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
5748
+ const { join: join5 } = await import("path");
5321
5749
  const dir = process.cwd();
5322
- const receiptsDir = join4(dir, ".protect-mcp", "receipts");
5323
- if (!existsSync5(receiptsDir)) {
5750
+ const receiptsDir = join5(dir, ".protect-mcp", "receipts");
5751
+ if (!existsSync6(receiptsDir)) {
5324
5752
  process.stdout.write(` No local receipts found in ${receiptsDir}
5325
5753
 
5326
5754
  `);
5327
5755
  return;
5328
5756
  }
5329
- const { readdirSync } = await import("fs");
5330
- const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
5757
+ const { readdirSync: readdirSync2 } = await import("fs");
5758
+ const files = readdirSync2(receiptsDir).filter((f) => f.endsWith(".json"));
5331
5759
  process.stdout.write(` Scanning ${files.length} local receipts...
5332
5760
 
5333
5761
  `);
5334
5762
  const receipts = [];
5335
5763
  for (const file of files) {
5336
5764
  try {
5337
- const content = readFileSync7(join4(receiptsDir, file), "utf-8");
5765
+ const content = readFileSync8(join5(receiptsDir, file), "utf-8");
5338
5766
  const receipt = JSON.parse(content);
5339
5767
  receipts.push(receipt);
5340
5768
  } catch {
@@ -5436,12 +5864,57 @@ async function main() {
5436
5864
  await handleReport(args.slice(1));
5437
5865
  process.exit(0);
5438
5866
  }
5439
- const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
5867
+ if (args[0] === "doctor") {
5868
+ await handleDoctor();
5869
+ process.exit(0);
5870
+ }
5871
+ const { policyPath, cedarDir, slug, enforce, verbose, childCommand } = parseArgs(args);
5440
5872
  let policy = null;
5441
5873
  let policyDigest = "none";
5442
5874
  let credentials;
5443
5875
  let signing;
5444
- if (policyPath) {
5876
+ let cedarPolicySet = null;
5877
+ let effectiveCedarDir = cedarDir;
5878
+ if (!effectiveCedarDir && !policyPath) {
5879
+ const { existsSync: existsSync6, readdirSync: readdirSync2 } = await import("fs");
5880
+ for (const candidate of ["cedar", "policies", "."]) {
5881
+ try {
5882
+ if (existsSync6(candidate) && readdirSync2(candidate).some((f) => f.endsWith(".cedar"))) {
5883
+ effectiveCedarDir = candidate;
5884
+ process.stderr.write(`[PROTECT_MCP] Auto-detected Cedar policies in ./${candidate}/
5885
+ `);
5886
+ break;
5887
+ }
5888
+ } catch {
5889
+ }
5890
+ }
5891
+ }
5892
+ if (effectiveCedarDir) {
5893
+ try {
5894
+ const cedarAvailable = await isCedarAvailable();
5895
+ if (!cedarAvailable) {
5896
+ process.stderr.write("[PROTECT_MCP] Warning: @cedar-policy/cedar-wasm not installed. Install with: npm install @cedar-policy/cedar-wasm\n");
5897
+ process.stderr.write("[PROTECT_MCP] Cedar policies will be loaded but evaluated with fallback (allow-all).\n");
5898
+ }
5899
+ cedarPolicySet = loadCedarPolicies(effectiveCedarDir);
5900
+ policyDigest = cedarPolicySet.digest;
5901
+ policy = {
5902
+ tools: { "*": { require: "any" } },
5903
+ policy_engine: "cedar",
5904
+ cedar_dir: effectiveCedarDir
5905
+ };
5906
+ process.stderr.write(`[PROTECT_MCP] Cedar policy engine: loaded ${cedarPolicySet.fileCount} policies from ${effectiveCedarDir} (digest: ${policyDigest})
5907
+ `);
5908
+ if (verbose) {
5909
+ process.stderr.write(`[PROTECT_MCP] Cedar files: ${cedarPolicySet.files.join(", ")}
5910
+ `);
5911
+ }
5912
+ } catch (err) {
5913
+ process.stderr.write(`[PROTECT_MCP] Error loading Cedar policies: ${err instanceof Error ? err.message : err}
5914
+ `);
5915
+ process.exit(1);
5916
+ }
5917
+ } else if (policyPath) {
5445
5918
  try {
5446
5919
  const loaded = loadPolicy(policyPath);
5447
5920
  policy = loaded.policy;
@@ -5492,6 +5965,9 @@ async function main() {
5492
5965
  return;
5493
5966
  }
5494
5967
  const gateway = new ProtectGateway(config);
5968
+ if (cedarPolicySet) {
5969
+ gateway.setCedarPolicies(cedarPolicySet);
5970
+ }
5495
5971
  await gateway.start();
5496
5972
  }
5497
5973
  async function handleSimulate(args) {
@@ -5514,8 +5990,8 @@ async function handleSimulate(args) {
5514
5990
  process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
5515
5991
  process.exit(1);
5516
5992
  }
5517
- const { existsSync: existsSync5 } = await import("fs");
5518
- if (!existsSync5(logPath)) {
5993
+ const { existsSync: existsSync6 } = await import("fs");
5994
+ if (!existsSync6(logPath)) {
5519
5995
  process.stderr.write(`Log file not found: ${logPath}
5520
5996
  `);
5521
5997
  process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
@@ -5536,6 +6012,127 @@ async function handleSimulate(args) {
5536
6012
  process.stdout.write(formatSimulation(summary) + "\n");
5537
6013
  }
5538
6014
  }
6015
+ async function handleDoctor() {
6016
+ const { existsSync: existsSync6, readFileSync: readFileSync8, readdirSync: readdirSync2 } = await import("fs");
6017
+ const { join: join5 } = await import("path");
6018
+ const { execSync } = await import("child_process");
6019
+ const green2 = (s) => `\x1B[32m\u2713\x1B[0m ${s}`;
6020
+ const red2 = (s) => `\x1B[31m\u2717\x1B[0m ${s}`;
6021
+ const yellow2 = (s) => `\x1B[33m\u26A0\x1B[0m ${s}`;
6022
+ const dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
6023
+ process.stdout.write("\n\x1B[1mprotect-mcp doctor\x1B[0m\n");
6024
+ process.stdout.write(dim2("Checking your ScopeBlind setup...\n\n"));
6025
+ let issues = 0;
6026
+ const nodeVersion = process.version;
6027
+ const major = parseInt(nodeVersion.slice(1));
6028
+ if (major >= 18) {
6029
+ process.stdout.write(green2(`Node.js ${nodeVersion}
6030
+ `));
6031
+ } else {
6032
+ process.stdout.write(red2(`Node.js ${nodeVersion} \u2014 requires >= 18
6033
+ `));
6034
+ issues++;
6035
+ }
6036
+ const configPath = join5(process.cwd(), "scopeblind.config.json");
6037
+ if (existsSync6(configPath)) {
6038
+ try {
6039
+ const config = JSON.parse(readFileSync8(configPath, "utf-8"));
6040
+ if (config.signing?.private_key || config.signing?.key_file) {
6041
+ process.stdout.write(green2("Signing keys configured\n"));
6042
+ } else {
6043
+ process.stdout.write(yellow2("Config found but no signing keys \u2014 run: protect-mcp init\n"));
6044
+ issues++;
6045
+ }
6046
+ } catch {
6047
+ process.stdout.write(red2("Invalid scopeblind.config.json\n"));
6048
+ issues++;
6049
+ }
6050
+ } else {
6051
+ process.stdout.write(yellow2("No scopeblind.config.json \u2014 run: protect-mcp init\n"));
6052
+ }
6053
+ let policyFound = false;
6054
+ for (const dir of ["cedar", "policies", "."]) {
6055
+ try {
6056
+ if (existsSync6(dir) && readdirSync2(dir).some((f) => f.endsWith(".cedar"))) {
6057
+ process.stdout.write(green2(`Cedar policies found in ./${dir}/
6058
+ `));
6059
+ policyFound = true;
6060
+ break;
6061
+ }
6062
+ } catch {
6063
+ }
6064
+ }
6065
+ if (!policyFound) {
6066
+ for (const name of ["policy.json", "protect-mcp.policy.json", "scopeblind-policy.json"]) {
6067
+ if (existsSync6(name)) {
6068
+ process.stdout.write(green2(`JSON policy found: ${name}
6069
+ `));
6070
+ policyFound = true;
6071
+ break;
6072
+ }
6073
+ }
6074
+ }
6075
+ if (!policyFound) {
6076
+ process.stdout.write(yellow2("No policy files found \u2014 running in shadow mode (allow all)\n"));
6077
+ }
6078
+ try {
6079
+ const cedarAvailable = await isCedarAvailable();
6080
+ if (cedarAvailable) {
6081
+ process.stdout.write(green2("Cedar WASM engine available\n"));
6082
+ } else {
6083
+ process.stdout.write(dim2(" Cedar WASM not installed \u2014 install: npm install @cedar-policy/cedar-wasm\n"));
6084
+ }
6085
+ } catch {
6086
+ process.stdout.write(dim2(" Cedar WASM not installed\n"));
6087
+ }
6088
+ const logFile = join5(process.cwd(), "protect-mcp-decisions.jsonl");
6089
+ const receiptFile = join5(process.cwd(), "protect-mcp-receipts.jsonl");
6090
+ if (existsSync6(logFile)) {
6091
+ try {
6092
+ const lines = readFileSync8(logFile, "utf-8").trim().split("\n").length;
6093
+ process.stdout.write(green2(`Decision log: ${lines} entries
6094
+ `));
6095
+ } catch {
6096
+ process.stdout.write(green2("Decision log exists\n"));
6097
+ }
6098
+ } else {
6099
+ process.stdout.write(dim2(" No decision log yet \u2014 will be created on first tool call\n"));
6100
+ }
6101
+ if (existsSync6(receiptFile)) {
6102
+ try {
6103
+ const lines = readFileSync8(receiptFile, "utf-8").trim().split("\n").length;
6104
+ process.stdout.write(green2(`Receipt file: ${lines} signed receipts
6105
+ `));
6106
+ } catch {
6107
+ process.stdout.write(green2("Receipt file exists\n"));
6108
+ }
6109
+ }
6110
+ try {
6111
+ execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
6112
+ process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
6113
+ } catch {
6114
+ process.stdout.write(dim2(" Verifier not cached \u2014 install: npm install -g @veritasacta/verify\n"));
6115
+ }
6116
+ try {
6117
+ const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
6118
+ if (res.ok) {
6119
+ process.stdout.write(green2("ScopeBlind API reachable\n"));
6120
+ } else {
6121
+ process.stdout.write(yellow2("ScopeBlind API returned non-200 \u2014 receipts will be stored locally\n"));
6122
+ }
6123
+ } catch {
6124
+ process.stdout.write(dim2(" ScopeBlind API not reachable \u2014 offline mode (receipts stored locally)\n"));
6125
+ }
6126
+ process.stdout.write("\n");
6127
+ if (issues === 0) {
6128
+ process.stdout.write("\x1B[32m\x1B[1mAll checks passed.\x1B[0m Ready to wrap MCP servers.\n");
6129
+ process.stdout.write(dim2("\n npx protect-mcp -- node your-server.js\n\n"));
6130
+ } else {
6131
+ process.stdout.write(`\x1B[33m\x1B[1m${issues} issue(s) found.\x1B[0m Fix them and run doctor again.
6132
+
6133
+ `);
6134
+ }
6135
+ }
5539
6136
  async function handleReport(args) {
5540
6137
  let period = 30;
5541
6138
  let format = "json";
@@ -5554,9 +6151,9 @@ async function handleReport(args) {
5554
6151
  }
5555
6152
  }
5556
6153
  const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
5557
- const { join: join4 } = await import("path");
5558
- const logPath = join4(dir, ".protect-mcp-log.jsonl");
5559
- const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
6154
+ const { join: join5 } = await import("path");
6155
+ const logPath = join5(dir, ".protect-mcp-log.jsonl");
6156
+ const receiptPath = join5(dir, ".protect-mcp-receipts.jsonl");
5560
6157
  const report = generateReport2(logPath, receiptPath, period);
5561
6158
  let output;
5562
6159
  if (format === "md") {