protect-mcp 0.3.1 → 0.3.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/LICENSE +68 -0
- package/README.md +42 -2
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/{chunk-U7TMVD3E.mjs → chunk-WDCPUM2O.mjs} +202 -3
- package/dist/cli.js +804 -31
- package/dist/cli.mjs +414 -6
- package/dist/index.d.mts +151 -4
- package/dist/index.d.ts +151 -4
- package/dist/index.js +369 -12
- package/dist/index.mjs +13 -1
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +13 -3
- package/policies/claude-code-hooks.json +18 -0
- package/policies/clinejection.json +45 -0
- package/policies/data-exfiltration.json +52 -0
- package/policies/financial-safe.json +49 -0
- package/policies/github-mcp-hijack.json +54 -0
- package/policies/terraform-destroy.json +50 -0
package/dist/cli.js
CHANGED
|
@@ -2697,6 +2697,178 @@ var init_bundle = __esm({
|
|
|
2697
2697
|
}
|
|
2698
2698
|
});
|
|
2699
2699
|
|
|
2700
|
+
// src/report.ts
|
|
2701
|
+
var report_exports = {};
|
|
2702
|
+
__export(report_exports, {
|
|
2703
|
+
formatReportMarkdown: () => formatReportMarkdown,
|
|
2704
|
+
generateReport: () => generateReport
|
|
2705
|
+
});
|
|
2706
|
+
function generateReport(logPath, receiptPath, periodDays) {
|
|
2707
|
+
const now = /* @__PURE__ */ new Date();
|
|
2708
|
+
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
2709
|
+
const entries = [];
|
|
2710
|
+
if ((0, import_node_fs7.existsSync)(logPath)) {
|
|
2711
|
+
const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
|
|
2712
|
+
for (const line of raw.split("\n")) {
|
|
2713
|
+
const trimmed = line.trim();
|
|
2714
|
+
if (!trimmed) continue;
|
|
2715
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
2716
|
+
try {
|
|
2717
|
+
const parsed = JSON.parse(jsonStr);
|
|
2718
|
+
if (parsed.tool && parsed.decision && parsed.timestamp) {
|
|
2719
|
+
const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
|
|
2720
|
+
if (entryTime >= from.getTime()) {
|
|
2721
|
+
entries.push(parsed);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
} catch {
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
let receiptsSigned = 0;
|
|
2729
|
+
let signerKid = "";
|
|
2730
|
+
let signerIssuer = "";
|
|
2731
|
+
if ((0, import_node_fs7.existsSync)(receiptPath)) {
|
|
2732
|
+
const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
|
|
2733
|
+
for (const line of raw.split("\n")) {
|
|
2734
|
+
const trimmed = line.trim();
|
|
2735
|
+
if (!trimmed) continue;
|
|
2736
|
+
try {
|
|
2737
|
+
const parsed = JSON.parse(trimmed);
|
|
2738
|
+
if (parsed.signature) {
|
|
2739
|
+
receiptsSigned++;
|
|
2740
|
+
if (parsed.kid && !signerKid) signerKid = parsed.kid;
|
|
2741
|
+
if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
|
|
2742
|
+
}
|
|
2743
|
+
} catch {
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
2748
|
+
const tiers = /* @__PURE__ */ new Set();
|
|
2749
|
+
const policyDigests = /* @__PURE__ */ new Map();
|
|
2750
|
+
let allowed = 0;
|
|
2751
|
+
let blocked = 0;
|
|
2752
|
+
let rateLimited = 0;
|
|
2753
|
+
let approvalRequired = 0;
|
|
2754
|
+
for (const entry of entries) {
|
|
2755
|
+
const tool = entry.tool;
|
|
2756
|
+
if (!toolMap.has(tool)) {
|
|
2757
|
+
toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
|
|
2758
|
+
}
|
|
2759
|
+
const tm = toolMap.get(tool);
|
|
2760
|
+
tm.total++;
|
|
2761
|
+
if (entry.decision === "allow") {
|
|
2762
|
+
allowed++;
|
|
2763
|
+
tm.allowed++;
|
|
2764
|
+
} else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
|
|
2765
|
+
rateLimited++;
|
|
2766
|
+
tm.rate_limited++;
|
|
2767
|
+
} else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
|
|
2768
|
+
approvalRequired++;
|
|
2769
|
+
tm.approval_required++;
|
|
2770
|
+
} else {
|
|
2771
|
+
blocked++;
|
|
2772
|
+
tm.blocked++;
|
|
2773
|
+
}
|
|
2774
|
+
if (entry.tier) tiers.add(entry.tier);
|
|
2775
|
+
if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
|
|
2776
|
+
policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
|
|
2780
|
+
at,
|
|
2781
|
+
policy_digest: digest
|
|
2782
|
+
})).sort((a, b) => a.at.localeCompare(b.at));
|
|
2783
|
+
return {
|
|
2784
|
+
generated_at: now.toISOString(),
|
|
2785
|
+
period: { from: from.toISOString(), to: now.toISOString() },
|
|
2786
|
+
signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
|
|
2787
|
+
summary: {
|
|
2788
|
+
total_decisions: entries.length,
|
|
2789
|
+
allowed,
|
|
2790
|
+
blocked,
|
|
2791
|
+
rate_limited: rateLimited,
|
|
2792
|
+
approval_required: approvalRequired,
|
|
2793
|
+
unique_tools: toolMap.size,
|
|
2794
|
+
unique_tiers: tiers.size
|
|
2795
|
+
},
|
|
2796
|
+
tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
|
|
2797
|
+
policy_changes: policyChanges,
|
|
2798
|
+
verification: {
|
|
2799
|
+
receipts_signed: receiptsSigned,
|
|
2800
|
+
receipts_unsigned: entries.length - receiptsSigned,
|
|
2801
|
+
verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
function formatReportMarkdown(report) {
|
|
2806
|
+
const lines = [];
|
|
2807
|
+
lines.push("# ScopeBlind Compliance Report");
|
|
2808
|
+
lines.push("");
|
|
2809
|
+
lines.push(`**Generated:** ${report.generated_at}`);
|
|
2810
|
+
lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
|
|
2811
|
+
if (report.signing_identity) {
|
|
2812
|
+
lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
|
|
2813
|
+
}
|
|
2814
|
+
lines.push("");
|
|
2815
|
+
lines.push("## Summary");
|
|
2816
|
+
lines.push("");
|
|
2817
|
+
lines.push(`| Metric | Value |`);
|
|
2818
|
+
lines.push(`|--------|-------|`);
|
|
2819
|
+
lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
|
|
2820
|
+
lines.push(`| Allowed | ${report.summary.allowed} |`);
|
|
2821
|
+
lines.push(`| Blocked | ${report.summary.blocked} |`);
|
|
2822
|
+
lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
|
|
2823
|
+
lines.push(`| Approval required | ${report.summary.approval_required} |`);
|
|
2824
|
+
lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
|
|
2825
|
+
lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
|
|
2826
|
+
lines.push("");
|
|
2827
|
+
if (report.tool_breakdown.length > 0) {
|
|
2828
|
+
lines.push("## Tool Breakdown");
|
|
2829
|
+
lines.push("");
|
|
2830
|
+
lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
|
|
2831
|
+
lines.push("|------|-------|---------|---------|--------------|----------|");
|
|
2832
|
+
for (const t of report.tool_breakdown) {
|
|
2833
|
+
lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
|
|
2834
|
+
}
|
|
2835
|
+
lines.push("");
|
|
2836
|
+
}
|
|
2837
|
+
if (report.policy_changes.length > 0) {
|
|
2838
|
+
lines.push("## Policy History");
|
|
2839
|
+
lines.push("");
|
|
2840
|
+
lines.push("| Timestamp | Policy Digest |");
|
|
2841
|
+
lines.push("|-----------|--------------|");
|
|
2842
|
+
for (const pc of report.policy_changes) {
|
|
2843
|
+
lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
|
|
2844
|
+
}
|
|
2845
|
+
lines.push("");
|
|
2846
|
+
}
|
|
2847
|
+
lines.push("## Verification");
|
|
2848
|
+
lines.push("");
|
|
2849
|
+
lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
|
|
2850
|
+
lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
|
|
2851
|
+
lines.push("");
|
|
2852
|
+
lines.push("Verify the audit bundle:");
|
|
2853
|
+
lines.push("");
|
|
2854
|
+
lines.push("```bash");
|
|
2855
|
+
lines.push(report.verification.verify_command);
|
|
2856
|
+
lines.push("```");
|
|
2857
|
+
lines.push("");
|
|
2858
|
+
lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
|
|
2859
|
+
lines.push("");
|
|
2860
|
+
lines.push("---");
|
|
2861
|
+
lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
|
|
2862
|
+
return lines.join("\n");
|
|
2863
|
+
}
|
|
2864
|
+
var import_node_fs7;
|
|
2865
|
+
var init_report = __esm({
|
|
2866
|
+
"src/report.ts"() {
|
|
2867
|
+
"use strict";
|
|
2868
|
+
import_node_fs7 = require("fs");
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2700
2872
|
// src/gateway.ts
|
|
2701
2873
|
var import_node_child_process = require("child_process");
|
|
2702
2874
|
var import_node_crypto2 = require("crypto");
|
|
@@ -3019,7 +3191,11 @@ async function initSigning(config) {
|
|
|
3019
3191
|
return warnings;
|
|
3020
3192
|
}
|
|
3021
3193
|
try {
|
|
3022
|
-
|
|
3194
|
+
const moduleName = "@veritasacta/artifacts";
|
|
3195
|
+
artifactsModule = await import(
|
|
3196
|
+
/* @vite-ignore */
|
|
3197
|
+
moduleName
|
|
3198
|
+
);
|
|
3023
3199
|
} catch {
|
|
3024
3200
|
warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
|
|
3025
3201
|
return warnings;
|
|
@@ -3150,6 +3326,28 @@ function formatRequest(context, format) {
|
|
|
3150
3326
|
},
|
|
3151
3327
|
actions: [context.action.operation || "call"]
|
|
3152
3328
|
};
|
|
3329
|
+
case "cedar":
|
|
3330
|
+
return {
|
|
3331
|
+
principal: {
|
|
3332
|
+
type: "Agent",
|
|
3333
|
+
id: context.actor.id || "unknown"
|
|
3334
|
+
},
|
|
3335
|
+
action: {
|
|
3336
|
+
type: "Action",
|
|
3337
|
+
id: `MCP::Tool::${context.action.operation || "call"}`
|
|
3338
|
+
},
|
|
3339
|
+
resource: {
|
|
3340
|
+
type: "Tool",
|
|
3341
|
+
id: context.action.tool
|
|
3342
|
+
},
|
|
3343
|
+
context: {
|
|
3344
|
+
tier: context.actor.tier,
|
|
3345
|
+
manifest_hash: context.actor.manifest_hash || null,
|
|
3346
|
+
service: context.target.service || "default",
|
|
3347
|
+
mode: context.mode,
|
|
3348
|
+
credential_ref: context.credential_ref || null
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3153
3351
|
case "generic":
|
|
3154
3352
|
default:
|
|
3155
3353
|
return context;
|
|
@@ -3179,6 +3377,22 @@ function parseResponse(result, format) {
|
|
|
3179
3377
|
}
|
|
3180
3378
|
}
|
|
3181
3379
|
return { allowed: false, reason: "unrecognized Cerbos response" };
|
|
3380
|
+
case "cedar":
|
|
3381
|
+
if (typeof result.decision === "string") {
|
|
3382
|
+
return {
|
|
3383
|
+
allowed: result.decision === "Allow",
|
|
3384
|
+
reason: result.decision === "Deny" ? `cedar_deny${result.diagnostics ? ": " + JSON.stringify(result.diagnostics) : ""}` : void 0,
|
|
3385
|
+
metadata: result.diagnostics
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
if (Array.isArray(result.results) && result.results.length > 0) {
|
|
3389
|
+
const first = result.results[0];
|
|
3390
|
+
return {
|
|
3391
|
+
allowed: first.decision === "Allow",
|
|
3392
|
+
reason: first.decision === "Deny" ? "cedar_deny" : void 0
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
return { allowed: false, reason: "unrecognized Cedar response" };
|
|
3182
3396
|
case "generic":
|
|
3183
3397
|
default:
|
|
3184
3398
|
return {
|
|
@@ -3596,8 +3810,31 @@ var ProtectGateway = class {
|
|
|
3596
3810
|
async interceptToolCall(request) {
|
|
3597
3811
|
const toolName = request.params?.name || "unknown";
|
|
3598
3812
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
3599
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3600
3813
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
3814
|
+
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
3815
|
+
let effectiveToolPolicy;
|
|
3816
|
+
if (this.config.multiAgent?.enabled) {
|
|
3817
|
+
const paramKid = request.params?._passport_kid;
|
|
3818
|
+
if (paramKid) resolvedAgentKid = paramKid;
|
|
3819
|
+
const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
|
|
3820
|
+
if (agentOverrides && agentOverrides[toolName]) {
|
|
3821
|
+
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
3822
|
+
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
3823
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
3824
|
+
if (this.config.enforce) {
|
|
3825
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
3826
|
+
}
|
|
3827
|
+
return null;
|
|
3828
|
+
} else {
|
|
3829
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3830
|
+
}
|
|
3831
|
+
if (this.config.verbose && resolvedAgentKid) {
|
|
3832
|
+
this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
|
|
3833
|
+
}
|
|
3834
|
+
} else {
|
|
3835
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3836
|
+
}
|
|
3837
|
+
const toolPolicy = effectiveToolPolicy;
|
|
3601
3838
|
let credentialRef;
|
|
3602
3839
|
if (this.config.credentials) {
|
|
3603
3840
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -3772,6 +4009,137 @@ var ProtectGateway = class {
|
|
|
3772
4009
|
}
|
|
3773
4010
|
};
|
|
3774
4011
|
|
|
4012
|
+
// src/simulate.ts
|
|
4013
|
+
var import_node_fs6 = require("fs");
|
|
4014
|
+
function parseLogFile(path) {
|
|
4015
|
+
const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
4016
|
+
const entries = [];
|
|
4017
|
+
for (const line of raw.split("\n")) {
|
|
4018
|
+
const trimmed = line.trim();
|
|
4019
|
+
if (!trimmed) continue;
|
|
4020
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
4021
|
+
try {
|
|
4022
|
+
const parsed = JSON.parse(jsonStr);
|
|
4023
|
+
if (parsed.tool && parsed.decision) {
|
|
4024
|
+
entries.push(parsed);
|
|
4025
|
+
}
|
|
4026
|
+
} catch {
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
return entries;
|
|
4030
|
+
}
|
|
4031
|
+
function simulate(entries, policy, tier = "unknown") {
|
|
4032
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
4033
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
4034
|
+
const totals = {
|
|
4035
|
+
allow: 0,
|
|
4036
|
+
block: 0,
|
|
4037
|
+
rate_limited: 0,
|
|
4038
|
+
require_approval: 0,
|
|
4039
|
+
tier_insufficient: 0
|
|
4040
|
+
};
|
|
4041
|
+
const originalTotals = { allow: 0, deny: 0 };
|
|
4042
|
+
const changes = [];
|
|
4043
|
+
for (const entry of entries) {
|
|
4044
|
+
const toolName = entry.tool;
|
|
4045
|
+
const toolPolicy = getToolPolicy(toolName, policy);
|
|
4046
|
+
if (entry.decision === "allow") {
|
|
4047
|
+
originalTotals.allow++;
|
|
4048
|
+
} else {
|
|
4049
|
+
originalTotals.deny++;
|
|
4050
|
+
}
|
|
4051
|
+
let newDecision;
|
|
4052
|
+
if (toolPolicy.block) {
|
|
4053
|
+
newDecision = "block";
|
|
4054
|
+
} else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
|
|
4055
|
+
newDecision = "tier_insufficient";
|
|
4056
|
+
} else if (toolPolicy.require_approval) {
|
|
4057
|
+
newDecision = "require_approval";
|
|
4058
|
+
} else if (toolPolicy.rate_limit) {
|
|
4059
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
4060
|
+
const result = checkRateLimit(toolName, limit, rateLimitStore);
|
|
4061
|
+
newDecision = result.allowed ? "allow" : "rate_limited";
|
|
4062
|
+
} else {
|
|
4063
|
+
newDecision = "allow";
|
|
4064
|
+
}
|
|
4065
|
+
totals[newDecision]++;
|
|
4066
|
+
if (!toolResults.has(toolName)) {
|
|
4067
|
+
toolResults.set(toolName, {
|
|
4068
|
+
tool: toolName,
|
|
4069
|
+
calls: 0,
|
|
4070
|
+
results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
|
|
4071
|
+
original: { allow: 0, deny: 0 }
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
const tr = toolResults.get(toolName);
|
|
4075
|
+
tr.calls++;
|
|
4076
|
+
tr.results[newDecision]++;
|
|
4077
|
+
if (entry.decision === "allow") {
|
|
4078
|
+
tr.original.allow++;
|
|
4079
|
+
} else {
|
|
4080
|
+
tr.original.deny++;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
for (const [tool, result] of toolResults) {
|
|
4084
|
+
const wasAllBlocked = result.original.allow === 0;
|
|
4085
|
+
const nowAllBlocked = result.results.allow === 0;
|
|
4086
|
+
const wasAllAllowed = result.original.deny === 0;
|
|
4087
|
+
if (wasAllAllowed && result.results.block > 0) {
|
|
4088
|
+
changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
|
|
4089
|
+
}
|
|
4090
|
+
if (wasAllAllowed && result.results.rate_limited > 0) {
|
|
4091
|
+
changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
|
|
4092
|
+
}
|
|
4093
|
+
if (wasAllAllowed && result.results.require_approval > 0) {
|
|
4094
|
+
changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
|
|
4095
|
+
}
|
|
4096
|
+
if (wasAllAllowed && result.results.tier_insufficient > 0) {
|
|
4097
|
+
changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
|
|
4098
|
+
}
|
|
4099
|
+
if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
|
|
4100
|
+
changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
return {
|
|
4104
|
+
policy_file: "",
|
|
4105
|
+
log_file: "",
|
|
4106
|
+
total_calls: entries.length,
|
|
4107
|
+
results: totals,
|
|
4108
|
+
original: originalTotals,
|
|
4109
|
+
tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
|
|
4110
|
+
changes
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
function formatSimulation(summary) {
|
|
4114
|
+
const lines = [];
|
|
4115
|
+
lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
|
|
4116
|
+
`);
|
|
4117
|
+
const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
|
|
4118
|
+
for (const tr of summary.tool_breakdown) {
|
|
4119
|
+
const parts = [];
|
|
4120
|
+
if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
|
|
4121
|
+
if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
|
|
4122
|
+
if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
|
|
4123
|
+
if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
|
|
4124
|
+
if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
|
|
4125
|
+
const originalParts = [];
|
|
4126
|
+
if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
|
|
4127
|
+
if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
|
|
4128
|
+
lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
|
|
4129
|
+
}
|
|
4130
|
+
lines.push("");
|
|
4131
|
+
lines.push(`Summary: ${summary.results.allow} allow, ${summary.results.block} blocked, ${summary.results.rate_limited} rate_limited, ${summary.results.require_approval} require_approval, ${summary.results.tier_insufficient} tier_insufficient`);
|
|
4132
|
+
lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
|
|
4133
|
+
if (summary.changes.length > 0) {
|
|
4134
|
+
lines.push("");
|
|
4135
|
+
lines.push("Changes:");
|
|
4136
|
+
for (const change of summary.changes) {
|
|
4137
|
+
lines.push(` \u2022 ${change}`);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
return lines.join("\n");
|
|
4141
|
+
}
|
|
4142
|
+
|
|
3775
4143
|
// src/cli.ts
|
|
3776
4144
|
function printHelp() {
|
|
3777
4145
|
process.stderr.write(`
|
|
@@ -3779,12 +4147,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
|
3779
4147
|
|
|
3780
4148
|
Usage:
|
|
3781
4149
|
protect-mcp [options] -- <command> [args...]
|
|
4150
|
+
protect-mcp quickstart
|
|
3782
4151
|
protect-mcp init [--dir <path>]
|
|
3783
4152
|
protect-mcp demo
|
|
4153
|
+
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
3784
4154
|
protect-mcp status [--dir <path>]
|
|
3785
4155
|
protect-mcp digest [--today] [--dir <path>]
|
|
3786
4156
|
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
3787
4157
|
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
4158
|
+
protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
|
|
4159
|
+
protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
|
|
3788
4160
|
|
|
3789
4161
|
Options:
|
|
3790
4162
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -3794,19 +4166,22 @@ Options:
|
|
|
3794
4166
|
--help Show this help
|
|
3795
4167
|
|
|
3796
4168
|
Commands:
|
|
4169
|
+
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
3797
4170
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
3798
4171
|
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
4172
|
+
trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
|
|
3799
4173
|
status Show tool call statistics from the local decision log
|
|
3800
4174
|
digest Generate a human-readable summary of agent activity
|
|
3801
4175
|
receipts Show recent persisted signed receipts
|
|
3802
4176
|
bundle Export an offline-verifiable audit bundle
|
|
3803
4177
|
|
|
3804
4178
|
Examples:
|
|
4179
|
+
protect-mcp quickstart
|
|
3805
4180
|
protect-mcp -- node my-server.js
|
|
3806
4181
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
3807
|
-
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
3808
4182
|
protect-mcp init
|
|
3809
4183
|
protect-mcp demo
|
|
4184
|
+
protect-mcp trace sha256:abc123 --depth 5
|
|
3810
4185
|
protect-mcp status
|
|
3811
4186
|
protect-mcp digest --today
|
|
3812
4187
|
protect-mcp receipts --last 10
|
|
@@ -3854,7 +4229,7 @@ function parseArgs(argv) {
|
|
|
3854
4229
|
return { policyPath, slug, enforce, verbose, childCommand };
|
|
3855
4230
|
}
|
|
3856
4231
|
async function handleInit(argv) {
|
|
3857
|
-
const { writeFileSync: writeFileSync2, existsSync:
|
|
4232
|
+
const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
|
|
3858
4233
|
const { join: join4 } = await import("path");
|
|
3859
4234
|
let dir = process.cwd();
|
|
3860
4235
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -3864,17 +4239,14 @@ async function handleInit(argv) {
|
|
|
3864
4239
|
const configPath = join4(dir, "protect-mcp.json");
|
|
3865
4240
|
const keysDir = join4(dir, "keys");
|
|
3866
4241
|
const keyPath = join4(keysDir, "gateway.json");
|
|
3867
|
-
if (
|
|
4242
|
+
if (existsSync5(configPath)) {
|
|
3868
4243
|
process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
|
|
3869
4244
|
`);
|
|
3870
4245
|
process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
|
|
3871
4246
|
process.exit(1);
|
|
3872
4247
|
}
|
|
3873
4248
|
let keypair;
|
|
3874
|
-
|
|
3875
|
-
const artifacts = await import("@veritasacta/artifacts");
|
|
3876
|
-
keypair = artifacts.generateKeypair();
|
|
3877
|
-
} catch {
|
|
4249
|
+
{
|
|
3878
4250
|
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
3879
4251
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
3880
4252
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
@@ -3886,7 +4258,7 @@ async function handleInit(argv) {
|
|
|
3886
4258
|
kid: "generated"
|
|
3887
4259
|
};
|
|
3888
4260
|
}
|
|
3889
|
-
if (!
|
|
4261
|
+
if (!existsSync5(keysDir)) {
|
|
3890
4262
|
mkdirSync(keysDir, { recursive: true });
|
|
3891
4263
|
}
|
|
3892
4264
|
writeFileSync2(keyPath, JSON.stringify({
|
|
@@ -3897,7 +4269,7 @@ async function handleInit(argv) {
|
|
|
3897
4269
|
warning: "KEEP THIS FILE SECRET. Never commit to version control."
|
|
3898
4270
|
}, null, 2) + "\n");
|
|
3899
4271
|
const gitignorePath = join4(keysDir, ".gitignore");
|
|
3900
|
-
if (!
|
|
4272
|
+
if (!existsSync5(gitignorePath)) {
|
|
3901
4273
|
writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
|
|
3902
4274
|
}
|
|
3903
4275
|
const config = {
|
|
@@ -3970,13 +4342,13 @@ Add --enforce when ready to block policy violations.
|
|
|
3970
4342
|
`);
|
|
3971
4343
|
}
|
|
3972
4344
|
async function handleDemo() {
|
|
3973
|
-
const { existsSync:
|
|
4345
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3974
4346
|
const { join: join4, dirname, resolve } = await import("path");
|
|
3975
4347
|
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
3976
4348
|
const cliDir = dirname(cliPath);
|
|
3977
4349
|
const demoServerPath = join4(cliDir, "demo-server.js");
|
|
3978
4350
|
const configPath = join4(process.cwd(), "protect-mcp.json");
|
|
3979
|
-
const hasConfig =
|
|
4351
|
+
const hasConfig = existsSync5(configPath);
|
|
3980
4352
|
if (!hasConfig) {
|
|
3981
4353
|
process.stderr.write(`
|
|
3982
4354
|
${bold("protect-mcp demo")}
|
|
@@ -4050,7 +4422,7 @@ Starting demo server with 5 tools...
|
|
|
4050
4422
|
await gateway.start();
|
|
4051
4423
|
}
|
|
4052
4424
|
async function handleStatus2(argv) {
|
|
4053
|
-
const { readFileSync:
|
|
4425
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4054
4426
|
const { join: join4 } = await import("path");
|
|
4055
4427
|
let dir = process.cwd();
|
|
4056
4428
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -4058,7 +4430,7 @@ async function handleStatus2(argv) {
|
|
|
4058
4430
|
dir = argv[dirIdx + 1];
|
|
4059
4431
|
}
|
|
4060
4432
|
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4061
|
-
if (!
|
|
4433
|
+
if (!existsSync5(logPath)) {
|
|
4062
4434
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
4063
4435
|
|
|
4064
4436
|
`);
|
|
@@ -4068,7 +4440,7 @@ async function handleStatus2(argv) {
|
|
|
4068
4440
|
`);
|
|
4069
4441
|
process.exit(0);
|
|
4070
4442
|
}
|
|
4071
|
-
const raw =
|
|
4443
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
4072
4444
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4073
4445
|
if (lines.length === 0) {
|
|
4074
4446
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
@@ -4148,9 +4520,9 @@ ${bold("protect-mcp status")}
|
|
|
4148
4520
|
`);
|
|
4149
4521
|
}
|
|
4150
4522
|
const evidencePath = join4(dir, ".protect-mcp-evidence.json");
|
|
4151
|
-
if (
|
|
4523
|
+
if (existsSync5(evidencePath)) {
|
|
4152
4524
|
try {
|
|
4153
|
-
const evidenceRaw =
|
|
4525
|
+
const evidenceRaw = readFileSync7(evidencePath, "utf-8");
|
|
4154
4526
|
const evidence = JSON.parse(evidenceRaw);
|
|
4155
4527
|
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
4156
4528
|
process.stdout.write(`
|
|
@@ -4160,9 +4532,9 @@ ${bold("protect-mcp status")}
|
|
|
4160
4532
|
}
|
|
4161
4533
|
}
|
|
4162
4534
|
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4163
|
-
if (
|
|
4535
|
+
if (existsSync5(keyPath)) {
|
|
4164
4536
|
try {
|
|
4165
|
-
const keyData = JSON.parse(
|
|
4537
|
+
const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
|
|
4166
4538
|
if (keyData.publicKey) {
|
|
4167
4539
|
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4168
4540
|
process.stdout.write(`
|
|
@@ -4201,21 +4573,21 @@ function yellow(s) {
|
|
|
4201
4573
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
4202
4574
|
}
|
|
4203
4575
|
async function handleDigest(argv) {
|
|
4204
|
-
const { readFileSync:
|
|
4576
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4205
4577
|
const { join: join4 } = await import("path");
|
|
4206
4578
|
let dir = process.cwd();
|
|
4207
4579
|
const dirIdx = argv.indexOf("--dir");
|
|
4208
4580
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4209
4581
|
const today = argv.includes("--today");
|
|
4210
4582
|
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4211
|
-
if (!
|
|
4583
|
+
if (!existsSync5(logPath)) {
|
|
4212
4584
|
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4213
4585
|
|
|
4214
4586
|
No log file found. Run protect-mcp first.
|
|
4215
4587
|
`);
|
|
4216
4588
|
process.exit(0);
|
|
4217
4589
|
}
|
|
4218
|
-
const raw =
|
|
4590
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
4219
4591
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4220
4592
|
let entries = [];
|
|
4221
4593
|
for (const line of lines) {
|
|
@@ -4292,7 +4664,7 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
|
4292
4664
|
`);
|
|
4293
4665
|
}
|
|
4294
4666
|
async function handleReceipts2(argv) {
|
|
4295
|
-
const { readFileSync:
|
|
4667
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4296
4668
|
const { join: join4 } = await import("path");
|
|
4297
4669
|
let dir = process.cwd();
|
|
4298
4670
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -4300,14 +4672,14 @@ async function handleReceipts2(argv) {
|
|
|
4300
4672
|
const lastIdx = argv.indexOf("--last");
|
|
4301
4673
|
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
4302
4674
|
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4303
|
-
if (!
|
|
4675
|
+
if (!existsSync5(receiptsPath)) {
|
|
4304
4676
|
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
4305
4677
|
|
|
4306
4678
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4307
4679
|
`);
|
|
4308
4680
|
process.exit(0);
|
|
4309
4681
|
}
|
|
4310
|
-
const raw =
|
|
4682
|
+
const raw = readFileSync7(receiptsPath, "utf-8");
|
|
4311
4683
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4312
4684
|
const recent = lines.slice(-count);
|
|
4313
4685
|
process.stdout.write(`
|
|
@@ -4330,7 +4702,7 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
|
4330
4702
|
`);
|
|
4331
4703
|
}
|
|
4332
4704
|
async function handleBundle(argv) {
|
|
4333
|
-
const { readFileSync:
|
|
4705
|
+
const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
|
|
4334
4706
|
const { join: join4 } = await import("path");
|
|
4335
4707
|
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
4336
4708
|
let dir = process.cwd();
|
|
@@ -4340,22 +4712,22 @@ async function handleBundle(argv) {
|
|
|
4340
4712
|
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
|
|
4341
4713
|
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4342
4714
|
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4343
|
-
if (!
|
|
4715
|
+
if (!existsSync5(receiptsPath)) {
|
|
4344
4716
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4345
4717
|
|
|
4346
4718
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4347
4719
|
`);
|
|
4348
4720
|
process.exit(0);
|
|
4349
4721
|
}
|
|
4350
|
-
if (!
|
|
4722
|
+
if (!existsSync5(keyPath)) {
|
|
4351
4723
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4352
4724
|
|
|
4353
4725
|
No key file found at ${keyPath}
|
|
4354
4726
|
`);
|
|
4355
4727
|
process.exit(1);
|
|
4356
4728
|
}
|
|
4357
|
-
const receipts =
|
|
4358
|
-
const keyData = JSON.parse(
|
|
4729
|
+
const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4730
|
+
const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
|
|
4359
4731
|
const bundle = createAuditBundle2({
|
|
4360
4732
|
tenant: keyData.issuer || "protect-mcp",
|
|
4361
4733
|
receipts,
|
|
@@ -4380,12 +4752,322 @@ ${bold("protect-mcp bundle")}
|
|
|
4380
4752
|
|
|
4381
4753
|
`);
|
|
4382
4754
|
}
|
|
4755
|
+
async function handleQuickstart() {
|
|
4756
|
+
const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
|
|
4757
|
+
const { join: join4 } = await import("path");
|
|
4758
|
+
const { tmpdir } = await import("os");
|
|
4759
|
+
const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
|
|
4760
|
+
process.stdout.write(`
|
|
4761
|
+
${bold("protect-mcp quickstart")}
|
|
4762
|
+
`);
|
|
4763
|
+
process.stdout.write(`${"\u2500".repeat(50)}
|
|
4764
|
+
|
|
4765
|
+
`);
|
|
4766
|
+
process.stdout.write(` This will:
|
|
4767
|
+
`);
|
|
4768
|
+
process.stdout.write(` 1. Generate an Ed25519 signing keypair
|
|
4769
|
+
`);
|
|
4770
|
+
process.stdout.write(` 2. Create a shadow-mode policy
|
|
4771
|
+
`);
|
|
4772
|
+
process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
|
|
4773
|
+
`);
|
|
4774
|
+
process.stdout.write(` 4. Log signed receipts for every tool call
|
|
4775
|
+
|
|
4776
|
+
`);
|
|
4777
|
+
process.stdout.write(` Working dir: ${dir}
|
|
4778
|
+
|
|
4779
|
+
`);
|
|
4780
|
+
const keysDir = join4(dir, "keys");
|
|
4781
|
+
mkdirSync(keysDir, { recursive: true });
|
|
4782
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
4783
|
+
let keypair;
|
|
4784
|
+
try {
|
|
4785
|
+
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
4786
|
+
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
4787
|
+
const privateKey = randomBytes3(32);
|
|
4788
|
+
const publicKey = ed255192.getPublicKey(privateKey);
|
|
4789
|
+
keypair = {
|
|
4790
|
+
privateKey: bytesToHex2(privateKey),
|
|
4791
|
+
publicKey: bytesToHex2(publicKey),
|
|
4792
|
+
kid: `quickstart-${Date.now()}`
|
|
4793
|
+
};
|
|
4794
|
+
} catch {
|
|
4795
|
+
keypair = {
|
|
4796
|
+
privateKey: randomBytes3(32).toString("hex"),
|
|
4797
|
+
publicKey: randomBytes3(32).toString("hex"),
|
|
4798
|
+
kid: `quickstart-${Date.now()}`
|
|
4799
|
+
};
|
|
4800
|
+
}
|
|
4801
|
+
writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
|
|
4802
|
+
privateKey: keypair.privateKey,
|
|
4803
|
+
publicKey: keypair.publicKey,
|
|
4804
|
+
kid: keypair.kid,
|
|
4805
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4806
|
+
}, null, 2) + "\n");
|
|
4807
|
+
const configPath = join4(dir, "protect-mcp.json");
|
|
4808
|
+
const config = {
|
|
4809
|
+
tools: {
|
|
4810
|
+
"*": { rate_limit: "100/hour" },
|
|
4811
|
+
"delete_file": { block: true }
|
|
4812
|
+
},
|
|
4813
|
+
default_tier: "unknown",
|
|
4814
|
+
signing: {
|
|
4815
|
+
key_path: join4(keysDir, "gateway.json"),
|
|
4816
|
+
issuer: "protect-mcp-quickstart",
|
|
4817
|
+
enabled: true
|
|
4818
|
+
}
|
|
4819
|
+
};
|
|
4820
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
4821
|
+
process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
|
|
4822
|
+
`);
|
|
4823
|
+
process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
|
|
4824
|
+
`);
|
|
4825
|
+
process.stdout.write(` \u2713 Signing enabled (Ed25519)
|
|
4826
|
+
|
|
4827
|
+
`);
|
|
4828
|
+
process.stdout.write(`${bold("Starting demo server...")}
|
|
4829
|
+
|
|
4830
|
+
`);
|
|
4831
|
+
process.stdout.write(` Every tool call will produce a signed receipt.
|
|
4832
|
+
`);
|
|
4833
|
+
process.stdout.write(` Try it with Claude Desktop or any MCP client.
|
|
4834
|
+
|
|
4835
|
+
`);
|
|
4836
|
+
process.stdout.write(` ${bold("To use in production:")}
|
|
4837
|
+
`);
|
|
4838
|
+
process.stdout.write(` 1. Copy ${configPath} to your project
|
|
4839
|
+
`);
|
|
4840
|
+
process.stdout.write(` 2. Edit tool policies to match your server
|
|
4841
|
+
`);
|
|
4842
|
+
process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
|
|
4843
|
+
|
|
4844
|
+
`);
|
|
4845
|
+
process.stdout.write(`${"\u2500".repeat(50)}
|
|
4846
|
+
|
|
4847
|
+
`);
|
|
4848
|
+
process.env.PROTECT_MCP_CONFIG = configPath;
|
|
4849
|
+
await handleDemo();
|
|
4850
|
+
}
|
|
4851
|
+
async function handleTrace(argv) {
|
|
4852
|
+
const receiptId = argv[0];
|
|
4853
|
+
if (!receiptId) {
|
|
4854
|
+
process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
|
|
4855
|
+
process.exit(1);
|
|
4856
|
+
}
|
|
4857
|
+
let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
|
|
4858
|
+
let depth = 3;
|
|
4859
|
+
for (let i = 1; i < argv.length; i++) {
|
|
4860
|
+
if (argv[i] === "--endpoint" && argv[i + 1]) {
|
|
4861
|
+
endpoint = argv[++i];
|
|
4862
|
+
} else if (argv[i] === "--depth" && argv[i + 1]) {
|
|
4863
|
+
depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
process.stdout.write(`
|
|
4867
|
+
${bold("protect-mcp trace")}
|
|
4868
|
+
`);
|
|
4869
|
+
process.stdout.write(`${"\u2500".repeat(60)}
|
|
4870
|
+
|
|
4871
|
+
`);
|
|
4872
|
+
process.stdout.write(` Root: ${receiptId}
|
|
4873
|
+
`);
|
|
4874
|
+
process.stdout.write(` Endpoint: ${endpoint}
|
|
4875
|
+
`);
|
|
4876
|
+
process.stdout.write(` Depth: ${depth}
|
|
4877
|
+
|
|
4878
|
+
`);
|
|
4879
|
+
const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
|
|
4880
|
+
let graphData;
|
|
4881
|
+
try {
|
|
4882
|
+
const resp = await fetch(url);
|
|
4883
|
+
if (!resp.ok) {
|
|
4884
|
+
const body = await resp.text();
|
|
4885
|
+
process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
|
|
4886
|
+
`);
|
|
4887
|
+
process.exit(1);
|
|
4888
|
+
}
|
|
4889
|
+
graphData = await resp.json();
|
|
4890
|
+
} catch (err) {
|
|
4891
|
+
process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
|
|
4892
|
+
`);
|
|
4893
|
+
process.stderr.write(`[PROTECT_MCP] Trying local receipts...
|
|
4894
|
+
|
|
4895
|
+
`);
|
|
4896
|
+
await traceLocal(receiptId);
|
|
4897
|
+
return;
|
|
4898
|
+
}
|
|
4899
|
+
if (!graphData.nodes || graphData.nodes.length === 0) {
|
|
4900
|
+
process.stdout.write(` No receipts found for ${receiptId}
|
|
4901
|
+
|
|
4902
|
+
`);
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
|
|
4906
|
+
|
|
4907
|
+
`);
|
|
4908
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
4909
|
+
for (const node of graphData.nodes) {
|
|
4910
|
+
nodeMap.set(node.receipt_id, node);
|
|
4911
|
+
}
|
|
4912
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
4913
|
+
for (const edge of graphData.edges) {
|
|
4914
|
+
if (!childMap.has(edge.from)) childMap.set(edge.from, []);
|
|
4915
|
+
childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
|
|
4916
|
+
}
|
|
4917
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
4918
|
+
function renderNode(id, prefix, isLast) {
|
|
4919
|
+
const node = nodeMap.get(id);
|
|
4920
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4921
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
4922
|
+
const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
|
|
4923
|
+
const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
|
|
4924
|
+
const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
|
|
4925
|
+
const type = node?.receipt_type?.replace("acta:", "") || "unknown";
|
|
4926
|
+
process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
|
|
4927
|
+
`);
|
|
4928
|
+
if (rendered.has(id)) {
|
|
4929
|
+
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
|
|
4930
|
+
`);
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
rendered.add(id);
|
|
4934
|
+
const children = childMap.get(id) || [];
|
|
4935
|
+
for (let i = 0; i < children.length; i++) {
|
|
4936
|
+
const child = children[i];
|
|
4937
|
+
const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
|
|
4938
|
+
process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
|
|
4939
|
+
`);
|
|
4940
|
+
renderNode(child.to, prefix + childPrefix, i === children.length - 1);
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
const rootNode = nodeMap.get(receiptId);
|
|
4944
|
+
if (rootNode) {
|
|
4945
|
+
const typeEmoji = getTypeEmoji(rootNode.receipt_type);
|
|
4946
|
+
const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
|
|
4947
|
+
const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
|
|
4948
|
+
process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
|
|
4949
|
+
`);
|
|
4950
|
+
rendered.add(receiptId);
|
|
4951
|
+
const children = childMap.get(receiptId) || [];
|
|
4952
|
+
for (let i = 0; i < children.length; i++) {
|
|
4953
|
+
const child = children[i];
|
|
4954
|
+
const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
|
|
4955
|
+
process.stdout.write(` ${edgeLabel}
|
|
4956
|
+
`);
|
|
4957
|
+
renderNode(child.to, " ", i === children.length - 1);
|
|
4958
|
+
}
|
|
4959
|
+
const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
|
|
4960
|
+
if (incomingEdges.length > 0) {
|
|
4961
|
+
process.stdout.write(`
|
|
4962
|
+
${bold("Incoming edges:")}
|
|
4963
|
+
`);
|
|
4964
|
+
for (const edge of incomingEdges) {
|
|
4965
|
+
const fromNode = nodeMap.get(edge.from);
|
|
4966
|
+
const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
|
|
4967
|
+
process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
|
|
4968
|
+
`);
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
} else {
|
|
4972
|
+
for (const node of graphData.nodes) {
|
|
4973
|
+
const typeEmoji = getTypeEmoji(node.receipt_type);
|
|
4974
|
+
const type = node.receipt_type?.replace("acta:", "") || "unknown";
|
|
4975
|
+
process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
|
|
4976
|
+
`);
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
process.stdout.write(`
|
|
4980
|
+
${"\u2500".repeat(60)}
|
|
4981
|
+
`);
|
|
4982
|
+
process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
|
|
4983
|
+
|
|
4984
|
+
`);
|
|
4985
|
+
}
|
|
4986
|
+
async function traceLocal(receiptId) {
|
|
4987
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4988
|
+
const { join: join4 } = await import("path");
|
|
4989
|
+
const dir = process.cwd();
|
|
4990
|
+
const receiptsDir = join4(dir, ".protect-mcp", "receipts");
|
|
4991
|
+
if (!existsSync5(receiptsDir)) {
|
|
4992
|
+
process.stdout.write(` No local receipts found in ${receiptsDir}
|
|
4993
|
+
|
|
4994
|
+
`);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
const { readdirSync } = await import("fs");
|
|
4998
|
+
const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
|
|
4999
|
+
process.stdout.write(` Scanning ${files.length} local receipts...
|
|
5000
|
+
|
|
5001
|
+
`);
|
|
5002
|
+
const receipts = [];
|
|
5003
|
+
for (const file of files) {
|
|
5004
|
+
try {
|
|
5005
|
+
const content = readFileSync7(join4(receiptsDir, file), "utf-8");
|
|
5006
|
+
const receipt = JSON.parse(content);
|
|
5007
|
+
receipts.push(receipt);
|
|
5008
|
+
} catch {
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
const match = receipts.find(
|
|
5012
|
+
(r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
|
|
5013
|
+
);
|
|
5014
|
+
if (match) {
|
|
5015
|
+
const claims = match.signed_claims?.claims || match;
|
|
5016
|
+
process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
|
|
5017
|
+
`);
|
|
5018
|
+
process.stdout.write(` Event: ${claims.event_id || "?"}
|
|
5019
|
+
`);
|
|
5020
|
+
process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
|
|
5021
|
+
`);
|
|
5022
|
+
process.stdout.write(` Time: ${claims.event_time || "?"}
|
|
5023
|
+
`);
|
|
5024
|
+
if (claims.edges && claims.edges.length > 0) {
|
|
5025
|
+
process.stdout.write(`
|
|
5026
|
+
${bold("Edges:")}
|
|
5027
|
+
`);
|
|
5028
|
+
for (const edge of claims.edges) {
|
|
5029
|
+
process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
|
|
5030
|
+
`);
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
} else {
|
|
5034
|
+
process.stdout.write(` Receipt ${receiptId} not found locally.
|
|
5035
|
+
`);
|
|
5036
|
+
}
|
|
5037
|
+
process.stdout.write("\n");
|
|
5038
|
+
}
|
|
5039
|
+
function getTypeEmoji(type) {
|
|
5040
|
+
switch (type) {
|
|
5041
|
+
case "acta:observation":
|
|
5042
|
+
return "\u{1F441} ";
|
|
5043
|
+
case "acta:policy-load":
|
|
5044
|
+
return "\u{1F4CB}";
|
|
5045
|
+
case "acta:approval":
|
|
5046
|
+
return "\u2705";
|
|
5047
|
+
case "acta:decision":
|
|
5048
|
+
return "\u2696\uFE0F ";
|
|
5049
|
+
case "acta:execution":
|
|
5050
|
+
return "\u26A1";
|
|
5051
|
+
case "acta:outcome":
|
|
5052
|
+
return "\u{1F4E6}";
|
|
5053
|
+
case "acta:delegation":
|
|
5054
|
+
return "\u{1F91D}";
|
|
5055
|
+
case "acta:capability-attestation":
|
|
5056
|
+
return "\u{1F3C5}";
|
|
5057
|
+
default:
|
|
5058
|
+
return "\u{1F4C4}";
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
4383
5061
|
async function main() {
|
|
4384
5062
|
const args = process.argv.slice(2);
|
|
4385
5063
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
4386
5064
|
printHelp();
|
|
4387
5065
|
process.exit(0);
|
|
4388
5066
|
}
|
|
5067
|
+
if (args[0] === "quickstart") {
|
|
5068
|
+
await handleQuickstart();
|
|
5069
|
+
return;
|
|
5070
|
+
}
|
|
4389
5071
|
if (args[0] === "init") {
|
|
4390
5072
|
await handleInit(args.slice(1));
|
|
4391
5073
|
process.exit(0);
|
|
@@ -4410,6 +5092,18 @@ async function main() {
|
|
|
4410
5092
|
await handleBundle(args.slice(1));
|
|
4411
5093
|
process.exit(0);
|
|
4412
5094
|
}
|
|
5095
|
+
if (args[0] === "trace") {
|
|
5096
|
+
await handleTrace(args.slice(1));
|
|
5097
|
+
process.exit(0);
|
|
5098
|
+
}
|
|
5099
|
+
if (args[0] === "simulate") {
|
|
5100
|
+
await handleSimulate(args.slice(1));
|
|
5101
|
+
process.exit(0);
|
|
5102
|
+
}
|
|
5103
|
+
if (args[0] === "report") {
|
|
5104
|
+
await handleReport(args.slice(1));
|
|
5105
|
+
process.exit(0);
|
|
5106
|
+
}
|
|
4413
5107
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
4414
5108
|
let policy = null;
|
|
4415
5109
|
let policyDigest = "none";
|
|
@@ -4460,6 +5154,85 @@ async function main() {
|
|
|
4460
5154
|
const gateway = new ProtectGateway(config);
|
|
4461
5155
|
await gateway.start();
|
|
4462
5156
|
}
|
|
5157
|
+
async function handleSimulate(args) {
|
|
5158
|
+
let policyPath = "";
|
|
5159
|
+
let logPath = ".protect-mcp-log.jsonl";
|
|
5160
|
+
let tier = "unknown";
|
|
5161
|
+
let jsonOutput = false;
|
|
5162
|
+
for (let i = 0; i < args.length; i++) {
|
|
5163
|
+
if (args[i] === "--policy" && args[i + 1]) {
|
|
5164
|
+
policyPath = args[++i];
|
|
5165
|
+
} else if (args[i] === "--log" && args[i + 1]) {
|
|
5166
|
+
logPath = args[++i];
|
|
5167
|
+
} else if (args[i] === "--tier" && args[i + 1]) {
|
|
5168
|
+
tier = args[++i];
|
|
5169
|
+
} else if (args[i] === "--json") {
|
|
5170
|
+
jsonOutput = true;
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
if (!policyPath) {
|
|
5174
|
+
process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
|
|
5175
|
+
process.exit(1);
|
|
5176
|
+
}
|
|
5177
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
5178
|
+
if (!existsSync5(logPath)) {
|
|
5179
|
+
process.stderr.write(`Log file not found: ${logPath}
|
|
5180
|
+
`);
|
|
5181
|
+
process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
|
|
5182
|
+
process.exit(1);
|
|
5183
|
+
}
|
|
5184
|
+
const { policy } = loadPolicy(policyPath);
|
|
5185
|
+
const entries = parseLogFile(logPath);
|
|
5186
|
+
if (entries.length === 0) {
|
|
5187
|
+
process.stderr.write("No tool call entries found in log file.\n");
|
|
5188
|
+
process.exit(1);
|
|
5189
|
+
}
|
|
5190
|
+
const summary = simulate(entries, policy, tier);
|
|
5191
|
+
summary.policy_file = policyPath;
|
|
5192
|
+
summary.log_file = logPath;
|
|
5193
|
+
if (jsonOutput) {
|
|
5194
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
5195
|
+
} else {
|
|
5196
|
+
process.stdout.write(formatSimulation(summary) + "\n");
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
async function handleReport(args) {
|
|
5200
|
+
let period = 30;
|
|
5201
|
+
let format = "json";
|
|
5202
|
+
let outputPath = "";
|
|
5203
|
+
let dir = process.cwd();
|
|
5204
|
+
for (let i = 0; i < args.length; i++) {
|
|
5205
|
+
if (args[i] === "--period" && args[i + 1]) {
|
|
5206
|
+
const match = args[++i].match(/^(\d+)d$/);
|
|
5207
|
+
if (match) period = parseInt(match[1], 10);
|
|
5208
|
+
} else if (args[i] === "--format" && args[i + 1]) {
|
|
5209
|
+
format = args[++i];
|
|
5210
|
+
} else if (args[i] === "--output" && args[i + 1]) {
|
|
5211
|
+
outputPath = args[++i];
|
|
5212
|
+
} else if (args[i] === "--dir" && args[i + 1]) {
|
|
5213
|
+
dir = args[++i];
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
5217
|
+
const { join: join4 } = await import("path");
|
|
5218
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
5219
|
+
const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
5220
|
+
const report = generateReport2(logPath, receiptPath, period);
|
|
5221
|
+
let output;
|
|
5222
|
+
if (format === "md") {
|
|
5223
|
+
output = formatReportMarkdown2(report);
|
|
5224
|
+
} else {
|
|
5225
|
+
output = JSON.stringify(report, null, 2);
|
|
5226
|
+
}
|
|
5227
|
+
if (outputPath) {
|
|
5228
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
5229
|
+
writeFileSync2(outputPath, output, "utf-8");
|
|
5230
|
+
process.stderr.write(`Report written to ${outputPath}
|
|
5231
|
+
`);
|
|
5232
|
+
} else {
|
|
5233
|
+
process.stdout.write(output + "\n");
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
4463
5236
|
main().catch((err) => {
|
|
4464
5237
|
process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
|
|
4465
5238
|
`);
|