protect-mcp 0.4.2 → 0.4.4
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/README.md +1 -1
- package/dist/chunk-U76JZVH6.mjs +144 -0
- package/dist/{chunk-7HBHIKLN.mjs → chunk-VF3OCG4D.mjs} +422 -9
- package/dist/{chunk-VWUN6AI6.mjs → chunk-VIA2B65K.mjs} +1 -1
- package/dist/cli.js +690 -93
- package/dist/cli.mjs +183 -7
- package/dist/demo-server.d.mts +106 -0
- package/dist/demo-server.d.ts +106 -0
- package/dist/demo-server.js +32 -0
- package/dist/demo-server.mjs +6 -135
- package/dist/{http-transport-RIVV2RVQ.mjs → http-transport-VLIPOPIC.mjs} +1 -1
- package/dist/index.d.mts +1431 -2
- package/dist/index.d.ts +1431 -2
- package/dist/index.js +1871 -25
- package/dist/index.mjs +1277 -3
- package/package.json +4 -3
- package/policies/cedar/clinejection.cedar +50 -0
- package/policies/cedar/terraform-destroy.cedar +44 -0
- package/policies/clinejection.json +6 -0
- package/policies/data-exfiltration.json +6 -0
- package/policies/financial-safe.json +8 -0
- package/policies/github-mcp-hijack.json +6 -0
- package/policies/terraform-destroy.json +6 -0
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,
|
|
688
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
821
|
-
|
|
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,
|
|
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
|
-
|
|
1218
|
+
import_node_crypto3 = require("crypto");
|
|
859
1219
|
import_node_readline = require("readline");
|
|
860
|
-
|
|
861
|
-
|
|
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,
|
|
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,
|
|
892
|
-
this.receiptFilePath = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4167
|
-
const raw = (0,
|
|
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,
|
|
4188
|
-
const raw = (0,
|
|
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
|
|
4742
|
+
var import_node_fs8;
|
|
4321
4743
|
var init_report = __esm({
|
|
4322
4744
|
"src/report.ts"() {
|
|
4323
4745
|
"use strict";
|
|
4324
|
-
|
|
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
|
|
4757
|
+
var import_node_fs7 = require("fs");
|
|
4336
4758
|
init_policy();
|
|
4337
4759
|
init_admission();
|
|
4338
4760
|
function parseLogFile(path) {
|
|
4339
|
-
const raw = (0,
|
|
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:
|
|
4559
|
-
const { join:
|
|
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 =
|
|
4566
|
-
const keysDir =
|
|
4567
|
-
const keyPath =
|
|
4568
|
-
if (
|
|
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 (!
|
|
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 =
|
|
4598
|
-
if (!
|
|
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:
|
|
4672
|
-
const { join:
|
|
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 =
|
|
4682
|
-
const configPath =
|
|
4683
|
-
const hasConfig =
|
|
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:
|
|
4758
|
-
const { join:
|
|
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 =
|
|
4765
|
-
if (!
|
|
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 =
|
|
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 =
|
|
4855
|
-
if (
|
|
5282
|
+
const evidencePath = join5(dir, ".protect-mcp-evidence.json");
|
|
5283
|
+
if (existsSync6(evidencePath)) {
|
|
4856
5284
|
try {
|
|
4857
|
-
const evidenceRaw =
|
|
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 =
|
|
4867
|
-
if (
|
|
5294
|
+
const keyPath = join5(dir, "keys", "gateway.json");
|
|
5295
|
+
if (existsSync6(keyPath)) {
|
|
4868
5296
|
try {
|
|
4869
|
-
const keyData = JSON.parse(
|
|
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:
|
|
4909
|
-
const { join:
|
|
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 =
|
|
4915
|
-
if (!
|
|
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 =
|
|
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:
|
|
5000
|
-
const { join:
|
|
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 =
|
|
5007
|
-
if (!
|
|
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 =
|
|
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:
|
|
5038
|
-
const { join:
|
|
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] :
|
|
5045
|
-
const receiptsPath =
|
|
5046
|
-
const keyPath =
|
|
5047
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
5062
|
-
const keyData = JSON.parse(
|
|
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:
|
|
5089
|
-
const { join:
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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://
|
|
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:
|
|
5320
|
-
const { join:
|
|
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 =
|
|
5323
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
5518
|
-
if (!
|
|
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:
|
|
5558
|
-
const logPath =
|
|
5559
|
-
const receiptPath =
|
|
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") {
|