protect-mcp 0.3.0 → 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/README.md +11 -1
- package/dist/bundle-TXOTFJIJ.mjs +8 -0
- package/dist/chunk-5JXFV37Y.mjs +53 -0
- package/dist/{chunk-3WCA7O4D.mjs → chunk-GV7N53QE.mjs} +300 -11
- package/dist/chunk-JQDVKZBN.mjs +165 -0
- package/dist/cli.js +1180 -38
- package/dist/cli.mjs +640 -11
- package/dist/index.d.mts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.js +466 -19
- package/dist/index.mjs +17 -50
- package/dist/report-ENQ3KUI2.mjs +8 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ __export(utils_exports, {
|
|
|
67
67
|
isLE: () => isLE,
|
|
68
68
|
kdfInputToBytes: () => kdfInputToBytes,
|
|
69
69
|
nextTick: () => nextTick,
|
|
70
|
-
randomBytes: () =>
|
|
70
|
+
randomBytes: () => randomBytes2,
|
|
71
71
|
rotl: () => rotl,
|
|
72
72
|
rotr: () => rotr,
|
|
73
73
|
swap32IfBE: () => swap32IfBE,
|
|
@@ -257,7 +257,7 @@ function createXOFer(hashCons) {
|
|
|
257
257
|
hashC.create = (opts) => hashCons(opts);
|
|
258
258
|
return hashC;
|
|
259
259
|
}
|
|
260
|
-
function
|
|
260
|
+
function randomBytes2(bytesLength = 32) {
|
|
261
261
|
if (crypto && typeof crypto.getRandomValues === "function") {
|
|
262
262
|
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
|
263
263
|
}
|
|
@@ -1700,7 +1700,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
1700
1700
|
});
|
|
1701
1701
|
const { prehash } = eddsaOpts;
|
|
1702
1702
|
const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
|
|
1703
|
-
const
|
|
1703
|
+
const randomBytes3 = eddsaOpts.randomBytes || randomBytes2;
|
|
1704
1704
|
const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
|
|
1705
1705
|
const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
|
|
1706
1706
|
_abool2(phflag, "phflag");
|
|
@@ -1782,7 +1782,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
1782
1782
|
signature: 2 * _size,
|
|
1783
1783
|
seed: _size
|
|
1784
1784
|
};
|
|
1785
|
-
function randomSecretKey(seed =
|
|
1785
|
+
function randomSecretKey(seed = randomBytes3(lengths.seed)) {
|
|
1786
1786
|
return _abytes2(seed, lengths.seed, "seed");
|
|
1787
1787
|
}
|
|
1788
1788
|
function keygen(seed) {
|
|
@@ -2137,7 +2137,7 @@ function montgomery(curveDef) {
|
|
|
2137
2137
|
const is25519 = type === "x25519";
|
|
2138
2138
|
if (!is25519 && type !== "x448")
|
|
2139
2139
|
throw new Error("invalid type");
|
|
2140
|
-
const randomBytes_ = rand ||
|
|
2140
|
+
const randomBytes_ = rand || randomBytes2;
|
|
2141
2141
|
const montgomeryBits = is25519 ? 255 : 448;
|
|
2142
2142
|
const fieldLen = is25519 ? 32 : 56;
|
|
2143
2143
|
const Gu = is25519 ? BigInt(9) : BigInt(5);
|
|
@@ -2638,6 +2638,237 @@ var init_ed25519 = __esm({
|
|
|
2638
2638
|
}
|
|
2639
2639
|
});
|
|
2640
2640
|
|
|
2641
|
+
// src/bundle.ts
|
|
2642
|
+
var bundle_exports = {};
|
|
2643
|
+
__export(bundle_exports, {
|
|
2644
|
+
collectSignedReceipts: () => collectSignedReceipts,
|
|
2645
|
+
createAuditBundle: () => createAuditBundle
|
|
2646
|
+
});
|
|
2647
|
+
function createAuditBundle(opts) {
|
|
2648
|
+
const receipts = opts.receipts.filter(
|
|
2649
|
+
(r) => r && typeof r === "object" && typeof r.signature === "string"
|
|
2650
|
+
);
|
|
2651
|
+
if (receipts.length === 0) {
|
|
2652
|
+
throw new Error("Audit bundle requires at least one signed receipt");
|
|
2653
|
+
}
|
|
2654
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const key of opts.signingKeys) {
|
|
2656
|
+
if (!keyMap.has(key.kid)) {
|
|
2657
|
+
keyMap.set(key.kid, key);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
let timeRange = opts.timeRange || null;
|
|
2661
|
+
if (!timeRange) {
|
|
2662
|
+
const timestamps = receipts.map((r) => r.issued_at || r.timestamp).filter(Boolean).sort();
|
|
2663
|
+
if (timestamps.length > 0) {
|
|
2664
|
+
timeRange = {
|
|
2665
|
+
from: timestamps[0],
|
|
2666
|
+
to: timestamps[timestamps.length - 1]
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
format: "scopeblind:audit-bundle",
|
|
2672
|
+
version: 1,
|
|
2673
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2674
|
+
tenant: opts.tenant,
|
|
2675
|
+
time_range: timeRange,
|
|
2676
|
+
receipts,
|
|
2677
|
+
anchors: opts.anchors || [],
|
|
2678
|
+
verification: {
|
|
2679
|
+
algorithm: "ed25519",
|
|
2680
|
+
signing_keys: Array.from(keyMap.values()),
|
|
2681
|
+
instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. CLI: npx @veritasacta/verify bundle.json --bundle`
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
function collectSignedReceipts(logs) {
|
|
2686
|
+
return logs.filter((log) => log.v === 2).map((log) => {
|
|
2687
|
+
const logRecord = log;
|
|
2688
|
+
if (logRecord.receipt) {
|
|
2689
|
+
return logRecord.receipt;
|
|
2690
|
+
}
|
|
2691
|
+
return logRecord;
|
|
2692
|
+
}).filter((r) => typeof r.signature === "string");
|
|
2693
|
+
}
|
|
2694
|
+
var init_bundle = __esm({
|
|
2695
|
+
"src/bundle.ts"() {
|
|
2696
|
+
"use strict";
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
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
|
+
|
|
2641
2872
|
// src/gateway.ts
|
|
2642
2873
|
var import_node_child_process = require("child_process");
|
|
2643
2874
|
var import_node_crypto2 = require("crypto");
|
|
@@ -2960,7 +3191,11 @@ async function initSigning(config) {
|
|
|
2960
3191
|
return warnings;
|
|
2961
3192
|
}
|
|
2962
3193
|
try {
|
|
2963
|
-
|
|
3194
|
+
const moduleName = "@veritasacta/artifacts";
|
|
3195
|
+
artifactsModule = await import(
|
|
3196
|
+
/* @vite-ignore */
|
|
3197
|
+
moduleName
|
|
3198
|
+
);
|
|
2964
3199
|
} catch {
|
|
2965
3200
|
warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
|
|
2966
3201
|
return warnings;
|
|
@@ -2979,8 +3214,8 @@ async function initSigning(config) {
|
|
|
2979
3214
|
signerState = {
|
|
2980
3215
|
privateKey: keyData.privateKey,
|
|
2981
3216
|
publicKey: keyData.publicKey,
|
|
2982
|
-
kid: artifactsModule.computeKid(keyData.publicKey),
|
|
2983
|
-
issuer: config.issuer || "protect-mcp"
|
|
3217
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
3218
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
2984
3219
|
};
|
|
2985
3220
|
} catch (err) {
|
|
2986
3221
|
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3184,13 +3419,16 @@ var ReceiptBuffer = class {
|
|
|
3184
3419
|
count() {
|
|
3185
3420
|
return this.receipts.length;
|
|
3186
3421
|
}
|
|
3422
|
+
getLatest() {
|
|
3423
|
+
return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
|
|
3424
|
+
}
|
|
3187
3425
|
};
|
|
3188
|
-
function startStatusServer(config, receiptBuffer) {
|
|
3426
|
+
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
3189
3427
|
const startTime = Date.now();
|
|
3190
3428
|
const logDir = process.cwd();
|
|
3191
3429
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
3192
3430
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3193
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
3431
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
3194
3432
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
3195
3433
|
res.setHeader("Content-Type", "application/json");
|
|
3196
3434
|
if (req.method === "OPTIONS") {
|
|
@@ -3207,18 +3445,30 @@ function startStatusServer(config, receiptBuffer) {
|
|
|
3207
3445
|
handleStatus(res, logDir);
|
|
3208
3446
|
} else if (path === "/receipts") {
|
|
3209
3447
|
handleReceipts(res, receiptBuffer, url);
|
|
3448
|
+
} else if (path === "/receipts/latest") {
|
|
3449
|
+
handleReceiptLatest(res, receiptBuffer);
|
|
3210
3450
|
} else if (path.startsWith("/receipts/")) {
|
|
3211
3451
|
const id = path.slice("/receipts/".length);
|
|
3212
3452
|
handleReceiptById(res, receiptBuffer, id);
|
|
3453
|
+
} else if (path === "/approve" && req.method === "POST") {
|
|
3454
|
+
handleApprove(req, res, approvalStore, approvalNonce);
|
|
3455
|
+
} else if (path === "/approvals" && req.method === "GET") {
|
|
3456
|
+
handleListApprovals(res, approvalStore);
|
|
3213
3457
|
} else {
|
|
3214
3458
|
res.writeHead(404);
|
|
3215
|
-
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
|
|
3459
|
+
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
|
|
3216
3460
|
}
|
|
3217
3461
|
} catch (err) {
|
|
3218
3462
|
res.writeHead(500);
|
|
3219
3463
|
res.end(JSON.stringify({ error: "internal_error" }));
|
|
3220
3464
|
}
|
|
3221
3465
|
});
|
|
3466
|
+
server.on("error", (err) => {
|
|
3467
|
+
if (config.verbose) {
|
|
3468
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
|
|
3469
|
+
`);
|
|
3470
|
+
}
|
|
3471
|
+
});
|
|
3222
3472
|
server.listen(config.port, "127.0.0.1", () => {
|
|
3223
3473
|
if (config.verbose) {
|
|
3224
3474
|
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
@@ -3234,7 +3484,7 @@ function handleHealth(res, startTime, config) {
|
|
|
3234
3484
|
status: "ok",
|
|
3235
3485
|
uptime_ms: Date.now() - startTime,
|
|
3236
3486
|
mode: config.mode,
|
|
3237
|
-
version: "0.3.
|
|
3487
|
+
version: "0.3.1"
|
|
3238
3488
|
}));
|
|
3239
3489
|
}
|
|
3240
3490
|
function handleStatus(res, logDir) {
|
|
@@ -3283,6 +3533,16 @@ function handleReceipts(res, buffer, url) {
|
|
|
3283
3533
|
receipts
|
|
3284
3534
|
}));
|
|
3285
3535
|
}
|
|
3536
|
+
function handleReceiptLatest(res, buffer) {
|
|
3537
|
+
const latest = buffer.getLatest();
|
|
3538
|
+
if (!latest) {
|
|
3539
|
+
res.writeHead(404);
|
|
3540
|
+
res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
res.writeHead(200);
|
|
3544
|
+
res.end(JSON.stringify(latest));
|
|
3545
|
+
}
|
|
3286
3546
|
function handleReceiptById(res, buffer, id) {
|
|
3287
3547
|
const receipt = buffer.getById(id);
|
|
3288
3548
|
if (!receipt) {
|
|
@@ -3293,22 +3553,92 @@ function handleReceiptById(res, buffer, id) {
|
|
|
3293
3553
|
res.writeHead(200);
|
|
3294
3554
|
res.end(JSON.stringify(receipt));
|
|
3295
3555
|
}
|
|
3556
|
+
function handleApprove(req, res, approvalStore, expectedNonce) {
|
|
3557
|
+
if (!approvalStore) {
|
|
3558
|
+
res.writeHead(503);
|
|
3559
|
+
res.end(JSON.stringify({ error: "approval_store_not_available" }));
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
let body = "";
|
|
3563
|
+
req.on("data", (chunk) => {
|
|
3564
|
+
body += chunk.toString();
|
|
3565
|
+
});
|
|
3566
|
+
req.on("end", () => {
|
|
3567
|
+
try {
|
|
3568
|
+
const { request_id, tool, mode, nonce } = JSON.parse(body);
|
|
3569
|
+
if (expectedNonce && nonce !== expectedNonce) {
|
|
3570
|
+
res.writeHead(403);
|
|
3571
|
+
res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (!tool || typeof tool !== "string") {
|
|
3575
|
+
res.writeHead(400);
|
|
3576
|
+
res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const grantMode = mode === "always" ? "always" : "once";
|
|
3580
|
+
const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
|
|
3581
|
+
const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
|
|
3582
|
+
if (grantMode === "always") {
|
|
3583
|
+
approvalStore.set(`always:${tool}`, grantEntry);
|
|
3584
|
+
} else if (request_id) {
|
|
3585
|
+
approvalStore.set(request_id, grantEntry);
|
|
3586
|
+
} else {
|
|
3587
|
+
approvalStore.set(tool, grantEntry);
|
|
3588
|
+
}
|
|
3589
|
+
res.writeHead(200);
|
|
3590
|
+
res.end(JSON.stringify({
|
|
3591
|
+
approved: true,
|
|
3592
|
+
request_id: request_id || null,
|
|
3593
|
+
tool,
|
|
3594
|
+
mode: grantMode,
|
|
3595
|
+
expires_in_seconds: ttlMs / 1e3
|
|
3596
|
+
}));
|
|
3597
|
+
} catch {
|
|
3598
|
+
res.writeHead(400);
|
|
3599
|
+
res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
function handleListApprovals(res, approvalStore) {
|
|
3604
|
+
if (!approvalStore) {
|
|
3605
|
+
res.writeHead(200);
|
|
3606
|
+
res.end(JSON.stringify({ grants: [] }));
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
const now = Date.now();
|
|
3610
|
+
const grants = [];
|
|
3611
|
+
for (const [key, grant] of approvalStore) {
|
|
3612
|
+
if (now < grant.expires_at) {
|
|
3613
|
+
grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
res.writeHead(200);
|
|
3617
|
+
res.end(JSON.stringify({ grants }));
|
|
3618
|
+
}
|
|
3296
3619
|
|
|
3297
3620
|
// src/gateway.ts
|
|
3298
3621
|
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
3622
|
+
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
3299
3623
|
var ProtectGateway = class {
|
|
3300
3624
|
child = null;
|
|
3301
3625
|
config;
|
|
3302
3626
|
rateLimitStore = /* @__PURE__ */ new Map();
|
|
3303
3627
|
clientReader = null;
|
|
3304
3628
|
logFilePath;
|
|
3629
|
+
receiptFilePath;
|
|
3305
3630
|
evidenceStore;
|
|
3306
3631
|
receiptBuffer;
|
|
3632
|
+
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
3633
|
+
approvalStore = /* @__PURE__ */ new Map();
|
|
3634
|
+
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
3635
|
+
approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
3307
3636
|
currentTier = "unknown";
|
|
3308
3637
|
admissionResult = null;
|
|
3309
3638
|
constructor(config) {
|
|
3310
3639
|
this.config = config;
|
|
3311
3640
|
this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
|
|
3641
|
+
this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
|
|
3312
3642
|
this.evidenceStore = new EvidenceStore();
|
|
3313
3643
|
this.receiptBuffer = new ReceiptBuffer();
|
|
3314
3644
|
}
|
|
@@ -3332,12 +3662,15 @@ var ProtectGateway = class {
|
|
|
3332
3662
|
this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
|
|
3333
3663
|
}
|
|
3334
3664
|
}
|
|
3665
|
+
this.log(`Approval nonce: ${this.approvalNonce}`);
|
|
3335
3666
|
const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
|
|
3336
3667
|
if (httpPort > 0) {
|
|
3337
3668
|
try {
|
|
3338
3669
|
startStatusServer(
|
|
3339
3670
|
{ port: httpPort, mode, verbose },
|
|
3340
|
-
this.receiptBuffer
|
|
3671
|
+
this.receiptBuffer,
|
|
3672
|
+
this.approvalStore,
|
|
3673
|
+
this.approvalNonce
|
|
3341
3674
|
);
|
|
3342
3675
|
} catch {
|
|
3343
3676
|
if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
|
|
@@ -3439,8 +3772,31 @@ var ProtectGateway = class {
|
|
|
3439
3772
|
async interceptToolCall(request) {
|
|
3440
3773
|
const toolName = request.params?.name || "unknown";
|
|
3441
3774
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
3442
|
-
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
3443
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;
|
|
3444
3800
|
let credentialRef;
|
|
3445
3801
|
if (this.config.credentials) {
|
|
3446
3802
|
const cred = resolveCredential(toolName, this.config.credentials);
|
|
@@ -3491,6 +3847,32 @@ var ProtectGateway = class {
|
|
|
3491
3847
|
}
|
|
3492
3848
|
return null;
|
|
3493
3849
|
}
|
|
3850
|
+
if (toolPolicy.require_approval) {
|
|
3851
|
+
const grant = this.approvalStore.get(requestId);
|
|
3852
|
+
const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
|
|
3853
|
+
if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
|
|
3854
|
+
if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
|
|
3855
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3858
|
+
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3859
|
+
if (this.config.enforce) {
|
|
3860
|
+
return {
|
|
3861
|
+
jsonrpc: "2.0",
|
|
3862
|
+
id: request.id,
|
|
3863
|
+
result: {
|
|
3864
|
+
content: [
|
|
3865
|
+
{
|
|
3866
|
+
type: "text",
|
|
3867
|
+
text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
|
|
3868
|
+
}
|
|
3869
|
+
],
|
|
3870
|
+
isError: true
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
return null;
|
|
3875
|
+
}
|
|
3494
3876
|
const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
|
|
3495
3877
|
if (rateSpec) {
|
|
3496
3878
|
try {
|
|
@@ -3548,6 +3930,10 @@ var ProtectGateway = class {
|
|
|
3548
3930
|
if (signed.signed) {
|
|
3549
3931
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
3550
3932
|
`);
|
|
3933
|
+
try {
|
|
3934
|
+
(0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
3935
|
+
} catch {
|
|
3936
|
+
}
|
|
3551
3937
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
3552
3938
|
if (this.admissionResult?.agent_id) {
|
|
3553
3939
|
this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
|
|
@@ -3585,17 +3971,154 @@ var ProtectGateway = class {
|
|
|
3585
3971
|
}
|
|
3586
3972
|
};
|
|
3587
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
|
+
|
|
3588
4105
|
// src/cli.ts
|
|
3589
|
-
var import_meta = {};
|
|
3590
4106
|
function printHelp() {
|
|
3591
4107
|
process.stderr.write(`
|
|
3592
4108
|
protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
3593
4109
|
|
|
3594
4110
|
Usage:
|
|
3595
4111
|
protect-mcp [options] -- <command> [args...]
|
|
4112
|
+
protect-mcp quickstart
|
|
3596
4113
|
protect-mcp init [--dir <path>]
|
|
3597
4114
|
protect-mcp demo
|
|
4115
|
+
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
3598
4116
|
protect-mcp status [--dir <path>]
|
|
4117
|
+
protect-mcp digest [--today] [--dir <path>]
|
|
4118
|
+
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
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>]
|
|
3599
4122
|
|
|
3600
4123
|
Options:
|
|
3601
4124
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -3605,17 +4128,26 @@ Options:
|
|
|
3605
4128
|
--help Show this help
|
|
3606
4129
|
|
|
3607
4130
|
Commands:
|
|
4131
|
+
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
3608
4132
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
3609
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)
|
|
3610
4135
|
status Show tool call statistics from the local decision log
|
|
4136
|
+
digest Generate a human-readable summary of agent activity
|
|
4137
|
+
receipts Show recent persisted signed receipts
|
|
4138
|
+
bundle Export an offline-verifiable audit bundle
|
|
3611
4139
|
|
|
3612
4140
|
Examples:
|
|
4141
|
+
protect-mcp quickstart
|
|
3613
4142
|
protect-mcp -- node my-server.js
|
|
3614
4143
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
3615
|
-
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
3616
4144
|
protect-mcp init
|
|
3617
4145
|
protect-mcp demo
|
|
4146
|
+
protect-mcp trace sha256:abc123 --depth 5
|
|
3618
4147
|
protect-mcp status
|
|
4148
|
+
protect-mcp digest --today
|
|
4149
|
+
protect-mcp receipts --last 10
|
|
4150
|
+
protect-mcp bundle --output audit.json
|
|
3619
4151
|
|
|
3620
4152
|
`);
|
|
3621
4153
|
}
|
|
@@ -3659,7 +4191,7 @@ function parseArgs(argv) {
|
|
|
3659
4191
|
return { policyPath, slug, enforce, verbose, childCommand };
|
|
3660
4192
|
}
|
|
3661
4193
|
async function handleInit(argv) {
|
|
3662
|
-
const { writeFileSync: writeFileSync2, existsSync:
|
|
4194
|
+
const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
|
|
3663
4195
|
const { join: join4 } = await import("path");
|
|
3664
4196
|
let dir = process.cwd();
|
|
3665
4197
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -3669,21 +4201,18 @@ async function handleInit(argv) {
|
|
|
3669
4201
|
const configPath = join4(dir, "protect-mcp.json");
|
|
3670
4202
|
const keysDir = join4(dir, "keys");
|
|
3671
4203
|
const keyPath = join4(keysDir, "gateway.json");
|
|
3672
|
-
if (
|
|
4204
|
+
if (existsSync5(configPath)) {
|
|
3673
4205
|
process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
|
|
3674
4206
|
`);
|
|
3675
4207
|
process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
|
|
3676
4208
|
process.exit(1);
|
|
3677
4209
|
}
|
|
3678
4210
|
let keypair;
|
|
3679
|
-
|
|
3680
|
-
const
|
|
3681
|
-
keypair = artifacts.generateKeypair();
|
|
3682
|
-
} catch {
|
|
3683
|
-
const { randomBytes: randomBytes2 } = await import("crypto");
|
|
4211
|
+
{
|
|
4212
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
3684
4213
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
3685
4214
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3686
|
-
const privateKey =
|
|
4215
|
+
const privateKey = randomBytes3(32);
|
|
3687
4216
|
const publicKey = ed255192.getPublicKey(privateKey);
|
|
3688
4217
|
keypair = {
|
|
3689
4218
|
privateKey: bytesToHex2(privateKey),
|
|
@@ -3691,7 +4220,7 @@ async function handleInit(argv) {
|
|
|
3691
4220
|
kid: "generated"
|
|
3692
4221
|
};
|
|
3693
4222
|
}
|
|
3694
|
-
if (!
|
|
4223
|
+
if (!existsSync5(keysDir)) {
|
|
3695
4224
|
mkdirSync(keysDir, { recursive: true });
|
|
3696
4225
|
}
|
|
3697
4226
|
writeFileSync2(keyPath, JSON.stringify({
|
|
@@ -3702,7 +4231,7 @@ async function handleInit(argv) {
|
|
|
3702
4231
|
warning: "KEEP THIS FILE SECRET. Never commit to version control."
|
|
3703
4232
|
}, null, 2) + "\n");
|
|
3704
4233
|
const gitignorePath = join4(keysDir, ".gitignore");
|
|
3705
|
-
if (!
|
|
4234
|
+
if (!existsSync5(gitignorePath)) {
|
|
3706
4235
|
writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
|
|
3707
4236
|
}
|
|
3708
4237
|
const config = {
|
|
@@ -3775,14 +4304,13 @@ Add --enforce when ready to block policy violations.
|
|
|
3775
4304
|
`);
|
|
3776
4305
|
}
|
|
3777
4306
|
async function handleDemo() {
|
|
3778
|
-
const { existsSync:
|
|
3779
|
-
const { join: join4, dirname } = await import("path");
|
|
3780
|
-
const
|
|
3781
|
-
const
|
|
3782
|
-
const
|
|
3783
|
-
const demoServerPath = join4(__dirname, "demo-server.js");
|
|
4307
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
4308
|
+
const { join: join4, dirname, resolve } = await import("path");
|
|
4309
|
+
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
4310
|
+
const cliDir = dirname(cliPath);
|
|
4311
|
+
const demoServerPath = join4(cliDir, "demo-server.js");
|
|
3784
4312
|
const configPath = join4(process.cwd(), "protect-mcp.json");
|
|
3785
|
-
const hasConfig =
|
|
4313
|
+
const hasConfig = existsSync5(configPath);
|
|
3786
4314
|
if (!hasConfig) {
|
|
3787
4315
|
process.stderr.write(`
|
|
3788
4316
|
${bold("protect-mcp demo")}
|
|
@@ -3856,7 +4384,7 @@ Starting demo server with 5 tools...
|
|
|
3856
4384
|
await gateway.start();
|
|
3857
4385
|
}
|
|
3858
4386
|
async function handleStatus2(argv) {
|
|
3859
|
-
const { readFileSync:
|
|
4387
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
3860
4388
|
const { join: join4 } = await import("path");
|
|
3861
4389
|
let dir = process.cwd();
|
|
3862
4390
|
const dirIdx = argv.indexOf("--dir");
|
|
@@ -3864,7 +4392,7 @@ async function handleStatus2(argv) {
|
|
|
3864
4392
|
dir = argv[dirIdx + 1];
|
|
3865
4393
|
}
|
|
3866
4394
|
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
3867
|
-
if (!
|
|
4395
|
+
if (!existsSync5(logPath)) {
|
|
3868
4396
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
3869
4397
|
|
|
3870
4398
|
`);
|
|
@@ -3874,7 +4402,7 @@ async function handleStatus2(argv) {
|
|
|
3874
4402
|
`);
|
|
3875
4403
|
process.exit(0);
|
|
3876
4404
|
}
|
|
3877
|
-
const raw =
|
|
4405
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
3878
4406
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
3879
4407
|
if (lines.length === 0) {
|
|
3880
4408
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
@@ -3954,9 +4482,9 @@ ${bold("protect-mcp status")}
|
|
|
3954
4482
|
`);
|
|
3955
4483
|
}
|
|
3956
4484
|
const evidencePath = join4(dir, ".protect-mcp-evidence.json");
|
|
3957
|
-
if (
|
|
4485
|
+
if (existsSync5(evidencePath)) {
|
|
3958
4486
|
try {
|
|
3959
|
-
const evidenceRaw =
|
|
4487
|
+
const evidenceRaw = readFileSync7(evidencePath, "utf-8");
|
|
3960
4488
|
const evidence = JSON.parse(evidenceRaw);
|
|
3961
4489
|
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
3962
4490
|
process.stdout.write(`
|
|
@@ -3965,6 +4493,27 @@ ${bold("protect-mcp status")}
|
|
|
3965
4493
|
} catch {
|
|
3966
4494
|
}
|
|
3967
4495
|
}
|
|
4496
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4497
|
+
if (existsSync5(keyPath)) {
|
|
4498
|
+
try {
|
|
4499
|
+
const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
|
|
4500
|
+
if (keyData.publicKey) {
|
|
4501
|
+
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4502
|
+
process.stdout.write(`
|
|
4503
|
+
${bold("\u{1F6E1}\uFE0F Passport identity:")}
|
|
4504
|
+
`);
|
|
4505
|
+
process.stdout.write(` Public key: ${fingerprint}
|
|
4506
|
+
`);
|
|
4507
|
+
if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
|
|
4508
|
+
`);
|
|
4509
|
+
process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
|
|
4510
|
+
`);
|
|
4511
|
+
process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
|
|
4512
|
+
`);
|
|
4513
|
+
}
|
|
4514
|
+
} catch {
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
3968
4517
|
process.stdout.write(`
|
|
3969
4518
|
Log file: ${dim(logPath)}
|
|
3970
4519
|
|
|
@@ -3985,12 +4534,502 @@ function red(s) {
|
|
|
3985
4534
|
function yellow(s) {
|
|
3986
4535
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
3987
4536
|
}
|
|
4537
|
+
async function handleDigest(argv) {
|
|
4538
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4539
|
+
const { join: join4 } = await import("path");
|
|
4540
|
+
let dir = process.cwd();
|
|
4541
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4542
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4543
|
+
const today = argv.includes("--today");
|
|
4544
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4545
|
+
if (!existsSync5(logPath)) {
|
|
4546
|
+
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4547
|
+
|
|
4548
|
+
No log file found. Run protect-mcp first.
|
|
4549
|
+
`);
|
|
4550
|
+
process.exit(0);
|
|
4551
|
+
}
|
|
4552
|
+
const raw = readFileSync7(logPath, "utf-8");
|
|
4553
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4554
|
+
let entries = [];
|
|
4555
|
+
for (const line of lines) {
|
|
4556
|
+
try {
|
|
4557
|
+
entries.push(JSON.parse(line));
|
|
4558
|
+
} catch {
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
if (today) {
|
|
4562
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
4563
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
4564
|
+
entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
|
|
4565
|
+
}
|
|
4566
|
+
if (entries.length === 0) {
|
|
4567
|
+
process.stdout.write(`
|
|
4568
|
+
${bold("\u{1F6E1}\uFE0F Agent Digest")}
|
|
4569
|
+
|
|
4570
|
+
No activity${today ? " today" : ""}.
|
|
4571
|
+
|
|
4572
|
+
`);
|
|
4573
|
+
process.exit(0);
|
|
4574
|
+
}
|
|
4575
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4576
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4577
|
+
const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
|
|
4578
|
+
const toolUsage = /* @__PURE__ */ new Map();
|
|
4579
|
+
for (const e of entries) {
|
|
4580
|
+
toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
|
|
4581
|
+
}
|
|
4582
|
+
const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
|
|
4583
|
+
const currentTier = entries[entries.length - 1]?.tier || "unknown";
|
|
4584
|
+
const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
4585
|
+
const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
4586
|
+
const durationMs = lastTime.getTime() - firstTime.getTime();
|
|
4587
|
+
const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
|
|
4588
|
+
process.stdout.write(`
|
|
4589
|
+
${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
4590
|
+
|
|
4591
|
+
`);
|
|
4592
|
+
process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
|
|
4593
|
+
process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
|
|
4594
|
+
process.stdout.write(`${red("\u2717 " + denied)} blocked`);
|
|
4595
|
+
if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
|
|
4596
|
+
process.stdout.write(`
|
|
4597
|
+
`);
|
|
4598
|
+
process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
|
|
4599
|
+
|
|
4600
|
+
`);
|
|
4601
|
+
process.stdout.write(` ${bold("Tools used:")}
|
|
4602
|
+
`);
|
|
4603
|
+
for (const [tool, count] of sortedTools.slice(0, 8)) {
|
|
4604
|
+
process.stdout.write(` ${tool.padEnd(22)} ${count}x
|
|
4605
|
+
`);
|
|
4606
|
+
}
|
|
4607
|
+
if (denied > 0) {
|
|
4608
|
+
const deniedTools = entries.filter((e) => e.decision === "deny");
|
|
4609
|
+
const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
|
|
4610
|
+
process.stdout.write(`
|
|
4611
|
+
${bold(red("Blocked tools:"))}
|
|
4612
|
+
`);
|
|
4613
|
+
for (const tool of deniedToolNames) {
|
|
4614
|
+
const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
|
|
4615
|
+
process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
|
|
4616
|
+
`);
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
process.stdout.write(`
|
|
4620
|
+
${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
|
|
4621
|
+
`);
|
|
4622
|
+
process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
|
|
4623
|
+
`);
|
|
4624
|
+
process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
|
|
4625
|
+
|
|
4626
|
+
`);
|
|
4627
|
+
}
|
|
4628
|
+
async function handleReceipts2(argv) {
|
|
4629
|
+
const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
|
|
4630
|
+
const { join: join4 } = await import("path");
|
|
4631
|
+
let dir = process.cwd();
|
|
4632
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4633
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4634
|
+
const lastIdx = argv.indexOf("--last");
|
|
4635
|
+
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
4636
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4637
|
+
if (!existsSync5(receiptsPath)) {
|
|
4638
|
+
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
4639
|
+
|
|
4640
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4641
|
+
`);
|
|
4642
|
+
process.exit(0);
|
|
4643
|
+
}
|
|
4644
|
+
const raw = readFileSync7(receiptsPath, "utf-8");
|
|
4645
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4646
|
+
const recent = lines.slice(-count);
|
|
4647
|
+
process.stdout.write(`
|
|
4648
|
+
${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
4649
|
+
|
|
4650
|
+
`);
|
|
4651
|
+
for (const line of recent) {
|
|
4652
|
+
try {
|
|
4653
|
+
const entry = JSON.parse(line);
|
|
4654
|
+
const payload = entry.payload || {};
|
|
4655
|
+
const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
|
|
4656
|
+
const decision = payload.decision || "unknown";
|
|
4657
|
+
const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
|
|
4658
|
+
process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
|
|
4659
|
+
`);
|
|
4660
|
+
} catch {
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
process.stdout.write(`
|
|
4664
|
+
`);
|
|
4665
|
+
}
|
|
4666
|
+
async function handleBundle(argv) {
|
|
4667
|
+
const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
|
|
4668
|
+
const { join: join4 } = await import("path");
|
|
4669
|
+
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
4670
|
+
let dir = process.cwd();
|
|
4671
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4672
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4673
|
+
const outputIdx = argv.indexOf("--output");
|
|
4674
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
|
|
4675
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4676
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4677
|
+
if (!existsSync5(receiptsPath)) {
|
|
4678
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4679
|
+
|
|
4680
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4681
|
+
`);
|
|
4682
|
+
process.exit(0);
|
|
4683
|
+
}
|
|
4684
|
+
if (!existsSync5(keyPath)) {
|
|
4685
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4686
|
+
|
|
4687
|
+
No key file found at ${keyPath}
|
|
4688
|
+
`);
|
|
4689
|
+
process.exit(1);
|
|
4690
|
+
}
|
|
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"));
|
|
4693
|
+
const bundle = createAuditBundle2({
|
|
4694
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
4695
|
+
receipts,
|
|
4696
|
+
signingKeys: [{
|
|
4697
|
+
kty: "OKP",
|
|
4698
|
+
crv: "Ed25519",
|
|
4699
|
+
kid: keyData.kid || "unknown",
|
|
4700
|
+
x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
|
|
4701
|
+
use: "sig"
|
|
4702
|
+
}]
|
|
4703
|
+
});
|
|
4704
|
+
writeFileSync2(outputPath, JSON.stringify(bundle, null, 2) + "\n");
|
|
4705
|
+
process.stdout.write(`
|
|
4706
|
+
${bold("protect-mcp bundle")}
|
|
4707
|
+
|
|
4708
|
+
`);
|
|
4709
|
+
process.stdout.write(` Receipts: ${receipts.length}
|
|
4710
|
+
`);
|
|
4711
|
+
process.stdout.write(` Output: ${outputPath}
|
|
4712
|
+
`);
|
|
4713
|
+
process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
|
|
4714
|
+
|
|
4715
|
+
`);
|
|
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
|
+
}
|
|
3988
5023
|
async function main() {
|
|
3989
5024
|
const args = process.argv.slice(2);
|
|
3990
5025
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
3991
5026
|
printHelp();
|
|
3992
5027
|
process.exit(0);
|
|
3993
5028
|
}
|
|
5029
|
+
if (args[0] === "quickstart") {
|
|
5030
|
+
await handleQuickstart();
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
3994
5033
|
if (args[0] === "init") {
|
|
3995
5034
|
await handleInit(args.slice(1));
|
|
3996
5035
|
process.exit(0);
|
|
@@ -4003,6 +5042,30 @@ async function main() {
|
|
|
4003
5042
|
await handleStatus2(args.slice(1));
|
|
4004
5043
|
process.exit(0);
|
|
4005
5044
|
}
|
|
5045
|
+
if (args[0] === "digest") {
|
|
5046
|
+
await handleDigest(args.slice(1));
|
|
5047
|
+
process.exit(0);
|
|
5048
|
+
}
|
|
5049
|
+
if (args[0] === "receipts") {
|
|
5050
|
+
await handleReceipts2(args.slice(1));
|
|
5051
|
+
process.exit(0);
|
|
5052
|
+
}
|
|
5053
|
+
if (args[0] === "bundle") {
|
|
5054
|
+
await handleBundle(args.slice(1));
|
|
5055
|
+
process.exit(0);
|
|
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
|
+
}
|
|
4006
5069
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
4007
5070
|
let policy = null;
|
|
4008
5071
|
let policyDigest = "none";
|
|
@@ -4053,6 +5116,85 @@ async function main() {
|
|
|
4053
5116
|
const gateway = new ProtectGateway(config);
|
|
4054
5117
|
await gateway.start();
|
|
4055
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
|
+
}
|
|
4056
5198
|
main().catch((err) => {
|
|
4057
5199
|
process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
|
|
4058
5200
|
`);
|