protect-mcp 0.2.2 → 0.3.1
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-U7TMVD3E.mjs +1105 -0
- package/dist/cli.js +1176 -172
- package/dist/cli.mjs +462 -5
- package/dist/demo-server.d.mts +1 -0
- package/dist/demo-server.d.ts +1 -0
- package/dist/demo-server.js +137 -0
- package/dist/demo-server.mjs +136 -0
- package/dist/index.d.mts +83 -61
- package/dist/index.d.ts +83 -61
- package/dist/index.js +637 -271
- package/dist/index.mjs +7 -172
- package/package.json +3 -3
- package/dist/chunk-ZCKNFULF.mjs +0 -613
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,10 +2638,71 @@ 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
|
+
|
|
2641
2700
|
// src/gateway.ts
|
|
2642
2701
|
var import_node_child_process = require("child_process");
|
|
2643
2702
|
var import_node_crypto2 = require("crypto");
|
|
2644
2703
|
var import_node_readline = require("readline");
|
|
2704
|
+
var import_node_fs5 = require("fs");
|
|
2705
|
+
var import_node_path3 = require("path");
|
|
2645
2706
|
|
|
2646
2707
|
// src/policy.ts
|
|
2647
2708
|
var import_node_crypto = require("crypto");
|
|
@@ -2719,8 +2780,126 @@ function checkRateLimit(key, limit, store) {
|
|
|
2719
2780
|
return { allowed: true, remaining: limit.count - timestamps.length };
|
|
2720
2781
|
}
|
|
2721
2782
|
|
|
2783
|
+
// src/evidence-store.ts
|
|
2784
|
+
var import_node_fs2 = require("fs");
|
|
2785
|
+
var import_node_path = require("path");
|
|
2786
|
+
var DEFAULT_THRESHOLDS = {
|
|
2787
|
+
min_receipts: 10,
|
|
2788
|
+
min_epoch_span: 3,
|
|
2789
|
+
min_issuers: 2
|
|
2790
|
+
};
|
|
2791
|
+
var EvidenceStore = class {
|
|
2792
|
+
agents = /* @__PURE__ */ new Map();
|
|
2793
|
+
filePath;
|
|
2794
|
+
dirty = false;
|
|
2795
|
+
constructor(dir) {
|
|
2796
|
+
this.filePath = (0, import_node_path.join)(dir || process.cwd(), ".protect-mcp-evidence.json");
|
|
2797
|
+
this.load();
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Record a receipt observation for an agent.
|
|
2801
|
+
*/
|
|
2802
|
+
record(agentId, issuer, timestamp) {
|
|
2803
|
+
const ts = timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
2804
|
+
const epochHour = Math.floor(new Date(ts).getTime() / (3600 * 1e3));
|
|
2805
|
+
const existing = this.agents.get(agentId);
|
|
2806
|
+
const observation = {
|
|
2807
|
+
issuer,
|
|
2808
|
+
timestamp: ts,
|
|
2809
|
+
epoch_hour: epochHour
|
|
2810
|
+
};
|
|
2811
|
+
if (existing) {
|
|
2812
|
+
existing.receipts.push(observation);
|
|
2813
|
+
existing.last_seen = ts;
|
|
2814
|
+
if (existing.receipts.length > 200) {
|
|
2815
|
+
existing.receipts = existing.receipts.slice(-200);
|
|
2816
|
+
}
|
|
2817
|
+
} else {
|
|
2818
|
+
this.agents.set(agentId, {
|
|
2819
|
+
agent_id: agentId,
|
|
2820
|
+
receipts: [observation],
|
|
2821
|
+
first_seen: ts,
|
|
2822
|
+
last_seen: ts
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
this.dirty = true;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Get the evidence summary for an agent.
|
|
2829
|
+
*/
|
|
2830
|
+
getSummary(agentId) {
|
|
2831
|
+
const record = this.agents.get(agentId);
|
|
2832
|
+
if (!record || record.receipts.length === 0) {
|
|
2833
|
+
return { receipt_count: 0, epoch_span: 0, issuer_count: 0 };
|
|
2834
|
+
}
|
|
2835
|
+
const uniqueIssuers = new Set(record.receipts.map((r) => r.issuer));
|
|
2836
|
+
const uniqueEpochs = new Set(record.receipts.map((r) => r.epoch_hour));
|
|
2837
|
+
return {
|
|
2838
|
+
receipt_count: record.receipts.length,
|
|
2839
|
+
epoch_span: uniqueEpochs.size,
|
|
2840
|
+
issuer_count: uniqueIssuers.size
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Check if an agent meets the evidenced tier thresholds.
|
|
2845
|
+
*/
|
|
2846
|
+
meetsEvidencedThreshold(agentId, thresholds = DEFAULT_THRESHOLDS) {
|
|
2847
|
+
const summary = this.getSummary(agentId);
|
|
2848
|
+
return summary.receipt_count >= thresholds.min_receipts && summary.epoch_span >= thresholds.min_epoch_span && summary.issuer_count >= thresholds.min_issuers;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Persist to disk (call periodically or on shutdown).
|
|
2852
|
+
*/
|
|
2853
|
+
save() {
|
|
2854
|
+
if (!this.dirty) return;
|
|
2855
|
+
const data = {};
|
|
2856
|
+
for (const [id, record] of this.agents) {
|
|
2857
|
+
data[id] = record;
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
(0, import_node_fs2.writeFileSync)(this.filePath, JSON.stringify({ v: 1, agents: data }, null, 2) + "\n");
|
|
2861
|
+
this.dirty = false;
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Load from disk.
|
|
2867
|
+
*/
|
|
2868
|
+
load() {
|
|
2869
|
+
if (!(0, import_node_fs2.existsSync)(this.filePath)) return;
|
|
2870
|
+
try {
|
|
2871
|
+
const raw = (0, import_node_fs2.readFileSync)(this.filePath, "utf-8");
|
|
2872
|
+
const parsed = JSON.parse(raw);
|
|
2873
|
+
if (parsed.agents && typeof parsed.agents === "object") {
|
|
2874
|
+
for (const [id, record] of Object.entries(parsed.agents)) {
|
|
2875
|
+
this.agents.set(id, record);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
} catch {
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Get total agent count (for status display).
|
|
2883
|
+
*/
|
|
2884
|
+
agentCount() {
|
|
2885
|
+
return this.agents.size;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Get all agent summaries (for status display).
|
|
2889
|
+
*/
|
|
2890
|
+
allSummaries() {
|
|
2891
|
+
const result = [];
|
|
2892
|
+
for (const [id] of this.agents) {
|
|
2893
|
+
result.push({ agent_id: id, summary: this.getSummary(id) });
|
|
2894
|
+
}
|
|
2895
|
+
return result;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2722
2899
|
// src/admission.ts
|
|
2723
|
-
function evaluateTier(manifest,
|
|
2900
|
+
function evaluateTier(manifest, opts) {
|
|
2901
|
+
const options = opts && ("evidenceStore" in opts || "overrides" in opts || "thresholds" in opts) ? opts : { overrides: opts };
|
|
2902
|
+
const { overrides, evidenceStore, thresholds } = options;
|
|
2724
2903
|
if (!manifest) {
|
|
2725
2904
|
return {
|
|
2726
2905
|
tier: "unknown",
|
|
@@ -2746,7 +2925,8 @@ function evaluateTier(manifest, overrides) {
|
|
|
2746
2925
|
if (manifest.signature_valid === true) {
|
|
2747
2926
|
if (manifest.evidence_summary) {
|
|
2748
2927
|
const es = manifest.evidence_summary;
|
|
2749
|
-
|
|
2928
|
+
const t = thresholds || DEFAULT_THRESHOLDS;
|
|
2929
|
+
if (es.receipt_count >= t.min_receipts && es.epoch_span >= t.min_epoch_span && es.issuer_count >= t.min_issuers) {
|
|
2750
2930
|
return {
|
|
2751
2931
|
tier: "evidenced",
|
|
2752
2932
|
agent_id: manifest.agent_id,
|
|
@@ -2755,6 +2935,16 @@ function evaluateTier(manifest, overrides) {
|
|
|
2755
2935
|
};
|
|
2756
2936
|
}
|
|
2757
2937
|
}
|
|
2938
|
+
if (evidenceStore && manifest.agent_id) {
|
|
2939
|
+
if (evidenceStore.meetsEvidencedThreshold(manifest.agent_id, thresholds)) {
|
|
2940
|
+
return {
|
|
2941
|
+
tier: "evidenced",
|
|
2942
|
+
agent_id: manifest.agent_id,
|
|
2943
|
+
manifest_hash: manifest.manifest_hash,
|
|
2944
|
+
reason: "evidence_store_threshold_met"
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2758
2948
|
return {
|
|
2759
2949
|
tier: "signed-known",
|
|
2760
2950
|
agent_id: manifest.agent_id,
|
|
@@ -2820,7 +3010,7 @@ function validateCredentials(credentials) {
|
|
|
2820
3010
|
}
|
|
2821
3011
|
|
|
2822
3012
|
// src/signing.ts
|
|
2823
|
-
var
|
|
3013
|
+
var import_node_fs3 = require("fs");
|
|
2824
3014
|
var signerState = null;
|
|
2825
3015
|
var artifactsModule = null;
|
|
2826
3016
|
async function initSigning(config) {
|
|
@@ -2835,12 +3025,12 @@ async function initSigning(config) {
|
|
|
2835
3025
|
return warnings;
|
|
2836
3026
|
}
|
|
2837
3027
|
if (config.key_path) {
|
|
2838
|
-
if (!(0,
|
|
3028
|
+
if (!(0, import_node_fs3.existsSync)(config.key_path)) {
|
|
2839
3029
|
warnings.push(`signing: key file not found at ${config.key_path} \u2014 run "protect-mcp init" to generate`);
|
|
2840
3030
|
return warnings;
|
|
2841
3031
|
}
|
|
2842
3032
|
try {
|
|
2843
|
-
const keyData = JSON.parse((0,
|
|
3033
|
+
const keyData = JSON.parse((0, import_node_fs3.readFileSync)(config.key_path, "utf-8"));
|
|
2844
3034
|
if (!keyData.privateKey || !keyData.publicKey) {
|
|
2845
3035
|
warnings.push("signing: key file missing privateKey or publicKey fields");
|
|
2846
3036
|
return warnings;
|
|
@@ -2848,8 +3038,8 @@ async function initSigning(config) {
|
|
|
2848
3038
|
signerState = {
|
|
2849
3039
|
privateKey: keyData.privateKey,
|
|
2850
3040
|
publicKey: keyData.publicKey,
|
|
2851
|
-
kid: artifactsModule.computeKid(keyData.publicKey),
|
|
2852
|
-
issuer: config.issuer || "protect-mcp"
|
|
3041
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
3042
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
2853
3043
|
};
|
|
2854
3044
|
} catch (err) {
|
|
2855
3045
|
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2904,21 +3094,378 @@ function isSigningEnabled() {
|
|
|
2904
3094
|
return signerState !== null && artifactsModule !== null;
|
|
2905
3095
|
}
|
|
2906
3096
|
|
|
3097
|
+
// src/external-pdp.ts
|
|
3098
|
+
async function queryExternalPDP(context, config) {
|
|
3099
|
+
const timeout = config.timeout_ms || 500;
|
|
3100
|
+
const controller = new AbortController();
|
|
3101
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
3102
|
+
try {
|
|
3103
|
+
const body = formatRequest(context, config.format || "generic");
|
|
3104
|
+
const response = await fetch(config.endpoint, {
|
|
3105
|
+
method: "POST",
|
|
3106
|
+
headers: { "Content-Type": "application/json" },
|
|
3107
|
+
body: JSON.stringify(body),
|
|
3108
|
+
signal: controller.signal
|
|
3109
|
+
});
|
|
3110
|
+
clearTimeout(timer);
|
|
3111
|
+
if (!response.ok) {
|
|
3112
|
+
return fallbackDecision(config, `PDP returned HTTP ${response.status}`);
|
|
3113
|
+
}
|
|
3114
|
+
const result = await response.json();
|
|
3115
|
+
return parseResponse(result, config.format || "generic");
|
|
3116
|
+
} catch (err) {
|
|
3117
|
+
clearTimeout(timer);
|
|
3118
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
3119
|
+
return fallbackDecision(config, `PDP timeout after ${timeout}ms`);
|
|
3120
|
+
}
|
|
3121
|
+
return fallbackDecision(config, `PDP error: ${err instanceof Error ? err.message : "unknown"}`);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function formatRequest(context, format) {
|
|
3125
|
+
switch (format) {
|
|
3126
|
+
case "opa":
|
|
3127
|
+
return {
|
|
3128
|
+
input: {
|
|
3129
|
+
actor: context.actor,
|
|
3130
|
+
action: context.action,
|
|
3131
|
+
target: context.target,
|
|
3132
|
+
credential_ref: context.credential_ref,
|
|
3133
|
+
mode: context.mode,
|
|
3134
|
+
metadata: context.request_metadata
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
case "cerbos":
|
|
3138
|
+
return {
|
|
3139
|
+
principal: {
|
|
3140
|
+
id: context.actor.id || "unknown",
|
|
3141
|
+
roles: [context.actor.tier],
|
|
3142
|
+
attr: {
|
|
3143
|
+
manifest_hash: context.actor.manifest_hash
|
|
3144
|
+
}
|
|
3145
|
+
},
|
|
3146
|
+
resource: {
|
|
3147
|
+
kind: "tool",
|
|
3148
|
+
id: context.action.tool,
|
|
3149
|
+
attr: context.target
|
|
3150
|
+
},
|
|
3151
|
+
actions: [context.action.operation || "call"]
|
|
3152
|
+
};
|
|
3153
|
+
case "generic":
|
|
3154
|
+
default:
|
|
3155
|
+
return context;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
function parseResponse(result, format) {
|
|
3159
|
+
switch (format) {
|
|
3160
|
+
case "opa":
|
|
3161
|
+
if (typeof result.result === "boolean") {
|
|
3162
|
+
return { allowed: result.result };
|
|
3163
|
+
}
|
|
3164
|
+
if (result.result && typeof result.result === "object") {
|
|
3165
|
+
const r = result.result;
|
|
3166
|
+
return {
|
|
3167
|
+
allowed: Boolean(r.allow),
|
|
3168
|
+
reason: r.reason,
|
|
3169
|
+
metadata: r
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
return { allowed: false, reason: "unrecognized OPA response" };
|
|
3173
|
+
case "cerbos":
|
|
3174
|
+
if (Array.isArray(result.results) && result.results.length > 0) {
|
|
3175
|
+
const actions = result.results[0].actions;
|
|
3176
|
+
if (actions) {
|
|
3177
|
+
const effect = Object.values(actions)[0];
|
|
3178
|
+
return { allowed: effect === "EFFECT_ALLOW" };
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return { allowed: false, reason: "unrecognized Cerbos response" };
|
|
3182
|
+
case "generic":
|
|
3183
|
+
default:
|
|
3184
|
+
return {
|
|
3185
|
+
allowed: Boolean(result.allowed),
|
|
3186
|
+
reason: result.reason,
|
|
3187
|
+
metadata: result.metadata
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
function fallbackDecision(config, reason) {
|
|
3192
|
+
const fallback = config.fallback || "deny";
|
|
3193
|
+
return {
|
|
3194
|
+
allowed: fallback === "allow",
|
|
3195
|
+
reason: `fallback_${fallback}: ${reason}`
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
function buildDecisionContext(toolName, tier, opts) {
|
|
3199
|
+
return {
|
|
3200
|
+
v: 1,
|
|
3201
|
+
actor: {
|
|
3202
|
+
id: opts.agentId,
|
|
3203
|
+
tier,
|
|
3204
|
+
manifest_hash: opts.manifestHash
|
|
3205
|
+
},
|
|
3206
|
+
action: {
|
|
3207
|
+
tool: toolName,
|
|
3208
|
+
operation: "call"
|
|
3209
|
+
},
|
|
3210
|
+
target: {
|
|
3211
|
+
service: opts.slug || "default"
|
|
3212
|
+
},
|
|
3213
|
+
credential_ref: opts.credentialRef,
|
|
3214
|
+
mode: opts.mode,
|
|
3215
|
+
request_metadata: opts.requestMetadata || {}
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// src/http-server.ts
|
|
3220
|
+
var import_node_http = require("http");
|
|
3221
|
+
var import_node_fs4 = require("fs");
|
|
3222
|
+
var import_node_path2 = require("path");
|
|
3223
|
+
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
3224
|
+
var MAX_RECEIPTS = 100;
|
|
3225
|
+
var ReceiptBuffer = class {
|
|
3226
|
+
receipts = [];
|
|
3227
|
+
add(requestId, receipt) {
|
|
3228
|
+
this.receipts.push({
|
|
3229
|
+
request_id: requestId,
|
|
3230
|
+
receipt,
|
|
3231
|
+
timestamp: Date.now()
|
|
3232
|
+
});
|
|
3233
|
+
if (this.receipts.length > MAX_RECEIPTS) {
|
|
3234
|
+
this.receipts = this.receipts.slice(-MAX_RECEIPTS);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
getAll() {
|
|
3238
|
+
return [...this.receipts].reverse();
|
|
3239
|
+
}
|
|
3240
|
+
getById(requestId) {
|
|
3241
|
+
return this.receipts.find((r) => r.request_id === requestId);
|
|
3242
|
+
}
|
|
3243
|
+
count() {
|
|
3244
|
+
return this.receipts.length;
|
|
3245
|
+
}
|
|
3246
|
+
getLatest() {
|
|
3247
|
+
return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
3251
|
+
const startTime = Date.now();
|
|
3252
|
+
const logDir = process.cwd();
|
|
3253
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
3254
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3255
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
3256
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
3257
|
+
res.setHeader("Content-Type", "application/json");
|
|
3258
|
+
if (req.method === "OPTIONS") {
|
|
3259
|
+
res.writeHead(204);
|
|
3260
|
+
res.end();
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
const url = new URL(req.url || "/", `http://localhost:${config.port}`);
|
|
3264
|
+
const path = url.pathname;
|
|
3265
|
+
try {
|
|
3266
|
+
if (path === "/health") {
|
|
3267
|
+
handleHealth(res, startTime, config);
|
|
3268
|
+
} else if (path === "/status") {
|
|
3269
|
+
handleStatus(res, logDir);
|
|
3270
|
+
} else if (path === "/receipts") {
|
|
3271
|
+
handleReceipts(res, receiptBuffer, url);
|
|
3272
|
+
} else if (path === "/receipts/latest") {
|
|
3273
|
+
handleReceiptLatest(res, receiptBuffer);
|
|
3274
|
+
} else if (path.startsWith("/receipts/")) {
|
|
3275
|
+
const id = path.slice("/receipts/".length);
|
|
3276
|
+
handleReceiptById(res, receiptBuffer, id);
|
|
3277
|
+
} else if (path === "/approve" && req.method === "POST") {
|
|
3278
|
+
handleApprove(req, res, approvalStore, approvalNonce);
|
|
3279
|
+
} else if (path === "/approvals" && req.method === "GET") {
|
|
3280
|
+
handleListApprovals(res, approvalStore);
|
|
3281
|
+
} else {
|
|
3282
|
+
res.writeHead(404);
|
|
3283
|
+
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
|
|
3284
|
+
}
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
res.writeHead(500);
|
|
3287
|
+
res.end(JSON.stringify({ error: "internal_error" }));
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3290
|
+
server.on("error", (err) => {
|
|
3291
|
+
if (config.verbose) {
|
|
3292
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
|
|
3293
|
+
`);
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
server.listen(config.port, "127.0.0.1", () => {
|
|
3297
|
+
if (config.verbose) {
|
|
3298
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
3299
|
+
`);
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
server.unref();
|
|
3303
|
+
return server;
|
|
3304
|
+
}
|
|
3305
|
+
function handleHealth(res, startTime, config) {
|
|
3306
|
+
res.writeHead(200);
|
|
3307
|
+
res.end(JSON.stringify({
|
|
3308
|
+
status: "ok",
|
|
3309
|
+
uptime_ms: Date.now() - startTime,
|
|
3310
|
+
mode: config.mode,
|
|
3311
|
+
version: "0.3.1"
|
|
3312
|
+
}));
|
|
3313
|
+
}
|
|
3314
|
+
function handleStatus(res, logDir) {
|
|
3315
|
+
const logPath = (0, import_node_path2.join)(logDir, LOG_FILE);
|
|
3316
|
+
if (!(0, import_node_fs4.existsSync)(logPath)) {
|
|
3317
|
+
res.writeHead(200);
|
|
3318
|
+
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
const raw = (0, import_node_fs4.readFileSync)(logPath, "utf-8");
|
|
3322
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
3323
|
+
const entries = [];
|
|
3324
|
+
for (const line of lines) {
|
|
3325
|
+
try {
|
|
3326
|
+
entries.push(JSON.parse(line));
|
|
3327
|
+
} catch {
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
const toolCounts = {};
|
|
3331
|
+
let allowCount = 0, denyCount = 0;
|
|
3332
|
+
const tierCounts = {};
|
|
3333
|
+
for (const e of entries) {
|
|
3334
|
+
toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
3335
|
+
if (e.decision === "allow") allowCount++;
|
|
3336
|
+
else denyCount++;
|
|
3337
|
+
if (e.tier) tierCounts[e.tier] = (tierCounts[e.tier] || 0) + 1;
|
|
3338
|
+
}
|
|
3339
|
+
res.writeHead(200);
|
|
3340
|
+
res.end(JSON.stringify({
|
|
3341
|
+
entries: entries.length,
|
|
3342
|
+
allow: allowCount,
|
|
3343
|
+
deny: denyCount,
|
|
3344
|
+
tools: toolCounts,
|
|
3345
|
+
tiers: tierCounts,
|
|
3346
|
+
first_timestamp: entries.length > 0 ? entries[0].timestamp : null,
|
|
3347
|
+
last_timestamp: entries.length > 0 ? entries[entries.length - 1].timestamp : null
|
|
3348
|
+
}));
|
|
3349
|
+
}
|
|
3350
|
+
function handleReceipts(res, buffer, url) {
|
|
3351
|
+
const limit = parseInt(url.searchParams.get("limit") || "20", 10);
|
|
3352
|
+
const receipts = buffer.getAll().slice(0, Math.min(limit, MAX_RECEIPTS));
|
|
3353
|
+
res.writeHead(200);
|
|
3354
|
+
res.end(JSON.stringify({
|
|
3355
|
+
count: receipts.length,
|
|
3356
|
+
total: buffer.count(),
|
|
3357
|
+
receipts
|
|
3358
|
+
}));
|
|
3359
|
+
}
|
|
3360
|
+
function handleReceiptLatest(res, buffer) {
|
|
3361
|
+
const latest = buffer.getLatest();
|
|
3362
|
+
if (!latest) {
|
|
3363
|
+
res.writeHead(404);
|
|
3364
|
+
res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
res.writeHead(200);
|
|
3368
|
+
res.end(JSON.stringify(latest));
|
|
3369
|
+
}
|
|
3370
|
+
function handleReceiptById(res, buffer, id) {
|
|
3371
|
+
const receipt = buffer.getById(id);
|
|
3372
|
+
if (!receipt) {
|
|
3373
|
+
res.writeHead(404);
|
|
3374
|
+
res.end(JSON.stringify({ error: "receipt_not_found", request_id: id }));
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
res.writeHead(200);
|
|
3378
|
+
res.end(JSON.stringify(receipt));
|
|
3379
|
+
}
|
|
3380
|
+
function handleApprove(req, res, approvalStore, expectedNonce) {
|
|
3381
|
+
if (!approvalStore) {
|
|
3382
|
+
res.writeHead(503);
|
|
3383
|
+
res.end(JSON.stringify({ error: "approval_store_not_available" }));
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
let body = "";
|
|
3387
|
+
req.on("data", (chunk) => {
|
|
3388
|
+
body += chunk.toString();
|
|
3389
|
+
});
|
|
3390
|
+
req.on("end", () => {
|
|
3391
|
+
try {
|
|
3392
|
+
const { request_id, tool, mode, nonce } = JSON.parse(body);
|
|
3393
|
+
if (expectedNonce && nonce !== expectedNonce) {
|
|
3394
|
+
res.writeHead(403);
|
|
3395
|
+
res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
if (!tool || typeof tool !== "string") {
|
|
3399
|
+
res.writeHead(400);
|
|
3400
|
+
res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
const grantMode = mode === "always" ? "always" : "once";
|
|
3404
|
+
const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
|
|
3405
|
+
const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
|
|
3406
|
+
if (grantMode === "always") {
|
|
3407
|
+
approvalStore.set(`always:${tool}`, grantEntry);
|
|
3408
|
+
} else if (request_id) {
|
|
3409
|
+
approvalStore.set(request_id, grantEntry);
|
|
3410
|
+
} else {
|
|
3411
|
+
approvalStore.set(tool, grantEntry);
|
|
3412
|
+
}
|
|
3413
|
+
res.writeHead(200);
|
|
3414
|
+
res.end(JSON.stringify({
|
|
3415
|
+
approved: true,
|
|
3416
|
+
request_id: request_id || null,
|
|
3417
|
+
tool,
|
|
3418
|
+
mode: grantMode,
|
|
3419
|
+
expires_in_seconds: ttlMs / 1e3
|
|
3420
|
+
}));
|
|
3421
|
+
} catch {
|
|
3422
|
+
res.writeHead(400);
|
|
3423
|
+
res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
function handleListApprovals(res, approvalStore) {
|
|
3428
|
+
if (!approvalStore) {
|
|
3429
|
+
res.writeHead(200);
|
|
3430
|
+
res.end(JSON.stringify({ grants: [] }));
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
const now = Date.now();
|
|
3434
|
+
const grants = [];
|
|
3435
|
+
for (const [key, grant] of approvalStore) {
|
|
3436
|
+
if (now < grant.expires_at) {
|
|
3437
|
+
grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
res.writeHead(200);
|
|
3441
|
+
res.end(JSON.stringify({ grants }));
|
|
3442
|
+
}
|
|
3443
|
+
|
|
2907
3444
|
// src/gateway.ts
|
|
3445
|
+
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
3446
|
+
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
2908
3447
|
var ProtectGateway = class {
|
|
2909
3448
|
child = null;
|
|
2910
3449
|
config;
|
|
2911
3450
|
rateLimitStore = /* @__PURE__ */ new Map();
|
|
2912
3451
|
clientReader = null;
|
|
2913
|
-
|
|
3452
|
+
logFilePath;
|
|
3453
|
+
receiptFilePath;
|
|
3454
|
+
evidenceStore;
|
|
3455
|
+
receiptBuffer;
|
|
3456
|
+
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
3457
|
+
approvalStore = /* @__PURE__ */ new Map();
|
|
3458
|
+
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
3459
|
+
approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
2914
3460
|
currentTier = "unknown";
|
|
2915
3461
|
admissionResult = null;
|
|
2916
3462
|
constructor(config) {
|
|
2917
3463
|
this.config = config;
|
|
3464
|
+
this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
|
|
3465
|
+
this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
|
|
3466
|
+
this.evidenceStore = new EvidenceStore();
|
|
3467
|
+
this.receiptBuffer = new ReceiptBuffer();
|
|
2918
3468
|
}
|
|
2919
|
-
/**
|
|
2920
|
-
* Start the gateway: spawn child process and wire up message relay.
|
|
2921
|
-
*/
|
|
2922
3469
|
async start() {
|
|
2923
3470
|
const { command, args, verbose } = this.config;
|
|
2924
3471
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
@@ -2935,11 +3482,37 @@ var ProtectGateway = class {
|
|
|
2935
3482
|
const labels = Object.keys(this.config.credentials);
|
|
2936
3483
|
this.log(`Credential vault: ${labels.length} credential(s) configured [${labels.join(", ")}]`);
|
|
2937
3484
|
}
|
|
3485
|
+
if (this.config.policy?.policy_engine === "external" || this.config.policy?.policy_engine === "hybrid") {
|
|
3486
|
+
this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
|
|
3487
|
+
}
|
|
2938
3488
|
}
|
|
2939
|
-
this.
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3489
|
+
this.log(`Approval nonce: ${this.approvalNonce}`);
|
|
3490
|
+
const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
|
|
3491
|
+
if (httpPort > 0) {
|
|
3492
|
+
try {
|
|
3493
|
+
startStatusServer(
|
|
3494
|
+
{ port: httpPort, mode, verbose },
|
|
3495
|
+
this.receiptBuffer,
|
|
3496
|
+
this.approvalStore,
|
|
3497
|
+
this.approvalNonce
|
|
3498
|
+
);
|
|
3499
|
+
} catch {
|
|
3500
|
+
if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
const childEnv = { ...process.env };
|
|
3504
|
+
if (this.config.credentials) {
|
|
3505
|
+
for (const [label, credConfig] of Object.entries(this.config.credentials)) {
|
|
3506
|
+
if (credConfig.inject === "env" && credConfig.name && credConfig.value_env) {
|
|
3507
|
+
const envValue = process.env[credConfig.value_env];
|
|
3508
|
+
if (envValue) {
|
|
3509
|
+
childEnv[credConfig.name] = envValue;
|
|
3510
|
+
if (verbose) this.log(`Credential "${label}": injected as env var "${credConfig.name}"`);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
this.child = (0, import_node_child_process.spawn)(command, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
|
|
2943
3516
|
if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
|
|
2944
3517
|
throw new Error("Failed to create pipes to child process");
|
|
2945
3518
|
}
|
|
@@ -2955,9 +3528,8 @@ var ProtectGateway = class {
|
|
|
2955
3528
|
this.handleClientMessage(line);
|
|
2956
3529
|
});
|
|
2957
3530
|
this.child.on("exit", (code, signal) => {
|
|
2958
|
-
if (
|
|
2959
|
-
|
|
2960
|
-
}
|
|
3531
|
+
if (verbose) this.log(`Child process exited (code=${code}, signal=${signal})`);
|
|
3532
|
+
this.evidenceStore.save();
|
|
2961
3533
|
process.exit(code ?? 1);
|
|
2962
3534
|
});
|
|
2963
3535
|
this.child.on("error", (err) => {
|
|
@@ -2967,30 +3539,18 @@ var ProtectGateway = class {
|
|
|
2967
3539
|
process.on("SIGINT", () => this.stop());
|
|
2968
3540
|
process.on("SIGTERM", () => this.stop());
|
|
2969
3541
|
process.stdin.on("end", () => {
|
|
2970
|
-
if (this.
|
|
2971
|
-
|
|
2972
|
-
}
|
|
2973
|
-
if (this.child?.stdin?.writable) {
|
|
2974
|
-
this.child.stdin.end();
|
|
2975
|
-
}
|
|
3542
|
+
if (verbose) this.log("Client stdin closed, closing child stdin");
|
|
3543
|
+
if (this.child?.stdin?.writable) this.child.stdin.end();
|
|
2976
3544
|
});
|
|
2977
3545
|
}
|
|
2978
|
-
/**
|
|
2979
|
-
* Set the trust tier for this session.
|
|
2980
|
-
* Called at admission (first interaction) or by explicit manifest presentation.
|
|
2981
|
-
*/
|
|
2982
3546
|
setManifest(manifest) {
|
|
2983
|
-
this.admissionResult = evaluateTier(manifest);
|
|
3547
|
+
this.admissionResult = evaluateTier(manifest, { evidenceStore: this.evidenceStore });
|
|
2984
3548
|
this.currentTier = this.admissionResult.tier;
|
|
2985
3549
|
if (this.config.verbose) {
|
|
2986
|
-
this.log(`Admission: tier=${this.currentTier} agent=${this.admissionResult.agent_id || "none"}
|
|
3550
|
+
this.log(`Admission: tier=${this.currentTier} agent=${this.admissionResult.agent_id || "none"}`);
|
|
2987
3551
|
}
|
|
2988
3552
|
return this.admissionResult;
|
|
2989
3553
|
}
|
|
2990
|
-
/**
|
|
2991
|
-
* Handle a message from the MCP client (stdin).
|
|
2992
|
-
* Intercept tools/call requests; pass through everything else.
|
|
2993
|
-
*/
|
|
2994
3554
|
handleClientMessage(raw) {
|
|
2995
3555
|
const trimmed = raw.trim();
|
|
2996
3556
|
if (!trimmed) return;
|
|
@@ -3002,25 +3562,38 @@ var ProtectGateway = class {
|
|
|
3002
3562
|
return;
|
|
3003
3563
|
}
|
|
3004
3564
|
if (message.method === "tools/call" && message.id !== void 0) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
this.sendToClient(JSON.stringify(result));
|
|
3008
|
-
return;
|
|
3009
|
-
}
|
|
3565
|
+
this.interceptToolCallAsync(message, trimmed);
|
|
3566
|
+
return;
|
|
3010
3567
|
}
|
|
3011
3568
|
this.sendToChild(trimmed);
|
|
3012
3569
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3570
|
+
async interceptToolCallAsync(request, raw) {
|
|
3571
|
+
const result = await this.interceptToolCall(request);
|
|
3572
|
+
if (result) {
|
|
3573
|
+
this.sendToClient(JSON.stringify(result));
|
|
3574
|
+
} else {
|
|
3575
|
+
const modified = this.injectParamsCredentials(request);
|
|
3576
|
+
this.sendToChild(JSON.stringify(modified));
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3017
3579
|
handleServerMessage(raw) {
|
|
3018
3580
|
this.sendToClient(raw);
|
|
3019
3581
|
}
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3582
|
+
injectParamsCredentials(request) {
|
|
3583
|
+
if (!this.config.credentials) return request;
|
|
3584
|
+
const injections = {};
|
|
3585
|
+
for (const [label, credConfig] of Object.entries(this.config.credentials)) {
|
|
3586
|
+
if (credConfig.inject === "header" || credConfig.inject === "query") {
|
|
3587
|
+
const cred = resolveCredential(label, this.config.credentials);
|
|
3588
|
+
if (cred.resolved && cred.value && cred.name) {
|
|
3589
|
+
injections[cred.name] = cred.value;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (Object.keys(injections).length === 0) return request;
|
|
3594
|
+
return { ...request, params: { ...request.params, _credentials: injections } };
|
|
3595
|
+
}
|
|
3596
|
+
async interceptToolCall(request) {
|
|
3024
3597
|
const toolName = request.params?.name || "unknown";
|
|
3025
3598
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
3026
3599
|
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
@@ -3031,54 +3604,76 @@ var ProtectGateway = class {
|
|
|
3031
3604
|
if (cred.resolved) {
|
|
3032
3605
|
credentialRef = cred.label;
|
|
3033
3606
|
} else if (cred.error && !cred.error.includes("not configured")) {
|
|
3034
|
-
this.emitDecisionLog({
|
|
3035
|
-
tool: toolName,
|
|
3036
|
-
decision: "deny",
|
|
3037
|
-
reason_code: "policy_block",
|
|
3038
|
-
request_id: requestId,
|
|
3039
|
-
credential_ref: toolName,
|
|
3040
|
-
tier: this.currentTier
|
|
3041
|
-
});
|
|
3607
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName });
|
|
3042
3608
|
if (this.config.enforce) {
|
|
3043
|
-
this.log(`Credential error for "${toolName}": ${cred.error}`);
|
|
3044
3609
|
return this.makeErrorResponse(request.id, -32600, `Credential error for tool "${toolName}"`);
|
|
3045
3610
|
}
|
|
3046
3611
|
}
|
|
3047
3612
|
}
|
|
3613
|
+
if (this.config.policy?.external && (this.config.policy.policy_engine === "external" || this.config.policy.policy_engine === "hybrid")) {
|
|
3614
|
+
try {
|
|
3615
|
+
const ctx = buildDecisionContext(toolName, this.currentTier, {
|
|
3616
|
+
agentId: this.admissionResult?.agent_id,
|
|
3617
|
+
manifestHash: this.admissionResult?.manifest_hash,
|
|
3618
|
+
credentialRef,
|
|
3619
|
+
mode,
|
|
3620
|
+
slug: this.config.slug
|
|
3621
|
+
});
|
|
3622
|
+
const externalDecision = await queryExternalPDP(ctx, this.config.policy.external);
|
|
3623
|
+
if (!externalDecision.allowed) {
|
|
3624
|
+
const reason = `external_pdp_deny${externalDecision.reason ? ": " + externalDecision.reason : ""}`;
|
|
3625
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3626
|
+
if (this.config.enforce) {
|
|
3627
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by external policy engine`);
|
|
3628
|
+
}
|
|
3629
|
+
if (this.config.policy.policy_engine === "external") return null;
|
|
3630
|
+
}
|
|
3631
|
+
} catch (err) {
|
|
3632
|
+
if (this.config.verbose) this.log(`External PDP error: ${err instanceof Error ? err.message : err}`);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3048
3635
|
if (toolPolicy.min_tier) {
|
|
3049
3636
|
if (!meetsMinTier(this.currentTier, toolPolicy.min_tier)) {
|
|
3050
|
-
this.emitDecisionLog({
|
|
3051
|
-
tool: toolName,
|
|
3052
|
-
decision: "deny",
|
|
3053
|
-
reason_code: "tier_insufficient",
|
|
3054
|
-
request_id: requestId,
|
|
3055
|
-
tier: this.currentTier,
|
|
3056
|
-
credential_ref: credentialRef
|
|
3057
|
-
});
|
|
3637
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3058
3638
|
if (this.config.enforce) {
|
|
3059
|
-
return this.makeErrorResponse(
|
|
3060
|
-
request.id,
|
|
3061
|
-
-32600,
|
|
3062
|
-
`Tool "${toolName}" requires tier "${toolPolicy.min_tier}", agent has "${this.currentTier}"`
|
|
3063
|
-
);
|
|
3639
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" requires tier "${toolPolicy.min_tier}"`);
|
|
3064
3640
|
}
|
|
3065
3641
|
return null;
|
|
3066
3642
|
}
|
|
3067
3643
|
}
|
|
3068
3644
|
if (toolPolicy.block) {
|
|
3069
|
-
this.emitDecisionLog({
|
|
3070
|
-
tool: toolName,
|
|
3071
|
-
decision: "deny",
|
|
3072
|
-
reason_code: "policy_block",
|
|
3073
|
-
request_id: requestId,
|
|
3074
|
-
tier: this.currentTier,
|
|
3075
|
-
credential_ref: credentialRef
|
|
3076
|
-
});
|
|
3645
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3077
3646
|
if (this.config.enforce) {
|
|
3078
3647
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" is blocked by policy`);
|
|
3079
3648
|
}
|
|
3080
3649
|
return null;
|
|
3081
3650
|
}
|
|
3651
|
+
if (toolPolicy.require_approval) {
|
|
3652
|
+
const grant = this.approvalStore.get(requestId);
|
|
3653
|
+
const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
|
|
3654
|
+
if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
|
|
3655
|
+
if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
|
|
3656
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3660
|
+
if (this.config.enforce) {
|
|
3661
|
+
return {
|
|
3662
|
+
jsonrpc: "2.0",
|
|
3663
|
+
id: request.id,
|
|
3664
|
+
result: {
|
|
3665
|
+
content: [
|
|
3666
|
+
{
|
|
3667
|
+
type: "text",
|
|
3668
|
+
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.`
|
|
3669
|
+
}
|
|
3670
|
+
],
|
|
3671
|
+
isError: true
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
return null;
|
|
3676
|
+
}
|
|
3082
3677
|
const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
|
|
3083
3678
|
if (rateSpec) {
|
|
3084
3679
|
try {
|
|
@@ -3086,59 +3681,22 @@ var ProtectGateway = class {
|
|
|
3086
3681
|
const key = `tool:${toolName}:${this.currentTier}`;
|
|
3087
3682
|
const { allowed, remaining } = checkRateLimit(key, limit, this.rateLimitStore);
|
|
3088
3683
|
if (!allowed) {
|
|
3089
|
-
this.emitDecisionLog({
|
|
3090
|
-
tool: toolName,
|
|
3091
|
-
decision: "deny",
|
|
3092
|
-
reason_code: "rate_limit_exceeded",
|
|
3093
|
-
request_id: requestId,
|
|
3094
|
-
rate_limit_remaining: 0,
|
|
3095
|
-
tier: this.currentTier,
|
|
3096
|
-
credential_ref: credentialRef
|
|
3097
|
-
});
|
|
3684
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef });
|
|
3098
3685
|
if (this.config.enforce) {
|
|
3099
|
-
return this.makeErrorResponse(
|
|
3100
|
-
request.id,
|
|
3101
|
-
-32600,
|
|
3102
|
-
`Tool "${toolName}" rate limit exceeded (${rateSpec})`
|
|
3103
|
-
);
|
|
3686
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" rate limit exceeded (${rateSpec})`);
|
|
3104
3687
|
}
|
|
3105
3688
|
return null;
|
|
3106
3689
|
}
|
|
3107
|
-
this.emitDecisionLog({
|
|
3108
|
-
tool: toolName,
|
|
3109
|
-
decision: "allow",
|
|
3110
|
-
reason_code: "policy_allow",
|
|
3111
|
-
request_id: requestId,
|
|
3112
|
-
rate_limit_remaining: remaining,
|
|
3113
|
-
tier: this.currentTier,
|
|
3114
|
-
credential_ref: credentialRef
|
|
3115
|
-
});
|
|
3690
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef });
|
|
3116
3691
|
} catch {
|
|
3117
|
-
this.emitDecisionLog({
|
|
3118
|
-
tool: toolName,
|
|
3119
|
-
decision: "allow",
|
|
3120
|
-
reason_code: "default_allow",
|
|
3121
|
-
request_id: requestId,
|
|
3122
|
-
tier: this.currentTier,
|
|
3123
|
-
credential_ref: credentialRef
|
|
3124
|
-
});
|
|
3692
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3125
3693
|
}
|
|
3126
3694
|
} else {
|
|
3127
3695
|
const reasonCode = this.config.enforce ? "policy_allow" : "observe_mode";
|
|
3128
|
-
this.emitDecisionLog({
|
|
3129
|
-
tool: toolName,
|
|
3130
|
-
decision: "allow",
|
|
3131
|
-
reason_code: reasonCode,
|
|
3132
|
-
request_id: requestId,
|
|
3133
|
-
tier: this.currentTier,
|
|
3134
|
-
credential_ref: credentialRef
|
|
3135
|
-
});
|
|
3696
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3136
3697
|
}
|
|
3137
3698
|
return null;
|
|
3138
3699
|
}
|
|
3139
|
-
/**
|
|
3140
|
-
* Get the applicable rate limit spec based on the agent's tier.
|
|
3141
|
-
*/
|
|
3142
3700
|
getTierRateLimit(policy, tier) {
|
|
3143
3701
|
if (policy.rate_limits && policy.rate_limits[tier]) {
|
|
3144
3702
|
const tierLimit = policy.rate_limits[tier];
|
|
@@ -3146,10 +3704,6 @@ var ProtectGateway = class {
|
|
|
3146
3704
|
}
|
|
3147
3705
|
return policy.rate_limit;
|
|
3148
3706
|
}
|
|
3149
|
-
/**
|
|
3150
|
-
* Emit a structured decision log to stderr.
|
|
3151
|
-
* If signing is enabled, also emits a signed artifact.
|
|
3152
|
-
*/
|
|
3153
3707
|
emitDecisionLog(entry) {
|
|
3154
3708
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
3155
3709
|
const log = {
|
|
@@ -3168,55 +3722,48 @@ var ProtectGateway = class {
|
|
|
3168
3722
|
};
|
|
3169
3723
|
process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
|
|
3170
3724
|
`);
|
|
3725
|
+
try {
|
|
3726
|
+
(0, import_node_fs5.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
|
|
3727
|
+
} catch {
|
|
3728
|
+
}
|
|
3171
3729
|
if (isSigningEnabled()) {
|
|
3172
3730
|
const signed = signDecision(log);
|
|
3173
3731
|
if (signed.signed) {
|
|
3174
3732
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
3175
3733
|
`);
|
|
3734
|
+
try {
|
|
3735
|
+
(0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3738
|
+
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
3739
|
+
if (this.admissionResult?.agent_id) {
|
|
3740
|
+
this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
|
|
3741
|
+
if (this.evidenceStore.getSummary(this.admissionResult.agent_id).receipt_count % 10 === 0) {
|
|
3742
|
+
this.evidenceStore.save();
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3176
3745
|
} else if (signed.warning) {
|
|
3177
3746
|
process.stderr.write(`[PROTECT_MCP] Warning: ${signed.warning}
|
|
3178
3747
|
`);
|
|
3179
3748
|
}
|
|
3180
3749
|
}
|
|
3181
3750
|
}
|
|
3182
|
-
/**
|
|
3183
|
-
* Create a JSON-RPC error response.
|
|
3184
|
-
*/
|
|
3185
3751
|
makeErrorResponse(id, code, message) {
|
|
3186
|
-
return {
|
|
3187
|
-
jsonrpc: "2.0",
|
|
3188
|
-
id,
|
|
3189
|
-
error: { code, message }
|
|
3190
|
-
};
|
|
3752
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
3191
3753
|
}
|
|
3192
|
-
/**
|
|
3193
|
-
* Send a message to the child process (wrapped MCP server).
|
|
3194
|
-
*/
|
|
3195
3754
|
sendToChild(message) {
|
|
3196
|
-
if (this.child?.stdin?.writable)
|
|
3197
|
-
this.child.stdin.write(message + "\n");
|
|
3198
|
-
}
|
|
3755
|
+
if (this.child?.stdin?.writable) this.child.stdin.write(message + "\n");
|
|
3199
3756
|
}
|
|
3200
|
-
/**
|
|
3201
|
-
* Send a message to the MCP client (stdout).
|
|
3202
|
-
*/
|
|
3203
3757
|
sendToClient(message) {
|
|
3204
3758
|
process.stdout.write(message + "\n");
|
|
3205
3759
|
}
|
|
3206
|
-
/**
|
|
3207
|
-
* Log a message to stderr (debug output).
|
|
3208
|
-
*/
|
|
3209
3760
|
log(message) {
|
|
3210
3761
|
process.stderr.write(`[PROTECT_MCP] ${message}
|
|
3211
3762
|
`);
|
|
3212
3763
|
}
|
|
3213
|
-
/**
|
|
3214
|
-
* Stop the gateway: kill child process and exit.
|
|
3215
|
-
*/
|
|
3216
3764
|
stop() {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
}
|
|
3765
|
+
this.evidenceStore.save();
|
|
3766
|
+
if (this.clientReader) this.clientReader.close();
|
|
3220
3767
|
if (this.child) {
|
|
3221
3768
|
this.child.kill("SIGTERM");
|
|
3222
3769
|
this.child = null;
|
|
@@ -3233,6 +3780,11 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
|
3233
3780
|
Usage:
|
|
3234
3781
|
protect-mcp [options] -- <command> [args...]
|
|
3235
3782
|
protect-mcp init [--dir <path>]
|
|
3783
|
+
protect-mcp demo
|
|
3784
|
+
protect-mcp status [--dir <path>]
|
|
3785
|
+
protect-mcp digest [--today] [--dir <path>]
|
|
3786
|
+
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
3787
|
+
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
3236
3788
|
|
|
3237
3789
|
Options:
|
|
3238
3790
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -3243,12 +3795,22 @@ Options:
|
|
|
3243
3795
|
|
|
3244
3796
|
Commands:
|
|
3245
3797
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
3798
|
+
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
3799
|
+
status Show tool call statistics from the local decision log
|
|
3800
|
+
digest Generate a human-readable summary of agent activity
|
|
3801
|
+
receipts Show recent persisted signed receipts
|
|
3802
|
+
bundle Export an offline-verifiable audit bundle
|
|
3246
3803
|
|
|
3247
3804
|
Examples:
|
|
3248
3805
|
protect-mcp -- node my-server.js
|
|
3249
3806
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
3250
3807
|
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
3251
3808
|
protect-mcp init
|
|
3809
|
+
protect-mcp demo
|
|
3810
|
+
protect-mcp status
|
|
3811
|
+
protect-mcp digest --today
|
|
3812
|
+
protect-mcp receipts --last 10
|
|
3813
|
+
protect-mcp bundle --output audit.json
|
|
3252
3814
|
|
|
3253
3815
|
`);
|
|
3254
3816
|
}
|
|
@@ -3292,17 +3854,17 @@ function parseArgs(argv) {
|
|
|
3292
3854
|
return { policyPath, slug, enforce, verbose, childCommand };
|
|
3293
3855
|
}
|
|
3294
3856
|
async function handleInit(argv) {
|
|
3295
|
-
const { writeFileSync, existsSync:
|
|
3296
|
-
const { join } = await import("path");
|
|
3857
|
+
const { writeFileSync: writeFileSync2, existsSync: existsSync4, mkdirSync } = await import("fs");
|
|
3858
|
+
const { join: join4 } = await import("path");
|
|
3297
3859
|
let dir = process.cwd();
|
|
3298
3860
|
const dirIdx = argv.indexOf("--dir");
|
|
3299
3861
|
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
3300
3862
|
dir = argv[dirIdx + 1];
|
|
3301
3863
|
}
|
|
3302
|
-
const configPath =
|
|
3303
|
-
const keysDir =
|
|
3304
|
-
const keyPath =
|
|
3305
|
-
if (
|
|
3864
|
+
const configPath = join4(dir, "protect-mcp.json");
|
|
3865
|
+
const keysDir = join4(dir, "keys");
|
|
3866
|
+
const keyPath = join4(keysDir, "gateway.json");
|
|
3867
|
+
if (existsSync4(configPath)) {
|
|
3306
3868
|
process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
|
|
3307
3869
|
`);
|
|
3308
3870
|
process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
|
|
@@ -3313,10 +3875,10 @@ async function handleInit(argv) {
|
|
|
3313
3875
|
const artifacts = await import("@veritasacta/artifacts");
|
|
3314
3876
|
keypair = artifacts.generateKeypair();
|
|
3315
3877
|
} catch {
|
|
3316
|
-
const { randomBytes:
|
|
3878
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
3317
3879
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
3318
3880
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3319
|
-
const privateKey =
|
|
3881
|
+
const privateKey = randomBytes3(32);
|
|
3320
3882
|
const publicKey = ed255192.getPublicKey(privateKey);
|
|
3321
3883
|
keypair = {
|
|
3322
3884
|
privateKey: bytesToHex2(privateKey),
|
|
@@ -3324,19 +3886,19 @@ async function handleInit(argv) {
|
|
|
3324
3886
|
kid: "generated"
|
|
3325
3887
|
};
|
|
3326
3888
|
}
|
|
3327
|
-
if (!
|
|
3889
|
+
if (!existsSync4(keysDir)) {
|
|
3328
3890
|
mkdirSync(keysDir, { recursive: true });
|
|
3329
3891
|
}
|
|
3330
|
-
|
|
3892
|
+
writeFileSync2(keyPath, JSON.stringify({
|
|
3331
3893
|
privateKey: keypair.privateKey,
|
|
3332
3894
|
publicKey: keypair.publicKey,
|
|
3333
3895
|
kid: keypair.kid,
|
|
3334
3896
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3335
3897
|
warning: "KEEP THIS FILE SECRET. Never commit to version control."
|
|
3336
3898
|
}, null, 2) + "\n");
|
|
3337
|
-
const gitignorePath =
|
|
3338
|
-
if (!
|
|
3339
|
-
|
|
3899
|
+
const gitignorePath = join4(keysDir, ".gitignore");
|
|
3900
|
+
if (!existsSync4(gitignorePath)) {
|
|
3901
|
+
writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
|
|
3340
3902
|
}
|
|
3341
3903
|
const config = {
|
|
3342
3904
|
tools: {
|
|
@@ -3363,19 +3925,27 @@ async function handleInit(argv) {
|
|
|
3363
3925
|
},
|
|
3364
3926
|
credentials: {
|
|
3365
3927
|
_example_api: {
|
|
3366
|
-
inject: "
|
|
3367
|
-
name: "
|
|
3928
|
+
inject: "env",
|
|
3929
|
+
name: "EXAMPLE_API_KEY",
|
|
3368
3930
|
value_env: "EXAMPLE_API_KEY",
|
|
3369
3931
|
_comment: "Remove the underscore prefix and set EXAMPLE_API_KEY in your environment"
|
|
3370
3932
|
}
|
|
3371
3933
|
}
|
|
3372
3934
|
};
|
|
3373
|
-
|
|
3935
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3936
|
+
const claudeConfig = {
|
|
3937
|
+
"mcpServers": {
|
|
3938
|
+
"my-server": {
|
|
3939
|
+
"command": "npx",
|
|
3940
|
+
"args": ["protect-mcp", "--policy", configPath, "--", "node", "my-server.js"]
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3374
3944
|
process.stderr.write(`
|
|
3375
3945
|
${bold("protect-mcp initialized!")}
|
|
3376
3946
|
|
|
3377
3947
|
Created:
|
|
3378
|
-
${configPath} Config with shadow mode +
|
|
3948
|
+
${configPath} Config with shadow mode + local signing
|
|
3379
3949
|
${keyPath} Ed25519 signing keypair
|
|
3380
3950
|
|
|
3381
3951
|
${bold("Next steps:")}
|
|
@@ -3389,13 +3959,427 @@ ${bold("Your gateway public key:")}
|
|
|
3389
3959
|
${bold("Key ID (kid):")}
|
|
3390
3960
|
${keypair.kid}
|
|
3391
3961
|
|
|
3962
|
+
${bold("Claude Desktop config snippet")} (add to claude_desktop_config.json):
|
|
3963
|
+
${dim(JSON.stringify(claudeConfig, null, 2))}
|
|
3964
|
+
|
|
3965
|
+
${bold("Quick demo:")}
|
|
3966
|
+
protect-mcp demo
|
|
3967
|
+
|
|
3392
3968
|
Shadow mode is the default \u2014 all tool calls are logged and nothing is blocked.
|
|
3393
|
-
|
|
3969
|
+
Add --enforce when ready to block policy violations.
|
|
3970
|
+
`);
|
|
3971
|
+
}
|
|
3972
|
+
async function handleDemo() {
|
|
3973
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
3974
|
+
const { join: join4, dirname, resolve } = await import("path");
|
|
3975
|
+
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
3976
|
+
const cliDir = dirname(cliPath);
|
|
3977
|
+
const demoServerPath = join4(cliDir, "demo-server.js");
|
|
3978
|
+
const configPath = join4(process.cwd(), "protect-mcp.json");
|
|
3979
|
+
const hasConfig = existsSync4(configPath);
|
|
3980
|
+
if (!hasConfig) {
|
|
3981
|
+
process.stderr.write(`
|
|
3982
|
+
${bold("protect-mcp demo")}
|
|
3983
|
+
|
|
3984
|
+
Starting demo with default shadow mode (no signing).
|
|
3985
|
+
For signed receipts, run ${dim("npx protect-mcp init")} first.
|
|
3986
|
+
|
|
3987
|
+
`);
|
|
3988
|
+
} else {
|
|
3989
|
+
process.stderr.write(`
|
|
3990
|
+
${bold("protect-mcp demo")}
|
|
3991
|
+
|
|
3992
|
+
Using config from ${configPath}
|
|
3993
|
+
Starting demo server with 5 tools...
|
|
3994
|
+
|
|
3995
|
+
`);
|
|
3996
|
+
}
|
|
3997
|
+
let policy = null;
|
|
3998
|
+
let policyDigest = "none";
|
|
3999
|
+
let credentials;
|
|
4000
|
+
let signing;
|
|
4001
|
+
if (hasConfig) {
|
|
4002
|
+
try {
|
|
4003
|
+
const loaded = loadPolicy(configPath);
|
|
4004
|
+
policy = loaded.policy;
|
|
4005
|
+
policyDigest = loaded.digest;
|
|
4006
|
+
credentials = loaded.credentials;
|
|
4007
|
+
signing = loaded.signing;
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
process.stderr.write(`[PROTECT_MCP] Warning: Could not load config: ${err instanceof Error ? err.message : err}
|
|
4010
|
+
`);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
if (signing) {
|
|
4014
|
+
const warnings = await initSigning(signing);
|
|
4015
|
+
for (const w of warnings) {
|
|
4016
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
4017
|
+
`);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
if (credentials) {
|
|
4021
|
+
const warnings = validateCredentials(credentials);
|
|
4022
|
+
for (const w of warnings) {
|
|
4023
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
4024
|
+
`);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
const config = {
|
|
4028
|
+
command: process.execPath,
|
|
4029
|
+
// node
|
|
4030
|
+
args: [demoServerPath],
|
|
4031
|
+
policy,
|
|
4032
|
+
policyDigest,
|
|
4033
|
+
enforce: false,
|
|
4034
|
+
// Demo always runs in shadow mode
|
|
4035
|
+
verbose: true,
|
|
4036
|
+
signing,
|
|
4037
|
+
credentials
|
|
4038
|
+
};
|
|
4039
|
+
const gateway = new ProtectGateway(config);
|
|
4040
|
+
process.stderr.write(`${bold("Demo ready!")} The demo server is running.
|
|
4041
|
+
`);
|
|
4042
|
+
process.stderr.write(`Send JSON-RPC tool calls on stdin, or use an MCP client.
|
|
4043
|
+
|
|
4044
|
+
`);
|
|
4045
|
+
process.stderr.write(`${dim("Example (paste into stdin):")}
|
|
4046
|
+
`);
|
|
4047
|
+
process.stderr.write(`${dim('{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/etc/hosts"}}}')}
|
|
4048
|
+
|
|
4049
|
+
`);
|
|
4050
|
+
await gateway.start();
|
|
4051
|
+
}
|
|
4052
|
+
async function handleStatus2(argv) {
|
|
4053
|
+
const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
|
|
4054
|
+
const { join: join4 } = await import("path");
|
|
4055
|
+
let dir = process.cwd();
|
|
4056
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4057
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
4058
|
+
dir = argv[dirIdx + 1];
|
|
4059
|
+
}
|
|
4060
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4061
|
+
if (!existsSync4(logPath)) {
|
|
4062
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
4063
|
+
|
|
4064
|
+
`);
|
|
4065
|
+
process.stderr.write(`No log file found at ${logPath}
|
|
4066
|
+
`);
|
|
4067
|
+
process.stderr.write(`Run protect-mcp with a wrapped server first to generate logs.
|
|
4068
|
+
`);
|
|
4069
|
+
process.exit(0);
|
|
4070
|
+
}
|
|
4071
|
+
const raw = readFileSync5(logPath, "utf-8");
|
|
4072
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4073
|
+
if (lines.length === 0) {
|
|
4074
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
4075
|
+
|
|
4076
|
+
No entries in log file.
|
|
4077
|
+
`);
|
|
4078
|
+
process.exit(0);
|
|
4079
|
+
}
|
|
4080
|
+
const entries = [];
|
|
4081
|
+
for (const line of lines) {
|
|
4082
|
+
try {
|
|
4083
|
+
entries.push(JSON.parse(line));
|
|
4084
|
+
} catch {
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
if (entries.length === 0) {
|
|
4088
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
4089
|
+
|
|
4090
|
+
No valid entries in log file.
|
|
4091
|
+
`);
|
|
4092
|
+
process.exit(0);
|
|
4093
|
+
}
|
|
4094
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
4095
|
+
let allowCount = 0;
|
|
4096
|
+
let denyCount = 0;
|
|
4097
|
+
let rateLimitCount = 0;
|
|
4098
|
+
const tierCounts = /* @__PURE__ */ new Map();
|
|
4099
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
4100
|
+
for (const entry of entries) {
|
|
4101
|
+
toolCounts.set(entry.tool, (toolCounts.get(entry.tool) || 0) + 1);
|
|
4102
|
+
if (entry.decision === "allow") allowCount++;
|
|
4103
|
+
else if (entry.decision === "deny") denyCount++;
|
|
4104
|
+
if (entry.reason_code === "rate_limit_exceeded") rateLimitCount++;
|
|
4105
|
+
if (entry.tier) tierCounts.set(entry.tier, (tierCounts.get(entry.tier) || 0) + 1);
|
|
4106
|
+
reasonCounts.set(entry.reason_code, (reasonCounts.get(entry.reason_code) || 0) + 1);
|
|
4107
|
+
}
|
|
4108
|
+
const firstTs = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
4109
|
+
const lastTs = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
4110
|
+
const sortedTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
4111
|
+
process.stdout.write(`
|
|
4112
|
+
${bold("protect-mcp status")}
|
|
4113
|
+
|
|
4114
|
+
`);
|
|
4115
|
+
process.stdout.write(` Total decisions: ${bold(String(entries.length))}
|
|
4116
|
+
`);
|
|
4117
|
+
process.stdout.write(` ${green("\u2713 Allow")}: ${allowCount} ${red("\u2717 Deny")}: ${denyCount} ${yellow("\u2298 Rate-limited")}: ${rateLimitCount}
|
|
4118
|
+
|
|
4119
|
+
`);
|
|
4120
|
+
process.stdout.write(` ${bold("Time range:")}
|
|
4121
|
+
`);
|
|
4122
|
+
process.stdout.write(` First: ${firstTs.toISOString()}
|
|
4123
|
+
`);
|
|
4124
|
+
process.stdout.write(` Last: ${lastTs.toISOString()}
|
|
4125
|
+
|
|
4126
|
+
`);
|
|
4127
|
+
process.stdout.write(` ${bold("Top tools:")}
|
|
4128
|
+
`);
|
|
4129
|
+
for (const [tool, count] of sortedTools.slice(0, 10)) {
|
|
4130
|
+
const bar = "\u2588".repeat(Math.min(Math.ceil(count / entries.length * 30), 30));
|
|
4131
|
+
process.stdout.write(` ${tool.padEnd(20)} ${String(count).padStart(4)} ${dim(bar)}
|
|
4132
|
+
`);
|
|
4133
|
+
}
|
|
4134
|
+
if (tierCounts.size > 0) {
|
|
4135
|
+
process.stdout.write(`
|
|
4136
|
+
${bold("Trust tiers seen:")}
|
|
4137
|
+
`);
|
|
4138
|
+
for (const [tier, count] of tierCounts) {
|
|
4139
|
+
process.stdout.write(` ${tier.padEnd(15)} ${count}
|
|
4140
|
+
`);
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
process.stdout.write(`
|
|
4144
|
+
${bold("Decision reasons:")}
|
|
4145
|
+
`);
|
|
4146
|
+
for (const [reason, count] of [...reasonCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
4147
|
+
process.stdout.write(` ${reason.padEnd(25)} ${count}
|
|
4148
|
+
`);
|
|
4149
|
+
}
|
|
4150
|
+
const evidencePath = join4(dir, ".protect-mcp-evidence.json");
|
|
4151
|
+
if (existsSync4(evidencePath)) {
|
|
4152
|
+
try {
|
|
4153
|
+
const evidenceRaw = readFileSync5(evidencePath, "utf-8");
|
|
4154
|
+
const evidence = JSON.parse(evidenceRaw);
|
|
4155
|
+
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
4156
|
+
process.stdout.write(`
|
|
4157
|
+
${bold("Evidence store:")} ${agentCount} agent(s) tracked
|
|
4158
|
+
`);
|
|
4159
|
+
} catch {
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4163
|
+
if (existsSync4(keyPath)) {
|
|
4164
|
+
try {
|
|
4165
|
+
const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
|
|
4166
|
+
if (keyData.publicKey) {
|
|
4167
|
+
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4168
|
+
process.stdout.write(`
|
|
4169
|
+
${bold("\u{1F6E1}\uFE0F Passport identity:")}
|
|
4170
|
+
`);
|
|
4171
|
+
process.stdout.write(` Public key: ${fingerprint}
|
|
4172
|
+
`);
|
|
4173
|
+
if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
|
|
4174
|
+
`);
|
|
4175
|
+
process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
|
|
4176
|
+
`);
|
|
4177
|
+
process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
|
|
4178
|
+
`);
|
|
4179
|
+
}
|
|
4180
|
+
} catch {
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
process.stdout.write(`
|
|
4184
|
+
Log file: ${dim(logPath)}
|
|
4185
|
+
|
|
3394
4186
|
`);
|
|
3395
4187
|
}
|
|
3396
4188
|
function bold(s) {
|
|
3397
4189
|
return process.env.NO_COLOR ? s : `\x1B[1m${s}\x1B[0m`;
|
|
3398
4190
|
}
|
|
4191
|
+
function dim(s) {
|
|
4192
|
+
return process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`;
|
|
4193
|
+
}
|
|
4194
|
+
function green(s) {
|
|
4195
|
+
return process.env.NO_COLOR ? s : `\x1B[32m${s}\x1B[0m`;
|
|
4196
|
+
}
|
|
4197
|
+
function red(s) {
|
|
4198
|
+
return process.env.NO_COLOR ? s : `\x1B[31m${s}\x1B[0m`;
|
|
4199
|
+
}
|
|
4200
|
+
function yellow(s) {
|
|
4201
|
+
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
4202
|
+
}
|
|
4203
|
+
async function handleDigest(argv) {
|
|
4204
|
+
const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
|
|
4205
|
+
const { join: join4 } = await import("path");
|
|
4206
|
+
let dir = process.cwd();
|
|
4207
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4208
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4209
|
+
const today = argv.includes("--today");
|
|
4210
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4211
|
+
if (!existsSync4(logPath)) {
|
|
4212
|
+
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4213
|
+
|
|
4214
|
+
No log file found. Run protect-mcp first.
|
|
4215
|
+
`);
|
|
4216
|
+
process.exit(0);
|
|
4217
|
+
}
|
|
4218
|
+
const raw = readFileSync5(logPath, "utf-8");
|
|
4219
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4220
|
+
let entries = [];
|
|
4221
|
+
for (const line of lines) {
|
|
4222
|
+
try {
|
|
4223
|
+
entries.push(JSON.parse(line));
|
|
4224
|
+
} catch {
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
if (today) {
|
|
4228
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
4229
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
4230
|
+
entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
|
|
4231
|
+
}
|
|
4232
|
+
if (entries.length === 0) {
|
|
4233
|
+
process.stdout.write(`
|
|
4234
|
+
${bold("\u{1F6E1}\uFE0F Agent Digest")}
|
|
4235
|
+
|
|
4236
|
+
No activity${today ? " today" : ""}.
|
|
4237
|
+
|
|
4238
|
+
`);
|
|
4239
|
+
process.exit(0);
|
|
4240
|
+
}
|
|
4241
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4242
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4243
|
+
const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
|
|
4244
|
+
const toolUsage = /* @__PURE__ */ new Map();
|
|
4245
|
+
for (const e of entries) {
|
|
4246
|
+
toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
|
|
4247
|
+
}
|
|
4248
|
+
const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
|
|
4249
|
+
const currentTier = entries[entries.length - 1]?.tier || "unknown";
|
|
4250
|
+
const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
4251
|
+
const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
4252
|
+
const durationMs = lastTime.getTime() - firstTime.getTime();
|
|
4253
|
+
const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
|
|
4254
|
+
process.stdout.write(`
|
|
4255
|
+
${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
4256
|
+
|
|
4257
|
+
`);
|
|
4258
|
+
process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
|
|
4259
|
+
process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
|
|
4260
|
+
process.stdout.write(`${red("\u2717 " + denied)} blocked`);
|
|
4261
|
+
if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
|
|
4262
|
+
process.stdout.write(`
|
|
4263
|
+
`);
|
|
4264
|
+
process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
|
|
4265
|
+
|
|
4266
|
+
`);
|
|
4267
|
+
process.stdout.write(` ${bold("Tools used:")}
|
|
4268
|
+
`);
|
|
4269
|
+
for (const [tool, count] of sortedTools.slice(0, 8)) {
|
|
4270
|
+
process.stdout.write(` ${tool.padEnd(22)} ${count}x
|
|
4271
|
+
`);
|
|
4272
|
+
}
|
|
4273
|
+
if (denied > 0) {
|
|
4274
|
+
const deniedTools = entries.filter((e) => e.decision === "deny");
|
|
4275
|
+
const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
|
|
4276
|
+
process.stdout.write(`
|
|
4277
|
+
${bold(red("Blocked tools:"))}
|
|
4278
|
+
`);
|
|
4279
|
+
for (const tool of deniedToolNames) {
|
|
4280
|
+
const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
|
|
4281
|
+
process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
|
|
4282
|
+
`);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
process.stdout.write(`
|
|
4286
|
+
${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
|
|
4287
|
+
`);
|
|
4288
|
+
process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
|
|
4289
|
+
`);
|
|
4290
|
+
process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
|
|
4291
|
+
|
|
4292
|
+
`);
|
|
4293
|
+
}
|
|
4294
|
+
async function handleReceipts2(argv) {
|
|
4295
|
+
const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
|
|
4296
|
+
const { join: join4 } = await import("path");
|
|
4297
|
+
let dir = process.cwd();
|
|
4298
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4299
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4300
|
+
const lastIdx = argv.indexOf("--last");
|
|
4301
|
+
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
4302
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4303
|
+
if (!existsSync4(receiptsPath)) {
|
|
4304
|
+
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
4305
|
+
|
|
4306
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4307
|
+
`);
|
|
4308
|
+
process.exit(0);
|
|
4309
|
+
}
|
|
4310
|
+
const raw = readFileSync5(receiptsPath, "utf-8");
|
|
4311
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4312
|
+
const recent = lines.slice(-count);
|
|
4313
|
+
process.stdout.write(`
|
|
4314
|
+
${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
4315
|
+
|
|
4316
|
+
`);
|
|
4317
|
+
for (const line of recent) {
|
|
4318
|
+
try {
|
|
4319
|
+
const entry = JSON.parse(line);
|
|
4320
|
+
const payload = entry.payload || {};
|
|
4321
|
+
const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
|
|
4322
|
+
const decision = payload.decision || "unknown";
|
|
4323
|
+
const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
|
|
4324
|
+
process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
|
|
4325
|
+
`);
|
|
4326
|
+
} catch {
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
process.stdout.write(`
|
|
4330
|
+
`);
|
|
4331
|
+
}
|
|
4332
|
+
async function handleBundle(argv) {
|
|
4333
|
+
const { readFileSync: readFileSync5, writeFileSync: writeFileSync2, existsSync: existsSync4 } = await import("fs");
|
|
4334
|
+
const { join: join4 } = await import("path");
|
|
4335
|
+
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
4336
|
+
let dir = process.cwd();
|
|
4337
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4338
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4339
|
+
const outputIdx = argv.indexOf("--output");
|
|
4340
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
|
|
4341
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4342
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4343
|
+
if (!existsSync4(receiptsPath)) {
|
|
4344
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4345
|
+
|
|
4346
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4347
|
+
`);
|
|
4348
|
+
process.exit(0);
|
|
4349
|
+
}
|
|
4350
|
+
if (!existsSync4(keyPath)) {
|
|
4351
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4352
|
+
|
|
4353
|
+
No key file found at ${keyPath}
|
|
4354
|
+
`);
|
|
4355
|
+
process.exit(1);
|
|
4356
|
+
}
|
|
4357
|
+
const receipts = readFileSync5(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4358
|
+
const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
|
|
4359
|
+
const bundle = createAuditBundle2({
|
|
4360
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
4361
|
+
receipts,
|
|
4362
|
+
signingKeys: [{
|
|
4363
|
+
kty: "OKP",
|
|
4364
|
+
crv: "Ed25519",
|
|
4365
|
+
kid: keyData.kid || "unknown",
|
|
4366
|
+
x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
|
|
4367
|
+
use: "sig"
|
|
4368
|
+
}]
|
|
4369
|
+
});
|
|
4370
|
+
writeFileSync2(outputPath, JSON.stringify(bundle, null, 2) + "\n");
|
|
4371
|
+
process.stdout.write(`
|
|
4372
|
+
${bold("protect-mcp bundle")}
|
|
4373
|
+
|
|
4374
|
+
`);
|
|
4375
|
+
process.stdout.write(` Receipts: ${receipts.length}
|
|
4376
|
+
`);
|
|
4377
|
+
process.stdout.write(` Output: ${outputPath}
|
|
4378
|
+
`);
|
|
4379
|
+
process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
|
|
4380
|
+
|
|
4381
|
+
`);
|
|
4382
|
+
}
|
|
3399
4383
|
async function main() {
|
|
3400
4384
|
const args = process.argv.slice(2);
|
|
3401
4385
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -3406,6 +4390,26 @@ async function main() {
|
|
|
3406
4390
|
await handleInit(args.slice(1));
|
|
3407
4391
|
process.exit(0);
|
|
3408
4392
|
}
|
|
4393
|
+
if (args[0] === "demo") {
|
|
4394
|
+
await handleDemo();
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
if (args[0] === "status") {
|
|
4398
|
+
await handleStatus2(args.slice(1));
|
|
4399
|
+
process.exit(0);
|
|
4400
|
+
}
|
|
4401
|
+
if (args[0] === "digest") {
|
|
4402
|
+
await handleDigest(args.slice(1));
|
|
4403
|
+
process.exit(0);
|
|
4404
|
+
}
|
|
4405
|
+
if (args[0] === "receipts") {
|
|
4406
|
+
await handleReceipts2(args.slice(1));
|
|
4407
|
+
process.exit(0);
|
|
4408
|
+
}
|
|
4409
|
+
if (args[0] === "bundle") {
|
|
4410
|
+
await handleBundle(args.slice(1));
|
|
4411
|
+
process.exit(0);
|
|
4412
|
+
}
|
|
3409
4413
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
3410
4414
|
let policy = null;
|
|
3411
4415
|
let policyDigest = "none";
|