protect-mcp 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-IAJJA5IW.mjs +827 -0
- package/dist/{chunk-VF3OCG4D.mjs → chunk-IUFFDQYZ.mjs} +15 -583
- package/dist/chunk-UEHLYOJY.mjs +321 -0
- package/dist/chunk-V52W3XIN.mjs +582 -0
- package/dist/{chunk-VIA2B65K.mjs → chunk-YKM6W6T7.mjs} +4 -2
- package/dist/cli.js +1452 -100
- package/dist/cli.mjs +205 -10
- package/dist/hook-patterns.d.mts +41 -0
- package/dist/hook-patterns.d.ts +41 -0
- package/dist/hook-patterns.js +348 -0
- package/dist/hook-patterns.mjs +13 -0
- package/dist/hook-server.d.mts +38 -0
- package/dist/hook-server.d.ts +38 -0
- package/dist/hook-server.js +1211 -0
- package/dist/hook-server.mjs +8 -0
- package/dist/{http-transport-XCHIKTYG.mjs → http-transport-GXIXLVJQ.mjs} +2 -1
- package/dist/index.d.mts +194 -1
- package/dist/index.d.ts +194 -1
- package/dist/index.js +1181 -22
- package/dist/index.mjs +35 -19
- package/package.json +2 -2
|
@@ -1,81 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
tools: parsed.tools,
|
|
12
|
-
default_tier: parsed.default_tier || "unknown",
|
|
13
|
-
policy_engine: parsed.policy_engine || "built-in",
|
|
14
|
-
...parsed.external ? { external: parsed.external } : {}
|
|
15
|
-
};
|
|
16
|
-
const digest = computePolicyDigest(policy);
|
|
17
|
-
return {
|
|
18
|
-
policy,
|
|
19
|
-
digest,
|
|
20
|
-
credentials: parsed.credentials,
|
|
21
|
-
signing: parsed.signing
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
function computePolicyDigest(policy) {
|
|
25
|
-
const canonical = JSON.stringify(sortKeysDeep(policy));
|
|
26
|
-
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
27
|
-
}
|
|
28
|
-
function sortKeysDeep(obj) {
|
|
29
|
-
if (obj === null || typeof obj !== "object") return obj;
|
|
30
|
-
if (Array.isArray(obj)) return obj.map(sortKeysDeep);
|
|
31
|
-
const sorted = {};
|
|
32
|
-
for (const key of Object.keys(obj).sort()) {
|
|
33
|
-
sorted[key] = sortKeysDeep(obj[key]);
|
|
34
|
-
}
|
|
35
|
-
return sorted;
|
|
36
|
-
}
|
|
37
|
-
function getToolPolicy(toolName, policy) {
|
|
38
|
-
if (!policy) {
|
|
39
|
-
return { require: "any" };
|
|
40
|
-
}
|
|
41
|
-
if (policy.tools[toolName]) {
|
|
42
|
-
return policy.tools[toolName];
|
|
43
|
-
}
|
|
44
|
-
if (policy.tools["*"]) {
|
|
45
|
-
return policy.tools["*"];
|
|
46
|
-
}
|
|
47
|
-
return { require: "any" };
|
|
48
|
-
}
|
|
49
|
-
function parseRateLimit(spec) {
|
|
50
|
-
const match = spec.match(/^(\d+)\/(second|minute|hour|day)$/);
|
|
51
|
-
if (!match) {
|
|
52
|
-
throw new Error(`Invalid rate limit format: "${spec}". Expected "N/unit" (e.g. "5/hour")`);
|
|
53
|
-
}
|
|
54
|
-
const count = parseInt(match[1], 10);
|
|
55
|
-
const unit = match[2];
|
|
56
|
-
const windowMs = {
|
|
57
|
-
second: 1e3,
|
|
58
|
-
minute: 6e4,
|
|
59
|
-
hour: 36e5,
|
|
60
|
-
day: 864e5
|
|
61
|
-
};
|
|
62
|
-
return { count, windowMs: windowMs[unit] };
|
|
63
|
-
}
|
|
64
|
-
function checkRateLimit(key, limit, store) {
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
const windowStart = now - limit.windowMs;
|
|
67
|
-
const timestamps = (store.get(key) || []).filter((t) => t > windowStart);
|
|
68
|
-
if (timestamps.length >= limit.count) {
|
|
69
|
-
store.set(key, timestamps);
|
|
70
|
-
return { allowed: false, remaining: 0 };
|
|
71
|
-
}
|
|
72
|
-
timestamps.push(now);
|
|
73
|
-
store.set(key, timestamps);
|
|
74
|
-
return { allowed: true, remaining: limit.count - timestamps.length };
|
|
75
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ReceiptBuffer,
|
|
3
|
+
checkRateLimit,
|
|
4
|
+
evaluateCedar,
|
|
5
|
+
getToolPolicy,
|
|
6
|
+
isSigningEnabled,
|
|
7
|
+
parseRateLimit,
|
|
8
|
+
signDecision,
|
|
9
|
+
startStatusServer
|
|
10
|
+
} from "./chunk-V52W3XIN.mjs";
|
|
76
11
|
|
|
77
12
|
// src/evidence-store.ts
|
|
78
|
-
import { readFileSync
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
79
14
|
import { join } from "path";
|
|
80
15
|
var DEFAULT_THRESHOLDS = {
|
|
81
16
|
min_receipts: 10,
|
|
@@ -162,7 +97,7 @@ var EvidenceStore = class {
|
|
|
162
97
|
load() {
|
|
163
98
|
if (!existsSync(this.filePath)) return;
|
|
164
99
|
try {
|
|
165
|
-
const raw =
|
|
100
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
166
101
|
const parsed = JSON.parse(raw);
|
|
167
102
|
if (parsed.agents && typeof parsed.agents === "object") {
|
|
168
103
|
for (const [id, record] of Object.entries(parsed.agents)) {
|
|
@@ -307,103 +242,6 @@ function validateCredentials(credentials) {
|
|
|
307
242
|
return warnings;
|
|
308
243
|
}
|
|
309
244
|
|
|
310
|
-
// src/signing.ts
|
|
311
|
-
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
312
|
-
var signerState = null;
|
|
313
|
-
var artifactsModule = null;
|
|
314
|
-
async function initSigning(config) {
|
|
315
|
-
const warnings = [];
|
|
316
|
-
if (!config || config.enabled === false) {
|
|
317
|
-
return warnings;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
const moduleName = "@veritasacta/artifacts";
|
|
321
|
-
artifactsModule = await import(
|
|
322
|
-
/* @vite-ignore */
|
|
323
|
-
moduleName
|
|
324
|
-
);
|
|
325
|
-
} catch {
|
|
326
|
-
warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
|
|
327
|
-
return warnings;
|
|
328
|
-
}
|
|
329
|
-
if (config.key_path) {
|
|
330
|
-
if (!existsSync2(config.key_path)) {
|
|
331
|
-
warnings.push(`signing: key file not found at ${config.key_path} \u2014 run "protect-mcp init" to generate`);
|
|
332
|
-
return warnings;
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
const keyData = JSON.parse(readFileSync3(config.key_path, "utf-8"));
|
|
336
|
-
if (!keyData.privateKey || !keyData.publicKey) {
|
|
337
|
-
warnings.push("signing: key file missing privateKey or publicKey fields");
|
|
338
|
-
return warnings;
|
|
339
|
-
}
|
|
340
|
-
signerState = {
|
|
341
|
-
privateKey: keyData.privateKey,
|
|
342
|
-
publicKey: keyData.publicKey,
|
|
343
|
-
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
344
|
-
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
345
|
-
};
|
|
346
|
-
} catch (err) {
|
|
347
|
-
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return warnings;
|
|
351
|
-
}
|
|
352
|
-
function signDecision(entry) {
|
|
353
|
-
if (!signerState || !artifactsModule) {
|
|
354
|
-
return { signed: null, artifact_type: "none" };
|
|
355
|
-
}
|
|
356
|
-
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
357
|
-
try {
|
|
358
|
-
const payload = {
|
|
359
|
-
tool: entry.tool,
|
|
360
|
-
decision: entry.decision,
|
|
361
|
-
reason_code: entry.reason_code,
|
|
362
|
-
policy_digest: entry.policy_digest,
|
|
363
|
-
scope: entry.request_id,
|
|
364
|
-
// request scope
|
|
365
|
-
mode: entry.mode,
|
|
366
|
-
request_id: entry.request_id
|
|
367
|
-
};
|
|
368
|
-
if (entry.tier) payload.tier = entry.tier;
|
|
369
|
-
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
370
|
-
if (entry.rate_limit_remaining !== void 0) {
|
|
371
|
-
payload.rate_limit_remaining = entry.rate_limit_remaining;
|
|
372
|
-
}
|
|
373
|
-
if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
|
|
374
|
-
const result = artifactsModule.createSignedArtifact(
|
|
375
|
-
artifactType,
|
|
376
|
-
payload,
|
|
377
|
-
signerState.privateKey,
|
|
378
|
-
{
|
|
379
|
-
kid: signerState.kid,
|
|
380
|
-
issuer: signerState.issuer
|
|
381
|
-
}
|
|
382
|
-
);
|
|
383
|
-
return {
|
|
384
|
-
signed: JSON.stringify(result.artifact),
|
|
385
|
-
artifact_type: artifactType
|
|
386
|
-
};
|
|
387
|
-
} catch (err) {
|
|
388
|
-
return {
|
|
389
|
-
signed: null,
|
|
390
|
-
artifact_type: artifactType,
|
|
391
|
-
warning: `signing failed: ${err instanceof Error ? err.message : "unknown error"}`
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function getSignerInfo() {
|
|
396
|
-
if (!signerState) return null;
|
|
397
|
-
return {
|
|
398
|
-
publicKey: signerState.publicKey,
|
|
399
|
-
kid: signerState.kid,
|
|
400
|
-
issuer: signerState.issuer
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
function isSigningEnabled() {
|
|
404
|
-
return signerState !== null && artifactsModule !== null;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
245
|
// src/external-pdp.ts
|
|
408
246
|
async function queryExternalPDP(context, config) {
|
|
409
247
|
const timeout = config.timeout_ms || 500;
|
|
@@ -564,175 +402,6 @@ function buildDecisionContext(toolName, tier, opts) {
|
|
|
564
402
|
};
|
|
565
403
|
}
|
|
566
404
|
|
|
567
|
-
// src/cedar-evaluator.ts
|
|
568
|
-
import { createHash as createHash2 } from "crypto";
|
|
569
|
-
import { readFileSync as readFileSync4, readdirSync, existsSync as existsSync3 } from "fs";
|
|
570
|
-
import { join as join2, extname } from "path";
|
|
571
|
-
var cedarWasm = null;
|
|
572
|
-
var loadAttempted = false;
|
|
573
|
-
async function ensureCedarWasm() {
|
|
574
|
-
if (cedarWasm) return true;
|
|
575
|
-
if (loadAttempted) return false;
|
|
576
|
-
loadAttempted = true;
|
|
577
|
-
try {
|
|
578
|
-
const moduleName = "@cedar-policy/cedar-wasm";
|
|
579
|
-
cedarWasm = await import(
|
|
580
|
-
/* @vite-ignore */
|
|
581
|
-
moduleName
|
|
582
|
-
);
|
|
583
|
-
return true;
|
|
584
|
-
} catch {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
function loadCedarPolicies(dirPath) {
|
|
589
|
-
if (!existsSync3(dirPath)) {
|
|
590
|
-
throw new Error(`Cedar policy directory not found: ${dirPath}`);
|
|
591
|
-
}
|
|
592
|
-
const entries = readdirSync(dirPath).filter((f) => extname(f) === ".cedar").sort();
|
|
593
|
-
if (entries.length === 0) {
|
|
594
|
-
throw new Error(`No .cedar files found in: ${dirPath}`);
|
|
595
|
-
}
|
|
596
|
-
const sources = [];
|
|
597
|
-
for (const file of entries) {
|
|
598
|
-
const content = readFileSync4(join2(dirPath, file), "utf-8");
|
|
599
|
-
sources.push(content);
|
|
600
|
-
}
|
|
601
|
-
const concatenated = sources.join("\n\n");
|
|
602
|
-
const digest = createHash2("sha256").update(concatenated).digest("hex").slice(0, 16);
|
|
603
|
-
return {
|
|
604
|
-
source: concatenated,
|
|
605
|
-
digest,
|
|
606
|
-
fileCount: entries.length,
|
|
607
|
-
files: entries
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
function buildEntities(req) {
|
|
611
|
-
const agentId = req.agentId || req.tier;
|
|
612
|
-
return [
|
|
613
|
-
{
|
|
614
|
-
uid: { type: "Agent", id: agentId },
|
|
615
|
-
attrs: {
|
|
616
|
-
tier: req.tier,
|
|
617
|
-
...req.agentId ? { agent_id: req.agentId } : {}
|
|
618
|
-
},
|
|
619
|
-
parents: []
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
uid: { type: "Tool", id: req.tool },
|
|
623
|
-
attrs: {},
|
|
624
|
-
parents: []
|
|
625
|
-
}
|
|
626
|
-
];
|
|
627
|
-
}
|
|
628
|
-
async function evaluateCedar(policySet, req) {
|
|
629
|
-
const available = await ensureCedarWasm();
|
|
630
|
-
if (!available) {
|
|
631
|
-
return {
|
|
632
|
-
allowed: true,
|
|
633
|
-
reason: "cedar_wasm_not_available",
|
|
634
|
-
metadata: { fallback: true }
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
try {
|
|
638
|
-
const agentId = req.agentId || req.tier;
|
|
639
|
-
const authRequest = {
|
|
640
|
-
principal: { type: "Agent", id: agentId },
|
|
641
|
-
action: { type: "Action", id: "MCP::Tool::call" },
|
|
642
|
-
resource: { type: "Tool", id: req.tool },
|
|
643
|
-
context: {
|
|
644
|
-
tier: req.tier,
|
|
645
|
-
...req.context || {}
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
const entities = buildEntities(req);
|
|
649
|
-
let result;
|
|
650
|
-
if (typeof cedarWasm.isAuthorized === "function") {
|
|
651
|
-
result = cedarWasm.isAuthorized({
|
|
652
|
-
policies: policySet.source,
|
|
653
|
-
entities,
|
|
654
|
-
principal: authRequest.principal,
|
|
655
|
-
action: authRequest.action,
|
|
656
|
-
resource: authRequest.resource,
|
|
657
|
-
context: authRequest.context,
|
|
658
|
-
schema: null
|
|
659
|
-
// No schema enforcement — Cedar still evaluates correctly
|
|
660
|
-
});
|
|
661
|
-
} else if (typeof cedarWasm.checkAuthorization === "function") {
|
|
662
|
-
result = cedarWasm.checkAuthorization(
|
|
663
|
-
policySet.source,
|
|
664
|
-
JSON.stringify(entities),
|
|
665
|
-
JSON.stringify(authRequest)
|
|
666
|
-
);
|
|
667
|
-
} else {
|
|
668
|
-
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
669
|
-
if (typeof cedarEngine.isAuthorized === "function") {
|
|
670
|
-
result = cedarEngine.isAuthorized({
|
|
671
|
-
policies: policySet.source,
|
|
672
|
-
entities,
|
|
673
|
-
principal: authRequest.principal,
|
|
674
|
-
action: authRequest.action,
|
|
675
|
-
resource: authRequest.resource,
|
|
676
|
-
context: authRequest.context,
|
|
677
|
-
schema: null
|
|
678
|
-
});
|
|
679
|
-
} else {
|
|
680
|
-
return {
|
|
681
|
-
allowed: true,
|
|
682
|
-
reason: "cedar_wasm_api_unsupported",
|
|
683
|
-
metadata: { fallback: true, exports: Object.keys(cedarWasm) }
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
const decision = parseWasmResult(result);
|
|
688
|
-
return {
|
|
689
|
-
allowed: decision.allowed,
|
|
690
|
-
reason: decision.allowed ? void 0 : `cedar_deny${decision.diagnostics ? ": " + decision.diagnostics : ""}`,
|
|
691
|
-
metadata: {
|
|
692
|
-
policy_digest: policySet.digest,
|
|
693
|
-
...decision.matchedPolicies ? { matched_policies: decision.matchedPolicies } : {}
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
} catch (err) {
|
|
697
|
-
return {
|
|
698
|
-
allowed: true,
|
|
699
|
-
reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
700
|
-
metadata: { fallback: true, error: true }
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
function parseWasmResult(result) {
|
|
705
|
-
if (!result) {
|
|
706
|
-
return { allowed: true, diagnostics: "null result from Cedar WASM" };
|
|
707
|
-
}
|
|
708
|
-
if (result.type === "allow" || result.type === "Allow") {
|
|
709
|
-
return { allowed: true };
|
|
710
|
-
}
|
|
711
|
-
if (result.type === "deny" || result.type === "Deny") {
|
|
712
|
-
return {
|
|
713
|
-
allowed: false,
|
|
714
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
|
|
715
|
-
matchedPolicies: result.diagnostics?.reasons
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
if (result.decision === "Allow") {
|
|
719
|
-
return { allowed: true };
|
|
720
|
-
}
|
|
721
|
-
if (result.decision === "Deny") {
|
|
722
|
-
return {
|
|
723
|
-
allowed: false,
|
|
724
|
-
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
if (typeof result === "boolean") {
|
|
728
|
-
return { allowed: result };
|
|
729
|
-
}
|
|
730
|
-
return { allowed: true, diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
731
|
-
}
|
|
732
|
-
async function isCedarAvailable() {
|
|
733
|
-
return ensureCedarWasm();
|
|
734
|
-
}
|
|
735
|
-
|
|
736
405
|
// src/notifications.ts
|
|
737
406
|
async function sendApprovalNotification(config, notification) {
|
|
738
407
|
const promises = [];
|
|
@@ -918,235 +587,8 @@ import { spawn } from "child_process";
|
|
|
918
587
|
import { randomUUID, randomBytes } from "crypto";
|
|
919
588
|
import { createInterface } from "readline";
|
|
920
589
|
import { appendFileSync } from "fs";
|
|
921
|
-
import { join as
|
|
922
|
-
|
|
923
|
-
// src/http-server.ts
|
|
924
|
-
import { createServer } from "http";
|
|
925
|
-
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
926
|
-
import { join as join3 } from "path";
|
|
590
|
+
import { join as join2 } from "path";
|
|
927
591
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
928
|
-
var MAX_RECEIPTS = 100;
|
|
929
|
-
var ReceiptBuffer = class {
|
|
930
|
-
receipts = [];
|
|
931
|
-
add(requestId, receipt) {
|
|
932
|
-
this.receipts.push({
|
|
933
|
-
request_id: requestId,
|
|
934
|
-
receipt,
|
|
935
|
-
timestamp: Date.now()
|
|
936
|
-
});
|
|
937
|
-
if (this.receipts.length > MAX_RECEIPTS) {
|
|
938
|
-
this.receipts = this.receipts.slice(-MAX_RECEIPTS);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
getAll() {
|
|
942
|
-
return [...this.receipts].reverse();
|
|
943
|
-
}
|
|
944
|
-
getById(requestId) {
|
|
945
|
-
return this.receipts.find((r) => r.request_id === requestId);
|
|
946
|
-
}
|
|
947
|
-
count() {
|
|
948
|
-
return this.receipts.length;
|
|
949
|
-
}
|
|
950
|
-
getLatest() {
|
|
951
|
-
return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
|
|
952
|
-
}
|
|
953
|
-
};
|
|
954
|
-
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
955
|
-
const startTime = Date.now();
|
|
956
|
-
const logDir = process.cwd();
|
|
957
|
-
const server = createServer((req, res) => {
|
|
958
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
959
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
960
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
961
|
-
res.setHeader("Content-Type", "application/json");
|
|
962
|
-
if (req.method === "OPTIONS") {
|
|
963
|
-
res.writeHead(204);
|
|
964
|
-
res.end();
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
const url = new URL(req.url || "/", `http://localhost:${config.port}`);
|
|
968
|
-
const path = url.pathname;
|
|
969
|
-
try {
|
|
970
|
-
if (path === "/health") {
|
|
971
|
-
handleHealth(res, startTime, config);
|
|
972
|
-
} else if (path === "/status") {
|
|
973
|
-
handleStatus(res, logDir);
|
|
974
|
-
} else if (path === "/receipts") {
|
|
975
|
-
handleReceipts(res, receiptBuffer, url);
|
|
976
|
-
} else if (path === "/receipts/latest") {
|
|
977
|
-
handleReceiptLatest(res, receiptBuffer);
|
|
978
|
-
} else if (path.startsWith("/receipts/")) {
|
|
979
|
-
const id = path.slice("/receipts/".length);
|
|
980
|
-
handleReceiptById(res, receiptBuffer, id);
|
|
981
|
-
} else if (path === "/approve" && req.method === "POST") {
|
|
982
|
-
handleApprove(req, res, approvalStore, approvalNonce);
|
|
983
|
-
} else if (path === "/approvals" && req.method === "GET") {
|
|
984
|
-
handleListApprovals(res, approvalStore);
|
|
985
|
-
} else {
|
|
986
|
-
res.writeHead(404);
|
|
987
|
-
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
|
|
988
|
-
}
|
|
989
|
-
} catch (err) {
|
|
990
|
-
res.writeHead(500);
|
|
991
|
-
res.end(JSON.stringify({ error: "internal_error" }));
|
|
992
|
-
}
|
|
993
|
-
});
|
|
994
|
-
server.on("error", (err) => {
|
|
995
|
-
if (config.verbose) {
|
|
996
|
-
process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
|
|
997
|
-
`);
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
server.listen(config.port, "127.0.0.1", () => {
|
|
1001
|
-
if (config.verbose) {
|
|
1002
|
-
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
1003
|
-
`);
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
server.unref();
|
|
1007
|
-
return server;
|
|
1008
|
-
}
|
|
1009
|
-
function handleHealth(res, startTime, config) {
|
|
1010
|
-
res.writeHead(200);
|
|
1011
|
-
res.end(JSON.stringify({
|
|
1012
|
-
status: "ok",
|
|
1013
|
-
uptime_ms: Date.now() - startTime,
|
|
1014
|
-
mode: config.mode,
|
|
1015
|
-
version: "0.3.1"
|
|
1016
|
-
}));
|
|
1017
|
-
}
|
|
1018
|
-
function handleStatus(res, logDir) {
|
|
1019
|
-
const logPath = join3(logDir, LOG_FILE);
|
|
1020
|
-
if (!existsSync4(logPath)) {
|
|
1021
|
-
res.writeHead(200);
|
|
1022
|
-
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const raw = readFileSync5(logPath, "utf-8");
|
|
1026
|
-
const lines = raw.trim().split("\n").filter(Boolean);
|
|
1027
|
-
const entries = [];
|
|
1028
|
-
for (const line of lines) {
|
|
1029
|
-
try {
|
|
1030
|
-
entries.push(JSON.parse(line));
|
|
1031
|
-
} catch {
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
const toolCounts = {};
|
|
1035
|
-
let allowCount = 0, denyCount = 0;
|
|
1036
|
-
const tierCounts = {};
|
|
1037
|
-
for (const e of entries) {
|
|
1038
|
-
toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
1039
|
-
if (e.decision === "allow") allowCount++;
|
|
1040
|
-
else denyCount++;
|
|
1041
|
-
if (e.tier) tierCounts[e.tier] = (tierCounts[e.tier] || 0) + 1;
|
|
1042
|
-
}
|
|
1043
|
-
res.writeHead(200);
|
|
1044
|
-
res.end(JSON.stringify({
|
|
1045
|
-
entries: entries.length,
|
|
1046
|
-
allow: allowCount,
|
|
1047
|
-
deny: denyCount,
|
|
1048
|
-
tools: toolCounts,
|
|
1049
|
-
tiers: tierCounts,
|
|
1050
|
-
first_timestamp: entries.length > 0 ? entries[0].timestamp : null,
|
|
1051
|
-
last_timestamp: entries.length > 0 ? entries[entries.length - 1].timestamp : null
|
|
1052
|
-
}));
|
|
1053
|
-
}
|
|
1054
|
-
function handleReceipts(res, buffer, url) {
|
|
1055
|
-
const limit = parseInt(url.searchParams.get("limit") || "20", 10);
|
|
1056
|
-
const receipts = buffer.getAll().slice(0, Math.min(limit, MAX_RECEIPTS));
|
|
1057
|
-
res.writeHead(200);
|
|
1058
|
-
res.end(JSON.stringify({
|
|
1059
|
-
count: receipts.length,
|
|
1060
|
-
total: buffer.count(),
|
|
1061
|
-
receipts
|
|
1062
|
-
}));
|
|
1063
|
-
}
|
|
1064
|
-
function handleReceiptLatest(res, buffer) {
|
|
1065
|
-
const latest = buffer.getLatest();
|
|
1066
|
-
if (!latest) {
|
|
1067
|
-
res.writeHead(404);
|
|
1068
|
-
res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
res.writeHead(200);
|
|
1072
|
-
res.end(JSON.stringify(latest));
|
|
1073
|
-
}
|
|
1074
|
-
function handleReceiptById(res, buffer, id) {
|
|
1075
|
-
const receipt = buffer.getById(id);
|
|
1076
|
-
if (!receipt) {
|
|
1077
|
-
res.writeHead(404);
|
|
1078
|
-
res.end(JSON.stringify({ error: "receipt_not_found", request_id: id }));
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
res.writeHead(200);
|
|
1082
|
-
res.end(JSON.stringify(receipt));
|
|
1083
|
-
}
|
|
1084
|
-
function handleApprove(req, res, approvalStore, expectedNonce) {
|
|
1085
|
-
if (!approvalStore) {
|
|
1086
|
-
res.writeHead(503);
|
|
1087
|
-
res.end(JSON.stringify({ error: "approval_store_not_available" }));
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
let body = "";
|
|
1091
|
-
req.on("data", (chunk) => {
|
|
1092
|
-
body += chunk.toString();
|
|
1093
|
-
});
|
|
1094
|
-
req.on("end", () => {
|
|
1095
|
-
try {
|
|
1096
|
-
const { request_id, tool, mode, nonce } = JSON.parse(body);
|
|
1097
|
-
if (expectedNonce && nonce !== expectedNonce) {
|
|
1098
|
-
res.writeHead(403);
|
|
1099
|
-
res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
if (!tool || typeof tool !== "string") {
|
|
1103
|
-
res.writeHead(400);
|
|
1104
|
-
res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
const grantMode = mode === "always" ? "always" : "once";
|
|
1108
|
-
const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
|
|
1109
|
-
const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
|
|
1110
|
-
if (grantMode === "always") {
|
|
1111
|
-
approvalStore.set(`always:${tool}`, grantEntry);
|
|
1112
|
-
} else if (request_id) {
|
|
1113
|
-
approvalStore.set(request_id, grantEntry);
|
|
1114
|
-
} else {
|
|
1115
|
-
approvalStore.set(tool, grantEntry);
|
|
1116
|
-
}
|
|
1117
|
-
res.writeHead(200);
|
|
1118
|
-
res.end(JSON.stringify({
|
|
1119
|
-
approved: true,
|
|
1120
|
-
request_id: request_id || null,
|
|
1121
|
-
tool,
|
|
1122
|
-
mode: grantMode,
|
|
1123
|
-
expires_in_seconds: ttlMs / 1e3
|
|
1124
|
-
}));
|
|
1125
|
-
} catch {
|
|
1126
|
-
res.writeHead(400);
|
|
1127
|
-
res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
function handleListApprovals(res, approvalStore) {
|
|
1132
|
-
if (!approvalStore) {
|
|
1133
|
-
res.writeHead(200);
|
|
1134
|
-
res.end(JSON.stringify({ grants: [] }));
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
const now = Date.now();
|
|
1138
|
-
const grants = [];
|
|
1139
|
-
for (const [key, grant] of approvalStore) {
|
|
1140
|
-
if (now < grant.expires_at) {
|
|
1141
|
-
grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
res.writeHead(200);
|
|
1145
|
-
res.end(JSON.stringify({ grants }));
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// src/gateway.ts
|
|
1149
|
-
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
1150
592
|
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
1151
593
|
var ProtectGateway = class {
|
|
1152
594
|
child = null;
|
|
@@ -1172,8 +614,8 @@ var ProtectGateway = class {
|
|
|
1172
614
|
cedarPolicySet = null;
|
|
1173
615
|
constructor(config) {
|
|
1174
616
|
this.config = config;
|
|
1175
|
-
this.logFilePath =
|
|
1176
|
-
this.receiptFilePath =
|
|
617
|
+
this.logFilePath = join2(process.cwd(), LOG_FILE);
|
|
618
|
+
this.receiptFilePath = join2(process.cwd(), RECEIPTS_FILE);
|
|
1177
619
|
this.evidenceStore = new EvidenceStore();
|
|
1178
620
|
this.receiptBuffer = new ReceiptBuffer();
|
|
1179
621
|
this.notificationConfig = parseNotificationConfigFromEnv();
|
|
@@ -1663,23 +1105,13 @@ var ProtectGateway = class {
|
|
|
1663
1105
|
};
|
|
1664
1106
|
|
|
1665
1107
|
export {
|
|
1666
|
-
loadPolicy,
|
|
1667
|
-
getToolPolicy,
|
|
1668
|
-
parseRateLimit,
|
|
1669
|
-
checkRateLimit,
|
|
1670
1108
|
evaluateTier,
|
|
1671
1109
|
meetsMinTier,
|
|
1672
1110
|
resolveCredential,
|
|
1673
1111
|
listCredentialLabels,
|
|
1674
1112
|
validateCredentials,
|
|
1675
|
-
initSigning,
|
|
1676
|
-
signDecision,
|
|
1677
|
-
getSignerInfo,
|
|
1678
|
-
isSigningEnabled,
|
|
1679
1113
|
queryExternalPDP,
|
|
1680
1114
|
buildDecisionContext,
|
|
1681
|
-
loadCedarPolicies,
|
|
1682
|
-
isCedarAvailable,
|
|
1683
1115
|
sendApprovalNotification,
|
|
1684
1116
|
parseNotificationConfigFromEnv,
|
|
1685
1117
|
ProtectGateway
|