protect-mcp 0.2.1 → 0.3.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-3WCA7O4D.mjs +977 -0
- package/dist/cli.js +760 -163
- package/dist/cli.mjs +241 -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 +75 -60
- package/dist/index.d.ts +75 -60
- package/dist/index.js +507 -269
- package/dist/index.mjs +3 -123
- package/package.json +4 -4
- package/dist/chunk-ZCKNFULF.mjs +0 -613
package/dist/index.js
CHANGED
|
@@ -61,6 +61,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
61
61
|
var import_node_child_process = require("child_process");
|
|
62
62
|
var import_node_crypto2 = require("crypto");
|
|
63
63
|
var import_node_readline = require("readline");
|
|
64
|
+
var import_node_fs5 = require("fs");
|
|
65
|
+
var import_node_path3 = require("path");
|
|
64
66
|
|
|
65
67
|
// src/policy.ts
|
|
66
68
|
var import_node_crypto = require("crypto");
|
|
@@ -138,8 +140,126 @@ function checkRateLimit(key, limit, store) {
|
|
|
138
140
|
return { allowed: true, remaining: limit.count - timestamps.length };
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
// src/evidence-store.ts
|
|
144
|
+
var import_node_fs2 = require("fs");
|
|
145
|
+
var import_node_path = require("path");
|
|
146
|
+
var DEFAULT_THRESHOLDS = {
|
|
147
|
+
min_receipts: 10,
|
|
148
|
+
min_epoch_span: 3,
|
|
149
|
+
min_issuers: 2
|
|
150
|
+
};
|
|
151
|
+
var EvidenceStore = class {
|
|
152
|
+
agents = /* @__PURE__ */ new Map();
|
|
153
|
+
filePath;
|
|
154
|
+
dirty = false;
|
|
155
|
+
constructor(dir) {
|
|
156
|
+
this.filePath = (0, import_node_path.join)(dir || process.cwd(), ".protect-mcp-evidence.json");
|
|
157
|
+
this.load();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Record a receipt observation for an agent.
|
|
161
|
+
*/
|
|
162
|
+
record(agentId, issuer, timestamp) {
|
|
163
|
+
const ts = timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
164
|
+
const epochHour = Math.floor(new Date(ts).getTime() / (3600 * 1e3));
|
|
165
|
+
const existing = this.agents.get(agentId);
|
|
166
|
+
const observation = {
|
|
167
|
+
issuer,
|
|
168
|
+
timestamp: ts,
|
|
169
|
+
epoch_hour: epochHour
|
|
170
|
+
};
|
|
171
|
+
if (existing) {
|
|
172
|
+
existing.receipts.push(observation);
|
|
173
|
+
existing.last_seen = ts;
|
|
174
|
+
if (existing.receipts.length > 200) {
|
|
175
|
+
existing.receipts = existing.receipts.slice(-200);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
this.agents.set(agentId, {
|
|
179
|
+
agent_id: agentId,
|
|
180
|
+
receipts: [observation],
|
|
181
|
+
first_seen: ts,
|
|
182
|
+
last_seen: ts
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
this.dirty = true;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the evidence summary for an agent.
|
|
189
|
+
*/
|
|
190
|
+
getSummary(agentId) {
|
|
191
|
+
const record = this.agents.get(agentId);
|
|
192
|
+
if (!record || record.receipts.length === 0) {
|
|
193
|
+
return { receipt_count: 0, epoch_span: 0, issuer_count: 0 };
|
|
194
|
+
}
|
|
195
|
+
const uniqueIssuers = new Set(record.receipts.map((r) => r.issuer));
|
|
196
|
+
const uniqueEpochs = new Set(record.receipts.map((r) => r.epoch_hour));
|
|
197
|
+
return {
|
|
198
|
+
receipt_count: record.receipts.length,
|
|
199
|
+
epoch_span: uniqueEpochs.size,
|
|
200
|
+
issuer_count: uniqueIssuers.size
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Check if an agent meets the evidenced tier thresholds.
|
|
205
|
+
*/
|
|
206
|
+
meetsEvidencedThreshold(agentId, thresholds = DEFAULT_THRESHOLDS) {
|
|
207
|
+
const summary = this.getSummary(agentId);
|
|
208
|
+
return summary.receipt_count >= thresholds.min_receipts && summary.epoch_span >= thresholds.min_epoch_span && summary.issuer_count >= thresholds.min_issuers;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Persist to disk (call periodically or on shutdown).
|
|
212
|
+
*/
|
|
213
|
+
save() {
|
|
214
|
+
if (!this.dirty) return;
|
|
215
|
+
const data = {};
|
|
216
|
+
for (const [id, record] of this.agents) {
|
|
217
|
+
data[id] = record;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
(0, import_node_fs2.writeFileSync)(this.filePath, JSON.stringify({ v: 1, agents: data }, null, 2) + "\n");
|
|
221
|
+
this.dirty = false;
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Load from disk.
|
|
227
|
+
*/
|
|
228
|
+
load() {
|
|
229
|
+
if (!(0, import_node_fs2.existsSync)(this.filePath)) return;
|
|
230
|
+
try {
|
|
231
|
+
const raw = (0, import_node_fs2.readFileSync)(this.filePath, "utf-8");
|
|
232
|
+
const parsed = JSON.parse(raw);
|
|
233
|
+
if (parsed.agents && typeof parsed.agents === "object") {
|
|
234
|
+
for (const [id, record] of Object.entries(parsed.agents)) {
|
|
235
|
+
this.agents.set(id, record);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get total agent count (for status display).
|
|
243
|
+
*/
|
|
244
|
+
agentCount() {
|
|
245
|
+
return this.agents.size;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get all agent summaries (for status display).
|
|
249
|
+
*/
|
|
250
|
+
allSummaries() {
|
|
251
|
+
const result = [];
|
|
252
|
+
for (const [id] of this.agents) {
|
|
253
|
+
result.push({ agent_id: id, summary: this.getSummary(id) });
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
141
259
|
// src/admission.ts
|
|
142
|
-
function evaluateTier(manifest,
|
|
260
|
+
function evaluateTier(manifest, opts) {
|
|
261
|
+
const options = opts && ("evidenceStore" in opts || "overrides" in opts || "thresholds" in opts) ? opts : { overrides: opts };
|
|
262
|
+
const { overrides, evidenceStore, thresholds } = options;
|
|
143
263
|
if (!manifest) {
|
|
144
264
|
return {
|
|
145
265
|
tier: "unknown",
|
|
@@ -165,7 +285,8 @@ function evaluateTier(manifest, overrides) {
|
|
|
165
285
|
if (manifest.signature_valid === true) {
|
|
166
286
|
if (manifest.evidence_summary) {
|
|
167
287
|
const es = manifest.evidence_summary;
|
|
168
|
-
|
|
288
|
+
const t = thresholds || DEFAULT_THRESHOLDS;
|
|
289
|
+
if (es.receipt_count >= t.min_receipts && es.epoch_span >= t.min_epoch_span && es.issuer_count >= t.min_issuers) {
|
|
169
290
|
return {
|
|
170
291
|
tier: "evidenced",
|
|
171
292
|
agent_id: manifest.agent_id,
|
|
@@ -174,6 +295,16 @@ function evaluateTier(manifest, overrides) {
|
|
|
174
295
|
};
|
|
175
296
|
}
|
|
176
297
|
}
|
|
298
|
+
if (evidenceStore && manifest.agent_id) {
|
|
299
|
+
if (evidenceStore.meetsEvidencedThreshold(manifest.agent_id, thresholds)) {
|
|
300
|
+
return {
|
|
301
|
+
tier: "evidenced",
|
|
302
|
+
agent_id: manifest.agent_id,
|
|
303
|
+
manifest_hash: manifest.manifest_hash,
|
|
304
|
+
reason: "evidence_store_threshold_met"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
177
308
|
return {
|
|
178
309
|
tier: "signed-known",
|
|
179
310
|
agent_id: manifest.agent_id,
|
|
@@ -243,7 +374,7 @@ function validateCredentials(credentials) {
|
|
|
243
374
|
}
|
|
244
375
|
|
|
245
376
|
// src/signing.ts
|
|
246
|
-
var
|
|
377
|
+
var import_node_fs3 = require("fs");
|
|
247
378
|
var signerState = null;
|
|
248
379
|
var artifactsModule = null;
|
|
249
380
|
async function initSigning(config) {
|
|
@@ -258,12 +389,12 @@ async function initSigning(config) {
|
|
|
258
389
|
return warnings;
|
|
259
390
|
}
|
|
260
391
|
if (config.key_path) {
|
|
261
|
-
if (!(0,
|
|
392
|
+
if (!(0, import_node_fs3.existsSync)(config.key_path)) {
|
|
262
393
|
warnings.push(`signing: key file not found at ${config.key_path} \u2014 run "protect-mcp init" to generate`);
|
|
263
394
|
return warnings;
|
|
264
395
|
}
|
|
265
396
|
try {
|
|
266
|
-
const keyData = JSON.parse((0,
|
|
397
|
+
const keyData = JSON.parse((0, import_node_fs3.readFileSync)(config.key_path, "utf-8"));
|
|
267
398
|
if (!keyData.privateKey || !keyData.publicKey) {
|
|
268
399
|
warnings.push("signing: key file missing privateKey or publicKey fields");
|
|
269
400
|
return warnings;
|
|
@@ -335,21 +466,283 @@ function isSigningEnabled() {
|
|
|
335
466
|
return signerState !== null && artifactsModule !== null;
|
|
336
467
|
}
|
|
337
468
|
|
|
469
|
+
// src/external-pdp.ts
|
|
470
|
+
async function queryExternalPDP(context, config) {
|
|
471
|
+
const timeout = config.timeout_ms || 500;
|
|
472
|
+
const controller = new AbortController();
|
|
473
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
474
|
+
try {
|
|
475
|
+
const body = formatRequest(context, config.format || "generic");
|
|
476
|
+
const response = await fetch(config.endpoint, {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: { "Content-Type": "application/json" },
|
|
479
|
+
body: JSON.stringify(body),
|
|
480
|
+
signal: controller.signal
|
|
481
|
+
});
|
|
482
|
+
clearTimeout(timer);
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
return fallbackDecision(config, `PDP returned HTTP ${response.status}`);
|
|
485
|
+
}
|
|
486
|
+
const result = await response.json();
|
|
487
|
+
return parseResponse(result, config.format || "generic");
|
|
488
|
+
} catch (err) {
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
491
|
+
return fallbackDecision(config, `PDP timeout after ${timeout}ms`);
|
|
492
|
+
}
|
|
493
|
+
return fallbackDecision(config, `PDP error: ${err instanceof Error ? err.message : "unknown"}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function formatRequest(context, format) {
|
|
497
|
+
switch (format) {
|
|
498
|
+
case "opa":
|
|
499
|
+
return {
|
|
500
|
+
input: {
|
|
501
|
+
actor: context.actor,
|
|
502
|
+
action: context.action,
|
|
503
|
+
target: context.target,
|
|
504
|
+
credential_ref: context.credential_ref,
|
|
505
|
+
mode: context.mode,
|
|
506
|
+
metadata: context.request_metadata
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
case "cerbos":
|
|
510
|
+
return {
|
|
511
|
+
principal: {
|
|
512
|
+
id: context.actor.id || "unknown",
|
|
513
|
+
roles: [context.actor.tier],
|
|
514
|
+
attr: {
|
|
515
|
+
manifest_hash: context.actor.manifest_hash
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
resource: {
|
|
519
|
+
kind: "tool",
|
|
520
|
+
id: context.action.tool,
|
|
521
|
+
attr: context.target
|
|
522
|
+
},
|
|
523
|
+
actions: [context.action.operation || "call"]
|
|
524
|
+
};
|
|
525
|
+
case "generic":
|
|
526
|
+
default:
|
|
527
|
+
return context;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function parseResponse(result, format) {
|
|
531
|
+
switch (format) {
|
|
532
|
+
case "opa":
|
|
533
|
+
if (typeof result.result === "boolean") {
|
|
534
|
+
return { allowed: result.result };
|
|
535
|
+
}
|
|
536
|
+
if (result.result && typeof result.result === "object") {
|
|
537
|
+
const r = result.result;
|
|
538
|
+
return {
|
|
539
|
+
allowed: Boolean(r.allow),
|
|
540
|
+
reason: r.reason,
|
|
541
|
+
metadata: r
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return { allowed: false, reason: "unrecognized OPA response" };
|
|
545
|
+
case "cerbos":
|
|
546
|
+
if (Array.isArray(result.results) && result.results.length > 0) {
|
|
547
|
+
const actions = result.results[0].actions;
|
|
548
|
+
if (actions) {
|
|
549
|
+
const effect = Object.values(actions)[0];
|
|
550
|
+
return { allowed: effect === "EFFECT_ALLOW" };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { allowed: false, reason: "unrecognized Cerbos response" };
|
|
554
|
+
case "generic":
|
|
555
|
+
default:
|
|
556
|
+
return {
|
|
557
|
+
allowed: Boolean(result.allowed),
|
|
558
|
+
reason: result.reason,
|
|
559
|
+
metadata: result.metadata
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function fallbackDecision(config, reason) {
|
|
564
|
+
const fallback = config.fallback || "deny";
|
|
565
|
+
return {
|
|
566
|
+
allowed: fallback === "allow",
|
|
567
|
+
reason: `fallback_${fallback}: ${reason}`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function buildDecisionContext(toolName, tier, opts) {
|
|
571
|
+
return {
|
|
572
|
+
v: 1,
|
|
573
|
+
actor: {
|
|
574
|
+
id: opts.agentId,
|
|
575
|
+
tier,
|
|
576
|
+
manifest_hash: opts.manifestHash
|
|
577
|
+
},
|
|
578
|
+
action: {
|
|
579
|
+
tool: toolName,
|
|
580
|
+
operation: "call"
|
|
581
|
+
},
|
|
582
|
+
target: {
|
|
583
|
+
service: opts.slug || "default"
|
|
584
|
+
},
|
|
585
|
+
credential_ref: opts.credentialRef,
|
|
586
|
+
mode: opts.mode,
|
|
587
|
+
request_metadata: opts.requestMetadata || {}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/http-server.ts
|
|
592
|
+
var import_node_http = require("http");
|
|
593
|
+
var import_node_fs4 = require("fs");
|
|
594
|
+
var import_node_path2 = require("path");
|
|
595
|
+
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
596
|
+
var MAX_RECEIPTS = 100;
|
|
597
|
+
var ReceiptBuffer = class {
|
|
598
|
+
receipts = [];
|
|
599
|
+
add(requestId, receipt) {
|
|
600
|
+
this.receipts.push({
|
|
601
|
+
request_id: requestId,
|
|
602
|
+
receipt,
|
|
603
|
+
timestamp: Date.now()
|
|
604
|
+
});
|
|
605
|
+
if (this.receipts.length > MAX_RECEIPTS) {
|
|
606
|
+
this.receipts = this.receipts.slice(-MAX_RECEIPTS);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
getAll() {
|
|
610
|
+
return [...this.receipts].reverse();
|
|
611
|
+
}
|
|
612
|
+
getById(requestId) {
|
|
613
|
+
return this.receipts.find((r) => r.request_id === requestId);
|
|
614
|
+
}
|
|
615
|
+
count() {
|
|
616
|
+
return this.receipts.length;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
function startStatusServer(config, receiptBuffer) {
|
|
620
|
+
const startTime = Date.now();
|
|
621
|
+
const logDir = process.cwd();
|
|
622
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
623
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
624
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
625
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
626
|
+
res.setHeader("Content-Type", "application/json");
|
|
627
|
+
if (req.method === "OPTIONS") {
|
|
628
|
+
res.writeHead(204);
|
|
629
|
+
res.end();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const url = new URL(req.url || "/", `http://localhost:${config.port}`);
|
|
633
|
+
const path = url.pathname;
|
|
634
|
+
try {
|
|
635
|
+
if (path === "/health") {
|
|
636
|
+
handleHealth(res, startTime, config);
|
|
637
|
+
} else if (path === "/status") {
|
|
638
|
+
handleStatus(res, logDir);
|
|
639
|
+
} else if (path === "/receipts") {
|
|
640
|
+
handleReceipts(res, receiptBuffer, url);
|
|
641
|
+
} else if (path.startsWith("/receipts/")) {
|
|
642
|
+
const id = path.slice("/receipts/".length);
|
|
643
|
+
handleReceiptById(res, receiptBuffer, id);
|
|
644
|
+
} else {
|
|
645
|
+
res.writeHead(404);
|
|
646
|
+
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
|
|
647
|
+
}
|
|
648
|
+
} catch (err) {
|
|
649
|
+
res.writeHead(500);
|
|
650
|
+
res.end(JSON.stringify({ error: "internal_error" }));
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
server.listen(config.port, "127.0.0.1", () => {
|
|
654
|
+
if (config.verbose) {
|
|
655
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
656
|
+
`);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
server.unref();
|
|
660
|
+
return server;
|
|
661
|
+
}
|
|
662
|
+
function handleHealth(res, startTime, config) {
|
|
663
|
+
res.writeHead(200);
|
|
664
|
+
res.end(JSON.stringify({
|
|
665
|
+
status: "ok",
|
|
666
|
+
uptime_ms: Date.now() - startTime,
|
|
667
|
+
mode: config.mode,
|
|
668
|
+
version: "0.3.0"
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
function handleStatus(res, logDir) {
|
|
672
|
+
const logPath = (0, import_node_path2.join)(logDir, LOG_FILE);
|
|
673
|
+
if (!(0, import_node_fs4.existsSync)(logPath)) {
|
|
674
|
+
res.writeHead(200);
|
|
675
|
+
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const raw = (0, import_node_fs4.readFileSync)(logPath, "utf-8");
|
|
679
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
680
|
+
const entries = [];
|
|
681
|
+
for (const line of lines) {
|
|
682
|
+
try {
|
|
683
|
+
entries.push(JSON.parse(line));
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const toolCounts = {};
|
|
688
|
+
let allowCount = 0, denyCount = 0;
|
|
689
|
+
const tierCounts = {};
|
|
690
|
+
for (const e of entries) {
|
|
691
|
+
toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
692
|
+
if (e.decision === "allow") allowCount++;
|
|
693
|
+
else denyCount++;
|
|
694
|
+
if (e.tier) tierCounts[e.tier] = (tierCounts[e.tier] || 0) + 1;
|
|
695
|
+
}
|
|
696
|
+
res.writeHead(200);
|
|
697
|
+
res.end(JSON.stringify({
|
|
698
|
+
entries: entries.length,
|
|
699
|
+
allow: allowCount,
|
|
700
|
+
deny: denyCount,
|
|
701
|
+
tools: toolCounts,
|
|
702
|
+
tiers: tierCounts,
|
|
703
|
+
first_timestamp: entries.length > 0 ? entries[0].timestamp : null,
|
|
704
|
+
last_timestamp: entries.length > 0 ? entries[entries.length - 1].timestamp : null
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
function handleReceipts(res, buffer, url) {
|
|
708
|
+
const limit = parseInt(url.searchParams.get("limit") || "20", 10);
|
|
709
|
+
const receipts = buffer.getAll().slice(0, Math.min(limit, MAX_RECEIPTS));
|
|
710
|
+
res.writeHead(200);
|
|
711
|
+
res.end(JSON.stringify({
|
|
712
|
+
count: receipts.length,
|
|
713
|
+
total: buffer.count(),
|
|
714
|
+
receipts
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
function handleReceiptById(res, buffer, id) {
|
|
718
|
+
const receipt = buffer.getById(id);
|
|
719
|
+
if (!receipt) {
|
|
720
|
+
res.writeHead(404);
|
|
721
|
+
res.end(JSON.stringify({ error: "receipt_not_found", request_id: id }));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
res.writeHead(200);
|
|
725
|
+
res.end(JSON.stringify(receipt));
|
|
726
|
+
}
|
|
727
|
+
|
|
338
728
|
// src/gateway.ts
|
|
729
|
+
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
339
730
|
var ProtectGateway = class {
|
|
340
731
|
child = null;
|
|
341
732
|
config;
|
|
342
733
|
rateLimitStore = /* @__PURE__ */ new Map();
|
|
343
734
|
clientReader = null;
|
|
344
|
-
|
|
735
|
+
logFilePath;
|
|
736
|
+
evidenceStore;
|
|
737
|
+
receiptBuffer;
|
|
345
738
|
currentTier = "unknown";
|
|
346
739
|
admissionResult = null;
|
|
347
740
|
constructor(config) {
|
|
348
741
|
this.config = config;
|
|
742
|
+
this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
|
|
743
|
+
this.evidenceStore = new EvidenceStore();
|
|
744
|
+
this.receiptBuffer = new ReceiptBuffer();
|
|
349
745
|
}
|
|
350
|
-
/**
|
|
351
|
-
* Start the gateway: spawn child process and wire up message relay.
|
|
352
|
-
*/
|
|
353
746
|
async start() {
|
|
354
747
|
const { command, args, verbose } = this.config;
|
|
355
748
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
@@ -366,11 +759,34 @@ var ProtectGateway = class {
|
|
|
366
759
|
const labels = Object.keys(this.config.credentials);
|
|
367
760
|
this.log(`Credential vault: ${labels.length} credential(s) configured [${labels.join(", ")}]`);
|
|
368
761
|
}
|
|
762
|
+
if (this.config.policy?.policy_engine === "external" || this.config.policy?.policy_engine === "hybrid") {
|
|
763
|
+
this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
|
|
764
|
+
}
|
|
369
765
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
766
|
+
const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
|
|
767
|
+
if (httpPort > 0) {
|
|
768
|
+
try {
|
|
769
|
+
startStatusServer(
|
|
770
|
+
{ port: httpPort, mode, verbose },
|
|
771
|
+
this.receiptBuffer
|
|
772
|
+
);
|
|
773
|
+
} catch {
|
|
774
|
+
if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const childEnv = { ...process.env };
|
|
778
|
+
if (this.config.credentials) {
|
|
779
|
+
for (const [label, credConfig] of Object.entries(this.config.credentials)) {
|
|
780
|
+
if (credConfig.inject === "env" && credConfig.name && credConfig.value_env) {
|
|
781
|
+
const envValue = process.env[credConfig.value_env];
|
|
782
|
+
if (envValue) {
|
|
783
|
+
childEnv[credConfig.name] = envValue;
|
|
784
|
+
if (verbose) this.log(`Credential "${label}": injected as env var "${credConfig.name}"`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
this.child = (0, import_node_child_process.spawn)(command, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
|
|
374
790
|
if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
|
|
375
791
|
throw new Error("Failed to create pipes to child process");
|
|
376
792
|
}
|
|
@@ -386,9 +802,8 @@ var ProtectGateway = class {
|
|
|
386
802
|
this.handleClientMessage(line);
|
|
387
803
|
});
|
|
388
804
|
this.child.on("exit", (code, signal) => {
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
}
|
|
805
|
+
if (verbose) this.log(`Child process exited (code=${code}, signal=${signal})`);
|
|
806
|
+
this.evidenceStore.save();
|
|
392
807
|
process.exit(code ?? 1);
|
|
393
808
|
});
|
|
394
809
|
this.child.on("error", (err) => {
|
|
@@ -398,30 +813,18 @@ var ProtectGateway = class {
|
|
|
398
813
|
process.on("SIGINT", () => this.stop());
|
|
399
814
|
process.on("SIGTERM", () => this.stop());
|
|
400
815
|
process.stdin.on("end", () => {
|
|
401
|
-
if (this.
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
if (this.child?.stdin?.writable) {
|
|
405
|
-
this.child.stdin.end();
|
|
406
|
-
}
|
|
816
|
+
if (verbose) this.log("Client stdin closed, closing child stdin");
|
|
817
|
+
if (this.child?.stdin?.writable) this.child.stdin.end();
|
|
407
818
|
});
|
|
408
819
|
}
|
|
409
|
-
/**
|
|
410
|
-
* Set the trust tier for this session.
|
|
411
|
-
* Called at admission (first interaction) or by explicit manifest presentation.
|
|
412
|
-
*/
|
|
413
820
|
setManifest(manifest) {
|
|
414
|
-
this.admissionResult = evaluateTier(manifest);
|
|
821
|
+
this.admissionResult = evaluateTier(manifest, { evidenceStore: this.evidenceStore });
|
|
415
822
|
this.currentTier = this.admissionResult.tier;
|
|
416
823
|
if (this.config.verbose) {
|
|
417
|
-
this.log(`Admission: tier=${this.currentTier} agent=${this.admissionResult.agent_id || "none"}
|
|
824
|
+
this.log(`Admission: tier=${this.currentTier} agent=${this.admissionResult.agent_id || "none"}`);
|
|
418
825
|
}
|
|
419
826
|
return this.admissionResult;
|
|
420
827
|
}
|
|
421
|
-
/**
|
|
422
|
-
* Handle a message from the MCP client (stdin).
|
|
423
|
-
* Intercept tools/call requests; pass through everything else.
|
|
424
|
-
*/
|
|
425
828
|
handleClientMessage(raw) {
|
|
426
829
|
const trimmed = raw.trim();
|
|
427
830
|
if (!trimmed) return;
|
|
@@ -433,25 +836,38 @@ var ProtectGateway = class {
|
|
|
433
836
|
return;
|
|
434
837
|
}
|
|
435
838
|
if (message.method === "tools/call" && message.id !== void 0) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.sendToClient(JSON.stringify(result));
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
839
|
+
this.interceptToolCallAsync(message, trimmed);
|
|
840
|
+
return;
|
|
441
841
|
}
|
|
442
842
|
this.sendToChild(trimmed);
|
|
443
843
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
844
|
+
async interceptToolCallAsync(request, raw) {
|
|
845
|
+
const result = await this.interceptToolCall(request);
|
|
846
|
+
if (result) {
|
|
847
|
+
this.sendToClient(JSON.stringify(result));
|
|
848
|
+
} else {
|
|
849
|
+
const modified = this.injectParamsCredentials(request);
|
|
850
|
+
this.sendToChild(JSON.stringify(modified));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
448
853
|
handleServerMessage(raw) {
|
|
449
854
|
this.sendToClient(raw);
|
|
450
855
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
856
|
+
injectParamsCredentials(request) {
|
|
857
|
+
if (!this.config.credentials) return request;
|
|
858
|
+
const injections = {};
|
|
859
|
+
for (const [label, credConfig] of Object.entries(this.config.credentials)) {
|
|
860
|
+
if (credConfig.inject === "header" || credConfig.inject === "query") {
|
|
861
|
+
const cred = resolveCredential(label, this.config.credentials);
|
|
862
|
+
if (cred.resolved && cred.value && cred.name) {
|
|
863
|
+
injections[cred.name] = cred.value;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (Object.keys(injections).length === 0) return request;
|
|
868
|
+
return { ...request, params: { ...request.params, _credentials: injections } };
|
|
869
|
+
}
|
|
870
|
+
async interceptToolCall(request) {
|
|
455
871
|
const toolName = request.params?.name || "unknown";
|
|
456
872
|
const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
|
|
457
873
|
const toolPolicy = getToolPolicy(toolName, this.config.policy);
|
|
@@ -462,49 +878,45 @@ var ProtectGateway = class {
|
|
|
462
878
|
if (cred.resolved) {
|
|
463
879
|
credentialRef = cred.label;
|
|
464
880
|
} else if (cred.error && !cred.error.includes("not configured")) {
|
|
465
|
-
this.emitDecisionLog({
|
|
466
|
-
tool: toolName,
|
|
467
|
-
decision: "deny",
|
|
468
|
-
reason_code: "policy_block",
|
|
469
|
-
request_id: requestId,
|
|
470
|
-
credential_ref: toolName,
|
|
471
|
-
tier: this.currentTier
|
|
472
|
-
});
|
|
881
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName });
|
|
473
882
|
if (this.config.enforce) {
|
|
474
|
-
this.log(`Credential error for "${toolName}": ${cred.error}`);
|
|
475
883
|
return this.makeErrorResponse(request.id, -32600, `Credential error for tool "${toolName}"`);
|
|
476
884
|
}
|
|
477
885
|
}
|
|
478
886
|
}
|
|
887
|
+
if (this.config.policy?.external && (this.config.policy.policy_engine === "external" || this.config.policy.policy_engine === "hybrid")) {
|
|
888
|
+
try {
|
|
889
|
+
const ctx = buildDecisionContext(toolName, this.currentTier, {
|
|
890
|
+
agentId: this.admissionResult?.agent_id,
|
|
891
|
+
manifestHash: this.admissionResult?.manifest_hash,
|
|
892
|
+
credentialRef,
|
|
893
|
+
mode,
|
|
894
|
+
slug: this.config.slug
|
|
895
|
+
});
|
|
896
|
+
const externalDecision = await queryExternalPDP(ctx, this.config.policy.external);
|
|
897
|
+
if (!externalDecision.allowed) {
|
|
898
|
+
const reason = `external_pdp_deny${externalDecision.reason ? ": " + externalDecision.reason : ""}`;
|
|
899
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
900
|
+
if (this.config.enforce) {
|
|
901
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by external policy engine`);
|
|
902
|
+
}
|
|
903
|
+
if (this.config.policy.policy_engine === "external") return null;
|
|
904
|
+
}
|
|
905
|
+
} catch (err) {
|
|
906
|
+
if (this.config.verbose) this.log(`External PDP error: ${err instanceof Error ? err.message : err}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
479
909
|
if (toolPolicy.min_tier) {
|
|
480
910
|
if (!meetsMinTier(this.currentTier, toolPolicy.min_tier)) {
|
|
481
|
-
this.emitDecisionLog({
|
|
482
|
-
tool: toolName,
|
|
483
|
-
decision: "deny",
|
|
484
|
-
reason_code: "tier_insufficient",
|
|
485
|
-
request_id: requestId,
|
|
486
|
-
tier: this.currentTier,
|
|
487
|
-
credential_ref: credentialRef
|
|
488
|
-
});
|
|
911
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
489
912
|
if (this.config.enforce) {
|
|
490
|
-
return this.makeErrorResponse(
|
|
491
|
-
request.id,
|
|
492
|
-
-32600,
|
|
493
|
-
`Tool "${toolName}" requires tier "${toolPolicy.min_tier}", agent has "${this.currentTier}"`
|
|
494
|
-
);
|
|
913
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" requires tier "${toolPolicy.min_tier}"`);
|
|
495
914
|
}
|
|
496
915
|
return null;
|
|
497
916
|
}
|
|
498
917
|
}
|
|
499
918
|
if (toolPolicy.block) {
|
|
500
|
-
this.emitDecisionLog({
|
|
501
|
-
tool: toolName,
|
|
502
|
-
decision: "deny",
|
|
503
|
-
reason_code: "policy_block",
|
|
504
|
-
request_id: requestId,
|
|
505
|
-
tier: this.currentTier,
|
|
506
|
-
credential_ref: credentialRef
|
|
507
|
-
});
|
|
919
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
508
920
|
if (this.config.enforce) {
|
|
509
921
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" is blocked by policy`);
|
|
510
922
|
}
|
|
@@ -517,59 +929,22 @@ var ProtectGateway = class {
|
|
|
517
929
|
const key = `tool:${toolName}:${this.currentTier}`;
|
|
518
930
|
const { allowed, remaining } = checkRateLimit(key, limit, this.rateLimitStore);
|
|
519
931
|
if (!allowed) {
|
|
520
|
-
this.emitDecisionLog({
|
|
521
|
-
tool: toolName,
|
|
522
|
-
decision: "deny",
|
|
523
|
-
reason_code: "rate_limit_exceeded",
|
|
524
|
-
request_id: requestId,
|
|
525
|
-
rate_limit_remaining: 0,
|
|
526
|
-
tier: this.currentTier,
|
|
527
|
-
credential_ref: credentialRef
|
|
528
|
-
});
|
|
932
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef });
|
|
529
933
|
if (this.config.enforce) {
|
|
530
|
-
return this.makeErrorResponse(
|
|
531
|
-
request.id,
|
|
532
|
-
-32600,
|
|
533
|
-
`Tool "${toolName}" rate limit exceeded (${rateSpec})`
|
|
534
|
-
);
|
|
934
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" rate limit exceeded (${rateSpec})`);
|
|
535
935
|
}
|
|
536
936
|
return null;
|
|
537
937
|
}
|
|
538
|
-
this.emitDecisionLog({
|
|
539
|
-
tool: toolName,
|
|
540
|
-
decision: "allow",
|
|
541
|
-
reason_code: "policy_allow",
|
|
542
|
-
request_id: requestId,
|
|
543
|
-
rate_limit_remaining: remaining,
|
|
544
|
-
tier: this.currentTier,
|
|
545
|
-
credential_ref: credentialRef
|
|
546
|
-
});
|
|
938
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef });
|
|
547
939
|
} catch {
|
|
548
|
-
this.emitDecisionLog({
|
|
549
|
-
tool: toolName,
|
|
550
|
-
decision: "allow",
|
|
551
|
-
reason_code: "default_allow",
|
|
552
|
-
request_id: requestId,
|
|
553
|
-
tier: this.currentTier,
|
|
554
|
-
credential_ref: credentialRef
|
|
555
|
-
});
|
|
940
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
556
941
|
}
|
|
557
942
|
} else {
|
|
558
943
|
const reasonCode = this.config.enforce ? "policy_allow" : "observe_mode";
|
|
559
|
-
this.emitDecisionLog({
|
|
560
|
-
tool: toolName,
|
|
561
|
-
decision: "allow",
|
|
562
|
-
reason_code: reasonCode,
|
|
563
|
-
request_id: requestId,
|
|
564
|
-
tier: this.currentTier,
|
|
565
|
-
credential_ref: credentialRef
|
|
566
|
-
});
|
|
944
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
567
945
|
}
|
|
568
946
|
return null;
|
|
569
947
|
}
|
|
570
|
-
/**
|
|
571
|
-
* Get the applicable rate limit spec based on the agent's tier.
|
|
572
|
-
*/
|
|
573
948
|
getTierRateLimit(policy, tier) {
|
|
574
949
|
if (policy.rate_limits && policy.rate_limits[tier]) {
|
|
575
950
|
const tierLimit = policy.rate_limits[tier];
|
|
@@ -577,10 +952,6 @@ var ProtectGateway = class {
|
|
|
577
952
|
}
|
|
578
953
|
return policy.rate_limit;
|
|
579
954
|
}
|
|
580
|
-
/**
|
|
581
|
-
* Emit a structured decision log to stderr.
|
|
582
|
-
* If signing is enabled, also emits a signed artifact.
|
|
583
|
-
*/
|
|
584
955
|
emitDecisionLog(entry) {
|
|
585
956
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
586
957
|
const log = {
|
|
@@ -599,55 +970,44 @@ var ProtectGateway = class {
|
|
|
599
970
|
};
|
|
600
971
|
process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
|
|
601
972
|
`);
|
|
973
|
+
try {
|
|
974
|
+
(0, import_node_fs5.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
602
977
|
if (isSigningEnabled()) {
|
|
603
978
|
const signed = signDecision(log);
|
|
604
979
|
if (signed.signed) {
|
|
605
980
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
606
981
|
`);
|
|
982
|
+
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
983
|
+
if (this.admissionResult?.agent_id) {
|
|
984
|
+
this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
|
|
985
|
+
if (this.evidenceStore.getSummary(this.admissionResult.agent_id).receipt_count % 10 === 0) {
|
|
986
|
+
this.evidenceStore.save();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
607
989
|
} else if (signed.warning) {
|
|
608
990
|
process.stderr.write(`[PROTECT_MCP] Warning: ${signed.warning}
|
|
609
991
|
`);
|
|
610
992
|
}
|
|
611
993
|
}
|
|
612
994
|
}
|
|
613
|
-
/**
|
|
614
|
-
* Create a JSON-RPC error response.
|
|
615
|
-
*/
|
|
616
995
|
makeErrorResponse(id, code, message) {
|
|
617
|
-
return {
|
|
618
|
-
jsonrpc: "2.0",
|
|
619
|
-
id,
|
|
620
|
-
error: { code, message }
|
|
621
|
-
};
|
|
996
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
622
997
|
}
|
|
623
|
-
/**
|
|
624
|
-
* Send a message to the child process (wrapped MCP server).
|
|
625
|
-
*/
|
|
626
998
|
sendToChild(message) {
|
|
627
|
-
if (this.child?.stdin?.writable)
|
|
628
|
-
this.child.stdin.write(message + "\n");
|
|
629
|
-
}
|
|
999
|
+
if (this.child?.stdin?.writable) this.child.stdin.write(message + "\n");
|
|
630
1000
|
}
|
|
631
|
-
/**
|
|
632
|
-
* Send a message to the MCP client (stdout).
|
|
633
|
-
*/
|
|
634
1001
|
sendToClient(message) {
|
|
635
1002
|
process.stdout.write(message + "\n");
|
|
636
1003
|
}
|
|
637
|
-
/**
|
|
638
|
-
* Log a message to stderr (debug output).
|
|
639
|
-
*/
|
|
640
1004
|
log(message) {
|
|
641
1005
|
process.stderr.write(`[PROTECT_MCP] ${message}
|
|
642
1006
|
`);
|
|
643
1007
|
}
|
|
644
|
-
/**
|
|
645
|
-
* Stop the gateway: kill child process and exit.
|
|
646
|
-
*/
|
|
647
1008
|
stop() {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
1009
|
+
this.evidenceStore.save();
|
|
1010
|
+
if (this.clientReader) this.clientReader.close();
|
|
651
1011
|
if (this.child) {
|
|
652
1012
|
this.child.kill("SIGTERM");
|
|
653
1013
|
this.child = null;
|
|
@@ -656,128 +1016,6 @@ var ProtectGateway = class {
|
|
|
656
1016
|
}
|
|
657
1017
|
};
|
|
658
1018
|
|
|
659
|
-
// src/external-pdp.ts
|
|
660
|
-
async function queryExternalPDP(context, config) {
|
|
661
|
-
const timeout = config.timeout_ms || 500;
|
|
662
|
-
const controller = new AbortController();
|
|
663
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
664
|
-
try {
|
|
665
|
-
const body = formatRequest(context, config.format || "generic");
|
|
666
|
-
const response = await fetch(config.endpoint, {
|
|
667
|
-
method: "POST",
|
|
668
|
-
headers: { "Content-Type": "application/json" },
|
|
669
|
-
body: JSON.stringify(body),
|
|
670
|
-
signal: controller.signal
|
|
671
|
-
});
|
|
672
|
-
clearTimeout(timer);
|
|
673
|
-
if (!response.ok) {
|
|
674
|
-
return fallbackDecision(config, `PDP returned HTTP ${response.status}`);
|
|
675
|
-
}
|
|
676
|
-
const result = await response.json();
|
|
677
|
-
return parseResponse(result, config.format || "generic");
|
|
678
|
-
} catch (err) {
|
|
679
|
-
clearTimeout(timer);
|
|
680
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
681
|
-
return fallbackDecision(config, `PDP timeout after ${timeout}ms`);
|
|
682
|
-
}
|
|
683
|
-
return fallbackDecision(config, `PDP error: ${err instanceof Error ? err.message : "unknown"}`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
function formatRequest(context, format) {
|
|
687
|
-
switch (format) {
|
|
688
|
-
case "opa":
|
|
689
|
-
return {
|
|
690
|
-
input: {
|
|
691
|
-
actor: context.actor,
|
|
692
|
-
action: context.action,
|
|
693
|
-
target: context.target,
|
|
694
|
-
credential_ref: context.credential_ref,
|
|
695
|
-
mode: context.mode,
|
|
696
|
-
metadata: context.request_metadata
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
case "cerbos":
|
|
700
|
-
return {
|
|
701
|
-
principal: {
|
|
702
|
-
id: context.actor.id || "unknown",
|
|
703
|
-
roles: [context.actor.tier],
|
|
704
|
-
attr: {
|
|
705
|
-
manifest_hash: context.actor.manifest_hash
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
resource: {
|
|
709
|
-
kind: "tool",
|
|
710
|
-
id: context.action.tool,
|
|
711
|
-
attr: context.target
|
|
712
|
-
},
|
|
713
|
-
actions: [context.action.operation || "call"]
|
|
714
|
-
};
|
|
715
|
-
case "generic":
|
|
716
|
-
default:
|
|
717
|
-
return context;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
function parseResponse(result, format) {
|
|
721
|
-
switch (format) {
|
|
722
|
-
case "opa":
|
|
723
|
-
if (typeof result.result === "boolean") {
|
|
724
|
-
return { allowed: result.result };
|
|
725
|
-
}
|
|
726
|
-
if (result.result && typeof result.result === "object") {
|
|
727
|
-
const r = result.result;
|
|
728
|
-
return {
|
|
729
|
-
allowed: Boolean(r.allow),
|
|
730
|
-
reason: r.reason,
|
|
731
|
-
metadata: r
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
return { allowed: false, reason: "unrecognized OPA response" };
|
|
735
|
-
case "cerbos":
|
|
736
|
-
if (Array.isArray(result.results) && result.results.length > 0) {
|
|
737
|
-
const actions = result.results[0].actions;
|
|
738
|
-
if (actions) {
|
|
739
|
-
const effect = Object.values(actions)[0];
|
|
740
|
-
return { allowed: effect === "EFFECT_ALLOW" };
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return { allowed: false, reason: "unrecognized Cerbos response" };
|
|
744
|
-
case "generic":
|
|
745
|
-
default:
|
|
746
|
-
return {
|
|
747
|
-
allowed: Boolean(result.allowed),
|
|
748
|
-
reason: result.reason,
|
|
749
|
-
metadata: result.metadata
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
function fallbackDecision(config, reason) {
|
|
754
|
-
const fallback = config.fallback || "deny";
|
|
755
|
-
return {
|
|
756
|
-
allowed: fallback === "allow",
|
|
757
|
-
reason: `fallback_${fallback}: ${reason}`
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
function buildDecisionContext(toolName, tier, opts) {
|
|
761
|
-
return {
|
|
762
|
-
v: 1,
|
|
763
|
-
actor: {
|
|
764
|
-
id: opts.agentId,
|
|
765
|
-
tier,
|
|
766
|
-
manifest_hash: opts.manifestHash
|
|
767
|
-
},
|
|
768
|
-
action: {
|
|
769
|
-
tool: toolName,
|
|
770
|
-
operation: "call"
|
|
771
|
-
},
|
|
772
|
-
target: {
|
|
773
|
-
service: opts.slug || "default"
|
|
774
|
-
},
|
|
775
|
-
credential_ref: opts.credentialRef,
|
|
776
|
-
mode: opts.mode,
|
|
777
|
-
request_metadata: opts.requestMetadata || {}
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
|
|
781
1019
|
// src/bundle.ts
|
|
782
1020
|
function createAuditBundle(opts) {
|
|
783
1021
|
const receipts = opts.receipts.filter(
|