protect-mcp 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-U7TMVD3E.mjs → chunk-GV7N53QE.mjs} +164 -3
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/cli.js +766 -31
- package/dist/cli.mjs +414 -6
- package/dist/index.d.mts +148 -1
- package/dist/index.d.ts +148 -1
- package/dist/index.js +331 -12
- package/dist/index.mjs +13 -1
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +2 -2
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;
|
|
@@ -3596,8 +3772,31 @@ var ProtectGateway = class {
|
|
|
3596
3772
|
async interceptToolCall(request) {
|
|
3597
3773
|
const toolName = request.params?.name || "unknown";
|
|
3598
3774
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
3599
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3600
3775
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
3776
|
+
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
3777
|
+
let effectiveToolPolicy;
|
|
3778
|
+
if (this.config.multiAgent?.enabled) {
|
|
3779
|
+
const paramKid = request.params?._passport_kid;
|
|
3780
|
+
if (paramKid) resolvedAgentKid = paramKid;
|
|
3781
|
+
const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
|
|
3782
|
+
if (agentOverrides && agentOverrides[toolName]) {
|
|
3783
|
+
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
3784
|
+
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
3785
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
3786
|
+
if (this.config.enforce) {
|
|
3787
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
3788
|
+
}
|
|
3789
|
+
return null;
|
|
3790
|
+
} else {
|
|
3791
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3792
|
+
}
|
|
3793
|
+
if (this.config.verbose && resolvedAgentKid) {
|
|
3794
|
+
this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
|
|
3795
|
+
}
|
|
3796
|
+
} else {
|
|
3797
|
+
effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3798
|
+
}
|
|
3799
|
+
const toolPolicy = effectiveToolPolicy;
|
|
3601
3800
|
let credentialRef;
|
|
3602
3801
|
if (this.config.credentials) {
|
|
3603
3802
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -3772,6 +3971,137 @@ var ProtectGateway = class {
|
|
|
3772
3971
|
}
|
|
3773
3972
|
};
|
|
3774
3973
|
|
|
3974
|
+
// src/simulate.ts
|
|
3975
|
+
var import_node_fs6 = require("fs");
|
|
3976
|
+
function parseLogFile(path) {
|
|
3977
|
+
const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
3978
|
+
const entries = [];
|
|
3979
|
+
for (const line of raw.split("\n")) {
|
|
3980
|
+
const trimmed = line.trim();
|
|
3981
|
+
if (!trimmed) continue;
|
|
3982
|
+
const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
|
|
3983
|
+
try {
|
|
3984
|
+
const parsed = JSON.parse(jsonStr);
|
|
3985
|
+
if (parsed.tool && parsed.decision) {
|
|
3986
|
+
entries.push(parsed);
|
|
3987
|
+
}
|
|
3988
|
+
} catch {
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
return entries;
|
|
3992
|
+
}
|
|
3993
|
+
function simulate(entries, policy, tier = "unknown") {
|
|
3994
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
3995
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
3996
|
+
const totals = {
|
|
3997
|
+
allow: 0,
|
|
3998
|
+
block: 0,
|
|
3999
|
+
rate_limited: 0,
|
|
4000
|
+
require_approval: 0,
|
|
4001
|
+
tier_insufficient: 0
|
|
4002
|
+
};
|
|
4003
|
+
const originalTotals = { allow: 0, deny: 0 };
|
|
4004
|
+
const changes = [];
|
|
4005
|
+
for (const entry of entries) {
|
|
4006
|
+
const toolName = entry.tool;
|
|
4007
|
+
const toolPolicy = getToolPolicy(toolName, policy);
|
|
4008
|
+
if (entry.decision === "allow") {
|
|
4009
|
+
originalTotals.allow++;
|
|
4010
|
+
} else {
|
|
4011
|
+
originalTotals.deny++;
|
|
4012
|
+
}
|
|
4013
|
+
let newDecision;
|
|
4014
|
+
if (toolPolicy.block) {
|
|
4015
|
+
newDecision = "block";
|
|
4016
|
+
} else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
|
|
4017
|
+
newDecision = "tier_insufficient";
|
|
4018
|
+
} else if (toolPolicy.require_approval) {
|
|
4019
|
+
newDecision = "require_approval";
|
|
4020
|
+
} else if (toolPolicy.rate_limit) {
|
|
4021
|
+
const limit = parseRateLimit(toolPolicy.rate_limit);
|
|
4022
|
+
const result = checkRateLimit(toolName, limit, rateLimitStore);
|
|
4023
|
+
newDecision = result.allowed ? "allow" : "rate_limited";
|
|
4024
|
+
} else {
|
|
4025
|
+
newDecision = "allow";
|
|
4026
|
+
}
|
|
4027
|
+
totals[newDecision]++;
|
|
4028
|
+
if (!toolResults.has(toolName)) {
|
|
4029
|
+
toolResults.set(toolName, {
|
|
4030
|
+
tool: toolName,
|
|
4031
|
+
calls: 0,
|
|
4032
|
+
results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
|
|
4033
|
+
original: { allow: 0, deny: 0 }
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
const tr = toolResults.get(toolName);
|
|
4037
|
+
tr.calls++;
|
|
4038
|
+
tr.results[newDecision]++;
|
|
4039
|
+
if (entry.decision === "allow") {
|
|
4040
|
+
tr.original.allow++;
|
|
4041
|
+
} else {
|
|
4042
|
+
tr.original.deny++;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
for (const [tool, result] of toolResults) {
|
|
4046
|
+
const wasAllBlocked = result.original.allow === 0;
|
|
4047
|
+
const nowAllBlocked = result.results.allow === 0;
|
|
4048
|
+
const wasAllAllowed = result.original.deny === 0;
|
|
4049
|
+
if (wasAllAllowed && result.results.block > 0) {
|
|
4050
|
+
changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
|
|
4051
|
+
}
|
|
4052
|
+
if (wasAllAllowed && result.results.rate_limited > 0) {
|
|
4053
|
+
changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
|
|
4054
|
+
}
|
|
4055
|
+
if (wasAllAllowed && result.results.require_approval > 0) {
|
|
4056
|
+
changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
|
|
4057
|
+
}
|
|
4058
|
+
if (wasAllAllowed && result.results.tier_insufficient > 0) {
|
|
4059
|
+
changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
|
|
4060
|
+
}
|
|
4061
|
+
if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
|
|
4062
|
+
changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
return {
|
|
4066
|
+
policy_file: "",
|
|
4067
|
+
log_file: "",
|
|
4068
|
+
total_calls: entries.length,
|
|
4069
|
+
results: totals,
|
|
4070
|
+
original: originalTotals,
|
|
4071
|
+
tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
|
|
4072
|
+
changes
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
function formatSimulation(summary) {
|
|
4076
|
+
const lines = [];
|
|
4077
|
+
lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
|
|
4078
|
+
`);
|
|
4079
|
+
const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
|
|
4080
|
+
for (const tr of summary.tool_breakdown) {
|
|
4081
|
+
const parts = [];
|
|
4082
|
+
if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
|
|
4083
|
+
if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
|
|
4084
|
+
if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
|
|
4085
|
+
if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
|
|
4086
|
+
if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
|
|
4087
|
+
const originalParts = [];
|
|
4088
|
+
if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
|
|
4089
|
+
if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
|
|
4090
|
+
lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
|
|
4091
|
+
}
|
|
4092
|
+
lines.push("");
|
|
4093
|
+
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`);
|
|
4094
|
+
lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
|
|
4095
|
+
if (summary.changes.length > 0) {
|
|
4096
|
+
lines.push("");
|
|
4097
|
+
lines.push("Changes:");
|
|
4098
|
+
for (const change of summary.changes) {
|
|
4099
|
+
lines.push(` \u2022 ${change}`);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
return lines.join("\n");
|
|
4103
|
+
}
|
|
4104
|
+
|
|
3775
4105
|
// src/cli.ts
|
|
3776
4106
|
function printHelp() {
|
|
3777
4107
|
process.stderr.write(`
|
|
@@ -3779,12 +4109,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
|
3779
4109
|
|
|
3780
4110
|
Usage:
|
|
3781
4111
|
protect-mcp [options] -- <command> [args...]
|
|
4112
|
+
protect-mcp quickstart
|
|
3782
4113
|
protect-mcp init [--dir <path>]
|
|
3783
4114
|
protect-mcp demo
|
|
4115
|
+
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
3784
4116
|
protect-mcp status [--dir <path>]
|
|
3785
4117
|
protect-mcp digest [--today] [--dir <path>]
|
|
3786
4118
|
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
3787
4119
|
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
4120
|
+
protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
|
|
4121
|
+
protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
|
|
3788
4122
|
|
|
3789
4123
|
Options:
|
|
3790
4124
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -3794,19 +4128,22 @@ Options:
|
|
|
3794
4128
|
--help Show this help
|
|
3795
4129
|
|
|
3796
4130
|
Commands:
|
|
4131
|
+
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
3797
4132
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
3798
4133
|
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
4134
|
+
trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
|
|
3799
4135
|
status Show tool call statistics from the local decision log
|
|
3800
4136
|
digest Generate a human-readable summary of agent activity
|
|
3801
4137
|
receipts Show recent persisted signed receipts
|
|
3802
4138
|
bundle Export an offline-verifiable audit bundle
|
|
3803
4139
|
|
|
3804
4140
|
Examples:
|
|
4141
|
+
protect-mcp quickstart
|
|
3805
4142
|
protect-mcp -- node my-server.js
|
|
3806
4143
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
3807
|
-
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
3808
4144
|
protect-mcp init
|
|
3809
4145
|
protect-mcp demo
|
|
4146
|
+
protect-mcp trace sha256:abc123 --depth 5
|
|
3810
4147
|
protect-mcp status
|
|
3811
4148
|
protect-mcp digest --today
|
|
3812
4149
|
protect-mcp receipts --last 10
|
|
@@ -3854,7 +4191,7 @@ function parseArgs(argv) {
|
|
|
3854
4191
|
return { policyPath, slug, enforce, verbose, childCommand };
|
|
3855
4192
|
}
|
|
3856
4193
|
async function handleInit(argv) {
|
|
3857
|
-
const { writeFileSync: writeFileSync2, existsSync:
|
|
4194
|
+
const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
|
|
3858
4195
|
const { join: join4 } = await import("path");
|
|
3859
4196
|
let dir = process.cwd();
|
|
3860
4197
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -3864,17 +4201,14 @@ async function handleInit(argv) {
|
|
|
3864
4201
|
const configPath = join4(dir, "protect-mcp.json");
|
|
3865
4202
|
const keysDir = join4(dir, "keys");
|
|
3866
4203
|
const keyPath = join4(keysDir, "gateway.json");
|
|
3867
|
-
if (
|
|
4204
|
+
if (existsSync5(configPath)) {
|
|
3868
4205
|
process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
|
|
3869
4206
|
`);
|
|
3870
4207
|
process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
|
|
3871
4208
|
process.exit(1);
|
|
3872
4209
|
}
|
|
3873
4210
|
let keypair;
|
|
3874
|
-
|
|
3875
|
-
const artifacts = await import("@veritasacta/artifacts");
|
|
3876
|
-
keypair = artifacts.generateKeypair();
|
|
3877
|
-
} catch {
|
|
4211
|
+
{
|
|
3878
4212
|
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
3879
4213
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
3880
4214
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
@@ -3886,7 +4220,7 @@ async function handleInit(argv) {
|
|
|
3886
4220
|
kid: "generated"
|
|
3887
4221
|
};
|
|
3888
4222
|
}
|
|
3889
|
-
if (!
|
|
4223
|
+
if (!existsSync5(keysDir)) {
|
|
3890
4224
|
mkdirSync(keysDir, { recursive: true });
|
|
3891
4225
|
}
|
|
3892
4226
|
writeFileSync2(keyPath, JSON.stringify({
|
|
@@ -3897,7 +4231,7 @@ async function handleInit(argv) {
|
|
|
3897
4231
|
warning: "KEEP THIS FILE SECRET. Never commit to version control."
|
|
3898
4232
|
}, null, 2) + "\n");
|
|
3899
4233
|
const gitignorePath = join4(keysDir, ".gitignore");
|
|
3900
|
-
if (!
|
|
4234
|
+
if (!existsSync5(gitignorePath)) {
|
|
3901
4235
|
writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
|
|
3902
4236
|
}
|
|
3903
4237
|
const config = {
|
|
@@ -3970,13 +4304,13 @@ Add --enforce when ready to block policy violations.
|
|
|
3970
4304
|
`);
|
|
3971
4305
|
}
|
|
3972
4306
|
async function handleDemo() {
|
|
3973
|
-
const { existsSync:
|
|
4307
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3974
4308
|
const { join: join4, dirname, resolve } = await import("path");
|
|
3975
4309
|
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
3976
4310
|
const cliDir = dirname(cliPath);
|
|
3977
4311
|
const demoServerPath = join4(cliDir, "demo-server.js");
|
|
3978
4312
|
const configPath = join4(process.cwd(), "protect-mcp.json");
|
|
3979
|
-
const hasConfig =
|
|
4313
|
+
const hasConfig = existsSync5(configPath);
|
|
3980
4314
|
if (!hasConfig) {
|
|
3981
4315
|
process.stderr.write(`
|
|
3982
4316
|
${bold("protect-mcp demo")}
|
|
@@ -4050,7 +4384,7 @@ Starting demo server with 5 tools...
|
|
|
4050
4384
|
await gateway.start();
|
|
4051
4385
|
}
|
|
4052
4386
|
async function handleStatus2(argv) {
|
|
4053
|
-
const { readFileSync:
|
|
4387
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4054
4388
|
const { join: join4 } = await import("path");
|
|
4055
4389
|
let dir = process.cwd();
|
|
4056
4390
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -4058,7 +4392,7 @@ async function handleStatus2(argv) {
|
|
|
4058
4392
|
dir = argv[dirIdx + 1];
|
|
4059
4393
|
}
|
|
4060
4394
|
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4061
|
-
if (!
|
|
4395
|
+
if (!existsSync5(logPath)) {
|
|
4062
4396
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
4063
4397
|
|
|
4064
4398
|
`);
|
|
@@ -4068,7 +4402,7 @@ async function handleStatus2(argv) {
|
|
|
4068
4402
|
`);
|
|
4069
4403
|
process.exit(0);
|
|
4070
4404
|
}
|
|
4071
|
-
const raw =
|
|
4405
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
4072
4406
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4073
4407
|
if (lines.length === 0) {
|
|
4074
4408
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
@@ -4148,9 +4482,9 @@ ${bold("protect-mcp status")}
|
|
|
4148
4482
|
`);
|
|
4149
4483
|
}
|
|
4150
4484
|
const evidencePath = join4(dir, ".protect-mcp-evidence.json");
|
|
4151
|
-
if (
|
|
4485
|
+
if (existsSync5(evidencePath)) {
|
|
4152
4486
|
try {
|
|
4153
|
-
const evidenceRaw =
|
|
4487
|
+
const evidenceRaw = readFileSync7(evidencePath, "utf-8");
|
|
4154
4488
|
const evidence = JSON.parse(evidenceRaw);
|
|
4155
4489
|
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
4156
4490
|
process.stdout.write(`
|
|
@@ -4160,9 +4494,9 @@ ${bold("protect-mcp status")}
|
|
|
4160
4494
|
}
|
|
4161
4495
|
}
|
|
4162
4496
|
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4163
|
-
if (
|
|
4497
|
+
if (existsSync5(keyPath)) {
|
|
4164
4498
|
try {
|
|
4165
|
-
const keyData = JSON.parse(
|
|
4499
|
+
const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
|
|
4166
4500
|
if (keyData.publicKey) {
|
|
4167
4501
|
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4168
4502
|
process.stdout.write(`
|
|
@@ -4201,21 +4535,21 @@ function yellow(s) {
|
|
|
4201
4535
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
4202
4536
|
}
|
|
4203
4537
|
async function handleDigest(argv) {
|
|
4204
|
-
const { readFileSync:
|
|
4538
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4205
4539
|
const { join: join4 } = await import("path");
|
|
4206
4540
|
let dir = process.cwd();
|
|
4207
4541
|
const dirIdx = argv.indexOf("--dir");
|
|
4208
4542
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4209
4543
|
const today = argv.includes("--today");
|
|
4210
4544
|
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4211
|
-
if (!
|
|
4545
|
+
if (!existsSync5(logPath)) {
|
|
4212
4546
|
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4213
4547
|
|
|
4214
4548
|
No log file found. Run protect-mcp first.
|
|
4215
4549
|
`);
|
|
4216
4550
|
process.exit(0);
|
|
4217
4551
|
}
|
|
4218
|
-
const raw =
|
|
4552
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
4219
4553
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4220
4554
|
let entries = [];
|
|
4221
4555
|
for (const line of lines) {
|
|
@@ -4292,7 +4626,7 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
|
4292
4626
|
`);
|
|
4293
4627
|
}
|
|
4294
4628
|
async function handleReceipts2(argv) {
|
|
4295
|
-
const { readFileSync:
|
|
4629
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4296
4630
|
const { join: join4 } = await import("path");
|
|
4297
4631
|
let dir = process.cwd();
|
|
4298
4632
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -4300,14 +4634,14 @@ async function handleReceipts2(argv) {
|
|
|
4300
4634
|
const lastIdx = argv.indexOf("--last");
|
|
4301
4635
|
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
4302
4636
|
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4303
|
-
if (!
|
|
4637
|
+
if (!existsSync5(receiptsPath)) {
|
|
4304
4638
|
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
4305
4639
|
|
|
4306
4640
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4307
4641
|
`);
|
|
4308
4642
|
process.exit(0);
|
|
4309
4643
|
}
|
|
4310
|
-
const raw =
|
|
4644
|
+
const raw = readFileSync7(receiptsPath, "utf-8");
|
|
4311
4645
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4312
4646
|
const recent = lines.slice(-count);
|
|
4313
4647
|
process.stdout.write(`
|
|
@@ -4330,7 +4664,7 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
|
4330
4664
|
`);
|
|
4331
4665
|
}
|
|
4332
4666
|
async function handleBundle(argv) {
|
|
4333
|
-
const { readFileSync:
|
|
4667
|
+
const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
|
|
4334
4668
|
const { join: join4 } = await import("path");
|
|
4335
4669
|
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
4336
4670
|
let dir = process.cwd();
|
|
@@ -4340,22 +4674,22 @@ async function handleBundle(argv) {
|
|
|
4340
4674
|
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
|
|
4341
4675
|
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4342
4676
|
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4343
|
-
if (!
|
|
4677
|
+
if (!existsSync5(receiptsPath)) {
|
|
4344
4678
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4345
4679
|
|
|
4346
4680
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4347
4681
|
`);
|
|
4348
4682
|
process.exit(0);
|
|
4349
4683
|
}
|
|
4350
|
-
if (!
|
|
4684
|
+
if (!existsSync5(keyPath)) {
|
|
4351
4685
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4352
4686
|
|
|
4353
4687
|
No key file found at ${keyPath}
|
|
4354
4688
|
`);
|
|
4355
4689
|
process.exit(1);
|
|
4356
4690
|
}
|
|
4357
|
-
const receipts =
|
|
4358
|
-
const keyData = JSON.parse(
|
|
4691
|
+
const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4692
|
+
const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
|
|
4359
4693
|
const bundle = createAuditBundle2({
|
|
4360
4694
|
tenant: keyData.issuer || "protect-mcp",
|
|
4361
4695
|
receipts,
|
|
@@ -4380,12 +4714,322 @@ ${bold("protect-mcp bundle")}
|
|
|
4380
4714
|
|
|
4381
4715
|
`);
|
|
4382
4716
|
}
|
|
4717
|
+
async function handleQuickstart() {
|
|
4718
|
+
const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
|
|
4719
|
+
const { join: join4 } = await import("path");
|
|
4720
|
+
const { tmpdir } = await import("os");
|
|
4721
|
+
const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
|
|
4722
|
+
process.stdout.write(`
|
|
4723
|
+
${bold("protect-mcp quickstart")}
|
|
4724
|
+
`);
|
|
4725
|
+
process.stdout.write(`${"\u2500".repeat(50)}
|
|
4726
|
+
|
|
4727
|
+
`);
|
|
4728
|
+
process.stdout.write(` This will:
|
|
4729
|
+
`);
|
|
4730
|
+
process.stdout.write(` 1. Generate an Ed25519 signing keypair
|
|
4731
|
+
`);
|
|
4732
|
+
process.stdout.write(` 2. Create a shadow-mode policy
|
|
4733
|
+
`);
|
|
4734
|
+
process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
|
|
4735
|
+
`);
|
|
4736
|
+
process.stdout.write(` 4. Log signed receipts for every tool call
|
|
4737
|
+
|
|
4738
|
+
`);
|
|
4739
|
+
process.stdout.write(` Working dir: ${dir}
|
|
4740
|
+
|
|
4741
|
+
`);
|
|
4742
|
+
const keysDir = join4(dir, "keys");
|
|
4743
|
+
mkdirSync(keysDir, { recursive: true });
|
|
4744
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
4745
|
+
let keypair;
|
|
4746
|
+
try {
|
|
4747
|
+
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
4748
|
+
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
4749
|
+
const privateKey = randomBytes3(32);
|
|
4750
|
+
const publicKey = ed255192.getPublicKey(privateKey);
|
|
4751
|
+
keypair = {
|
|
4752
|
+
privateKey: bytesToHex2(privateKey),
|
|
4753
|
+
publicKey: bytesToHex2(publicKey),
|
|
4754
|
+
kid: `quickstart-${Date.now()}`
|
|
4755
|
+
};
|
|
4756
|
+
} catch {
|
|
4757
|
+
keypair = {
|
|
4758
|
+
privateKey: randomBytes3(32).toString("hex"),
|
|
4759
|
+
publicKey: randomBytes3(32).toString("hex"),
|
|
4760
|
+
kid: `quickstart-${Date.now()}`
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
|
|
4764
|
+
privateKey: keypair.privateKey,
|
|
4765
|
+
publicKey: keypair.publicKey,
|
|
4766
|
+
kid: keypair.kid,
|
|
4767
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4768
|
+
}, null, 2) + "\n");
|
|
4769
|
+
const configPath = join4(dir, "protect-mcp.json");
|
|
4770
|
+
const config = {
|
|
4771
|
+
tools: {
|
|
4772
|
+
"*": { rate_limit: "100/hour" },
|
|
4773
|
+
"delete_file": { block: true }
|
|
4774
|
+
},
|
|
4775
|
+
default_tier: "unknown",
|
|
4776
|
+
signing: {
|
|
4777
|
+
key_path: join4(keysDir, "gateway.json"),
|
|
4778
|
+
issuer: "protect-mcp-quickstart",
|
|
4779
|
+
enabled: true
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4782
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
4783
|
+
process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
|
|
4784
|
+
`);
|
|
4785
|
+
process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
|
|
4786
|
+
`);
|
|
4787
|
+
process.stdout.write(` \u2713 Signing enabled (Ed25519)
|
|
4788
|
+
|
|
4789
|
+
`);
|
|
4790
|
+
process.stdout.write(`${bold("Starting demo server...")}
|
|
4791
|
+
|
|
4792
|
+
`);
|
|
4793
|
+
process.stdout.write(` Every tool call will produce a signed receipt.
|
|
4794
|
+
`);
|
|
4795
|
+
process.stdout.write(` Try it with Claude Desktop or any MCP client.
|
|
4796
|
+
|
|
4797
|
+
`);
|
|
4798
|
+
process.stdout.write(` ${bold("To use in production:")}
|
|
4799
|
+
`);
|
|
4800
|
+
process.stdout.write(` 1. Copy ${configPath} to your project
|
|
4801
|
+
`);
|
|
4802
|
+
process.stdout.write(` 2. Edit tool policies to match your server
|
|
4803
|
+
`);
|
|
4804
|
+
process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
|
|
4805
|
+
|
|
4806
|
+
`);
|
|
4807
|
+
process.stdout.write(`${"\u2500".repeat(50)}
|
|
4808
|
+
|
|
4809
|
+
`);
|
|
4810
|
+
process.env.PROTECT_MCP_CONFIG = configPath;
|
|
4811
|
+
await handleDemo();
|
|
4812
|
+
}
|
|
4813
|
+
async function handleTrace(argv) {
|
|
4814
|
+
const receiptId = argv[0];
|
|
4815
|
+
if (!receiptId) {
|
|
4816
|
+
process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
|
|
4817
|
+
process.exit(1);
|
|
4818
|
+
}
|
|
4819
|
+
let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
|
|
4820
|
+
let depth = 3;
|
|
4821
|
+
for (let i = 1; i < argv.length; i++) {
|
|
4822
|
+
if (argv[i] === "--endpoint" && argv[i + 1]) {
|
|
4823
|
+
endpoint = argv[++i];
|
|
4824
|
+
} else if (argv[i] === "--depth" && argv[i + 1]) {
|
|
4825
|
+
depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
process.stdout.write(`
|
|
4829
|
+
${bold("protect-mcp trace")}
|
|
4830
|
+
`);
|
|
4831
|
+
process.stdout.write(`${"\u2500".repeat(60)}
|
|
4832
|
+
|
|
4833
|
+
`);
|
|
4834
|
+
process.stdout.write(` Root: ${receiptId}
|
|
4835
|
+
`);
|
|
4836
|
+
process.stdout.write(` Endpoint: ${endpoint}
|
|
4837
|
+
`);
|
|
4838
|
+
process.stdout.write(` Depth: ${depth}
|
|
4839
|
+
|
|
4840
|
+
`);
|
|
4841
|
+
const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
|
|
4842
|
+
let graphData;
|
|
4843
|
+
try {
|
|
4844
|
+
const resp = await fetch(url);
|
|
4845
|
+
if (!resp.ok) {
|
|
4846
|
+
const body = await resp.text();
|
|
4847
|
+
process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
|
|
4848
|
+
`);
|
|
4849
|
+
process.exit(1);
|
|
4850
|
+
}
|
|
4851
|
+
graphData = await resp.json();
|
|
4852
|
+
} catch (err) {
|
|
4853
|
+
process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
|
|
4854
|
+
`);
|
|
4855
|
+
process.stderr.write(`[PROTECT_MCP] Trying local receipts...
|
|
4856
|
+
|
|
4857
|
+
`);
|
|
4858
|
+
await traceLocal(receiptId);
|
|
4859
|
+
return;
|
|
4860
|
+
}
|
|
4861
|
+
if (!graphData.nodes || graphData.nodes.length === 0) {
|
|
4862
|
+
process.stdout.write(` No receipts found for ${receiptId}
|
|
4863
|
+
|
|
4864
|
+
`);
|
|
4865
|
+
return;
|
|
4866
|
+
}
|
|
4867
|
+
process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
|
|
4868
|
+
|
|
4869
|
+
`);
|
|
4870
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
4871
|
+
for (const node of graphData.nodes) {
|
|
4872
|
+
nodeMap.set(node.receipt_id, node);
|
|
4873
|
+
}
|
|
4874
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
4875
|
+
for (const edge of graphData.edges) {
|
|
4876
|
+
if (!childMap.has(edge.from)) childMap.set(edge.from, []);
|
|
4877
|
+
childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
|
|
4878
|
+
}
|
|
4879
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
4880
|
+
function renderNode(id, prefix, isLast) {
|
|
4881
|
+
const node = nodeMap.get(id);
|
|
4882
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4883
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
4884
|
+
const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
|
|
4885
|
+
const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
|
|
4886
|
+
const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
|
|
4887
|
+
const type = node?.receipt_type?.replace("acta:", "") || "unknown";
|
|
4888
|
+
process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
|
|
4889
|
+
`);
|
|
4890
|
+
if (rendered.has(id)) {
|
|
4891
|
+
process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
|
|
4892
|
+
`);
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
rendered.add(id);
|
|
4896
|
+
const children = childMap.get(id) || [];
|
|
4897
|
+
for (let i = 0; i < children.length; i++) {
|
|
4898
|
+
const child = children[i];
|
|
4899
|
+
const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
|
|
4900
|
+
process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
|
|
4901
|
+
`);
|
|
4902
|
+
renderNode(child.to, prefix + childPrefix, i === children.length - 1);
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
const rootNode = nodeMap.get(receiptId);
|
|
4906
|
+
if (rootNode) {
|
|
4907
|
+
const typeEmoji = getTypeEmoji(rootNode.receipt_type);
|
|
4908
|
+
const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
|
|
4909
|
+
const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
|
|
4910
|
+
process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
|
|
4911
|
+
`);
|
|
4912
|
+
rendered.add(receiptId);
|
|
4913
|
+
const children = childMap.get(receiptId) || [];
|
|
4914
|
+
for (let i = 0; i < children.length; i++) {
|
|
4915
|
+
const child = children[i];
|
|
4916
|
+
const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
|
|
4917
|
+
process.stdout.write(` ${edgeLabel}
|
|
4918
|
+
`);
|
|
4919
|
+
renderNode(child.to, " ", i === children.length - 1);
|
|
4920
|
+
}
|
|
4921
|
+
const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
|
|
4922
|
+
if (incomingEdges.length > 0) {
|
|
4923
|
+
process.stdout.write(`
|
|
4924
|
+
${bold("Incoming edges:")}
|
|
4925
|
+
`);
|
|
4926
|
+
for (const edge of incomingEdges) {
|
|
4927
|
+
const fromNode = nodeMap.get(edge.from);
|
|
4928
|
+
const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
|
|
4929
|
+
process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
|
|
4930
|
+
`);
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
} else {
|
|
4934
|
+
for (const node of graphData.nodes) {
|
|
4935
|
+
const typeEmoji = getTypeEmoji(node.receipt_type);
|
|
4936
|
+
const type = node.receipt_type?.replace("acta:", "") || "unknown";
|
|
4937
|
+
process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
|
|
4938
|
+
`);
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
process.stdout.write(`
|
|
4942
|
+
${"\u2500".repeat(60)}
|
|
4943
|
+
`);
|
|
4944
|
+
process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
|
|
4945
|
+
|
|
4946
|
+
`);
|
|
4947
|
+
}
|
|
4948
|
+
async function traceLocal(receiptId) {
|
|
4949
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4950
|
+
const { join: join4 } = await import("path");
|
|
4951
|
+
const dir = process.cwd();
|
|
4952
|
+
const receiptsDir = join4(dir, ".protect-mcp", "receipts");
|
|
4953
|
+
if (!existsSync5(receiptsDir)) {
|
|
4954
|
+
process.stdout.write(` No local receipts found in ${receiptsDir}
|
|
4955
|
+
|
|
4956
|
+
`);
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
const { readdirSync } = await import("fs");
|
|
4960
|
+
const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
|
|
4961
|
+
process.stdout.write(` Scanning ${files.length} local receipts...
|
|
4962
|
+
|
|
4963
|
+
`);
|
|
4964
|
+
const receipts = [];
|
|
4965
|
+
for (const file of files) {
|
|
4966
|
+
try {
|
|
4967
|
+
const content = readFileSync7(join4(receiptsDir, file), "utf-8");
|
|
4968
|
+
const receipt = JSON.parse(content);
|
|
4969
|
+
receipts.push(receipt);
|
|
4970
|
+
} catch {
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
const match = receipts.find(
|
|
4974
|
+
(r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
|
|
4975
|
+
);
|
|
4976
|
+
if (match) {
|
|
4977
|
+
const claims = match.signed_claims?.claims || match;
|
|
4978
|
+
process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
|
|
4979
|
+
`);
|
|
4980
|
+
process.stdout.write(` Event: ${claims.event_id || "?"}
|
|
4981
|
+
`);
|
|
4982
|
+
process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
|
|
4983
|
+
`);
|
|
4984
|
+
process.stdout.write(` Time: ${claims.event_time || "?"}
|
|
4985
|
+
`);
|
|
4986
|
+
if (claims.edges && claims.edges.length > 0) {
|
|
4987
|
+
process.stdout.write(`
|
|
4988
|
+
${bold("Edges:")}
|
|
4989
|
+
`);
|
|
4990
|
+
for (const edge of claims.edges) {
|
|
4991
|
+
process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
|
|
4992
|
+
`);
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
} else {
|
|
4996
|
+
process.stdout.write(` Receipt ${receiptId} not found locally.
|
|
4997
|
+
`);
|
|
4998
|
+
}
|
|
4999
|
+
process.stdout.write("\n");
|
|
5000
|
+
}
|
|
5001
|
+
function getTypeEmoji(type) {
|
|
5002
|
+
switch (type) {
|
|
5003
|
+
case "acta:observation":
|
|
5004
|
+
return "\u{1F441} ";
|
|
5005
|
+
case "acta:policy-load":
|
|
5006
|
+
return "\u{1F4CB}";
|
|
5007
|
+
case "acta:approval":
|
|
5008
|
+
return "\u2705";
|
|
5009
|
+
case "acta:decision":
|
|
5010
|
+
return "\u2696\uFE0F ";
|
|
5011
|
+
case "acta:execution":
|
|
5012
|
+
return "\u26A1";
|
|
5013
|
+
case "acta:outcome":
|
|
5014
|
+
return "\u{1F4E6}";
|
|
5015
|
+
case "acta:delegation":
|
|
5016
|
+
return "\u{1F91D}";
|
|
5017
|
+
case "acta:capability-attestation":
|
|
5018
|
+
return "\u{1F3C5}";
|
|
5019
|
+
default:
|
|
5020
|
+
return "\u{1F4C4}";
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
4383
5023
|
async function main() {
|
|
4384
5024
|
const args = process.argv.slice(2);
|
|
4385
5025
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
4386
5026
|
printHelp();
|
|
4387
5027
|
process.exit(0);
|
|
4388
5028
|
}
|
|
5029
|
+
if (args[0] === "quickstart") {
|
|
5030
|
+
await handleQuickstart();
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
4389
5033
|
if (args[0] === "init") {
|
|
4390
5034
|
await handleInit(args.slice(1));
|
|
4391
5035
|
process.exit(0);
|
|
@@ -4410,6 +5054,18 @@ async function main() {
|
|
|
4410
5054
|
await handleBundle(args.slice(1));
|
|
4411
5055
|
process.exit(0);
|
|
4412
5056
|
}
|
|
5057
|
+
if (args[0] === "trace") {
|
|
5058
|
+
await handleTrace(args.slice(1));
|
|
5059
|
+
process.exit(0);
|
|
5060
|
+
}
|
|
5061
|
+
if (args[0] === "simulate") {
|
|
5062
|
+
await handleSimulate(args.slice(1));
|
|
5063
|
+
process.exit(0);
|
|
5064
|
+
}
|
|
5065
|
+
if (args[0] === "report") {
|
|
5066
|
+
await handleReport(args.slice(1));
|
|
5067
|
+
process.exit(0);
|
|
5068
|
+
}
|
|
4413
5069
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
4414
5070
|
let policy = null;
|
|
4415
5071
|
let policyDigest = "none";
|
|
@@ -4460,6 +5116,85 @@ async function main() {
|
|
|
4460
5116
|
const gateway = new ProtectGateway(config);
|
|
4461
5117
|
await gateway.start();
|
|
4462
5118
|
}
|
|
5119
|
+
async function handleSimulate(args) {
|
|
5120
|
+
let policyPath = "";
|
|
5121
|
+
let logPath = ".protect-mcp-log.jsonl";
|
|
5122
|
+
let tier = "unknown";
|
|
5123
|
+
let jsonOutput = false;
|
|
5124
|
+
for (let i = 0; i < args.length; i++) {
|
|
5125
|
+
if (args[i] === "--policy" && args[i + 1]) {
|
|
5126
|
+
policyPath = args[++i];
|
|
5127
|
+
} else if (args[i] === "--log" && args[i + 1]) {
|
|
5128
|
+
logPath = args[++i];
|
|
5129
|
+
} else if (args[i] === "--tier" && args[i + 1]) {
|
|
5130
|
+
tier = args[++i];
|
|
5131
|
+
} else if (args[i] === "--json") {
|
|
5132
|
+
jsonOutput = true;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
if (!policyPath) {
|
|
5136
|
+
process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
|
|
5137
|
+
process.exit(1);
|
|
5138
|
+
}
|
|
5139
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
5140
|
+
if (!existsSync5(logPath)) {
|
|
5141
|
+
process.stderr.write(`Log file not found: ${logPath}
|
|
5142
|
+
`);
|
|
5143
|
+
process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
|
|
5144
|
+
process.exit(1);
|
|
5145
|
+
}
|
|
5146
|
+
const { policy } = loadPolicy(policyPath);
|
|
5147
|
+
const entries = parseLogFile(logPath);
|
|
5148
|
+
if (entries.length === 0) {
|
|
5149
|
+
process.stderr.write("No tool call entries found in log file.\n");
|
|
5150
|
+
process.exit(1);
|
|
5151
|
+
}
|
|
5152
|
+
const summary = simulate(entries, policy, tier);
|
|
5153
|
+
summary.policy_file = policyPath;
|
|
5154
|
+
summary.log_file = logPath;
|
|
5155
|
+
if (jsonOutput) {
|
|
5156
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
5157
|
+
} else {
|
|
5158
|
+
process.stdout.write(formatSimulation(summary) + "\n");
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
async function handleReport(args) {
|
|
5162
|
+
let period = 30;
|
|
5163
|
+
let format = "json";
|
|
5164
|
+
let outputPath = "";
|
|
5165
|
+
let dir = process.cwd();
|
|
5166
|
+
for (let i = 0; i < args.length; i++) {
|
|
5167
|
+
if (args[i] === "--period" && args[i + 1]) {
|
|
5168
|
+
const match = args[++i].match(/^(\d+)d$/);
|
|
5169
|
+
if (match) period = parseInt(match[1], 10);
|
|
5170
|
+
} else if (args[i] === "--format" && args[i + 1]) {
|
|
5171
|
+
format = args[++i];
|
|
5172
|
+
} else if (args[i] === "--output" && args[i + 1]) {
|
|
5173
|
+
outputPath = args[++i];
|
|
5174
|
+
} else if (args[i] === "--dir" && args[i + 1]) {
|
|
5175
|
+
dir = args[++i];
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
5179
|
+
const { join: join4 } = await import("path");
|
|
5180
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
5181
|
+
const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
5182
|
+
const report = generateReport2(logPath, receiptPath, period);
|
|
5183
|
+
let output;
|
|
5184
|
+
if (format === "md") {
|
|
5185
|
+
output = formatReportMarkdown2(report);
|
|
5186
|
+
} else {
|
|
5187
|
+
output = JSON.stringify(report, null, 2);
|
|
5188
|
+
}
|
|
5189
|
+
if (outputPath) {
|
|
5190
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
5191
|
+
writeFileSync2(outputPath, output, "utf-8");
|
|
5192
|
+
process.stderr.write(`Report written to ${outputPath}
|
|
5193
|
+
`);
|
|
5194
|
+
} else {
|
|
5195
|
+
process.stdout.write(output + "\n");
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
4463
5198
|
main().catch((err) => {
|
|
4464
5199
|
process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
|
|
4465
5200
|
`);
|