protect-mcp 0.9.6 → 0.9.7
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 +30 -0
- package/dist/{chunk-WCZJGEBO.mjs → chunk-AUUAVWQM.mjs} +6 -4
- package/dist/{chunk-XLJUZ4WO.mjs → chunk-G6X763MH.mjs} +7 -215
- package/dist/{chunk-7MHK5RF4.mjs → chunk-MOXINIMB.mjs} +2 -2
- package/dist/chunk-MWXDXYWH.mjs +210 -0
- package/dist/{chunk-PB3TC7E3.mjs → chunk-YM6SOJBR.mjs} +4 -2
- package/dist/cli.js +279 -3
- package/dist/cli.mjs +15 -8
- package/dist/hook-server.mjs +3 -2
- package/dist/{http-transport-HLSMVBI6.mjs → http-transport-VHD3YBT5.mjs} +3 -2
- package/dist/index.mjs +11 -9
- package/dist/mcp-server.d.mts +24 -0
- package/dist/mcp-server.d.ts +24 -0
- package/dist/mcp-server.js +445 -0
- package/dist/mcp-server.mjs +269 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -57,6 +57,36 @@ The dashboard binds to `127.0.0.1`, reads only local log/receipt files, and does
|
|
|
57
57
|
not upload anything. Use `npx protect-mcp connect` only if you explicitly want a
|
|
58
58
|
hosted ScopeBlind dashboard.
|
|
59
59
|
|
|
60
|
+
## The gate as an MCP server
|
|
61
|
+
|
|
62
|
+
If you would rather call the gate as tools than wire the Claude Code hooks, run
|
|
63
|
+
it as an MCP server:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx protect-mcp mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It speaks MCP over stdio and exposes four read-only tools, the whole loop:
|
|
70
|
+
|
|
71
|
+
- **`evaluate_action`**: decide a proposed tool call against an inline Cedar policy, fail-closed (any policy error is DENY). Returns `{ allowed, decision, reason, policy_digest }`.
|
|
72
|
+
- **`sign_decision`**: turn a decision into an Ed25519 signed receipt (a denial signs a `gateway_restraint`, an allow a `decision_receipt`). Returns the receipt and its public key; generates an ephemeral key if you do not supply one.
|
|
73
|
+
- **`verify_receipt`**: verify a signed receipt offline against a public key. Returns `{ valid, error, type, kid, issuer }`.
|
|
74
|
+
- **`self_test`**: prove it, no inputs. A known-forbidden action is denied, then a signed receipt round-trips and a tampered copy fails.
|
|
75
|
+
|
|
76
|
+
Point any MCP host at it, for example Claude Desktop:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"protect-mcp": { "command": "npx", "args": ["-y", "protect-mcp", "mcp"] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Receipts are byte-compatible with the ones the gate signs at runtime, so a
|
|
87
|
+
receipt minted here verifies with [`@veritasacta/verify`](https://www.npmjs.com/package/@veritasacta/verify)
|
|
88
|
+
and the browser verifier just the same.
|
|
89
|
+
|
|
60
90
|
### Local Action Dashboard
|
|
61
91
|
|
|
62
92
|
`protect-mcp dashboard` is the operator view for moving from visibility to
|
|
@@ -5,17 +5,19 @@ import {
|
|
|
5
5
|
ReceiptBuffer,
|
|
6
6
|
buildActionReadback,
|
|
7
7
|
checkRateLimit,
|
|
8
|
-
evaluateCedar,
|
|
9
8
|
getSignerInfo,
|
|
10
9
|
getToolPolicy,
|
|
11
10
|
initSigning,
|
|
12
|
-
isCedarAvailable,
|
|
13
11
|
isSigningEnabled,
|
|
14
|
-
loadCedarPolicies,
|
|
15
12
|
loadPolicy,
|
|
16
13
|
parseRateLimit,
|
|
17
14
|
signDecision
|
|
18
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-G6X763MH.mjs";
|
|
16
|
+
import {
|
|
17
|
+
evaluateCedar,
|
|
18
|
+
isCedarAvailable,
|
|
19
|
+
loadCedarPolicies
|
|
20
|
+
} from "./chunk-MWXDXYWH.mjs";
|
|
19
21
|
|
|
20
22
|
// src/hook-server.ts
|
|
21
23
|
import { createServer } from "http";
|
|
@@ -236,213 +236,10 @@ function isSigningEnabled() {
|
|
|
236
236
|
return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// src/cedar-evaluator.ts
|
|
240
|
-
import { createHash as createHash2 } from "crypto";
|
|
241
|
-
import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync2 } from "fs";
|
|
242
|
-
import { join, extname } from "path";
|
|
243
|
-
var cedarWasm = null;
|
|
244
|
-
var loadAttempted = false;
|
|
245
|
-
async function ensureCedarWasm() {
|
|
246
|
-
if (cedarWasm) return true;
|
|
247
|
-
if (loadAttempted) return false;
|
|
248
|
-
loadAttempted = true;
|
|
249
|
-
try {
|
|
250
|
-
const moduleName = "@cedar-policy/cedar-wasm";
|
|
251
|
-
cedarWasm = await import(
|
|
252
|
-
/* @vite-ignore */
|
|
253
|
-
moduleName
|
|
254
|
-
);
|
|
255
|
-
return true;
|
|
256
|
-
} catch {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function loadCedarPolicies(dirPath) {
|
|
261
|
-
if (!existsSync2(dirPath)) {
|
|
262
|
-
throw new Error(`Cedar policy directory not found: ${dirPath}`);
|
|
263
|
-
}
|
|
264
|
-
const entries = readdirSync(dirPath).filter((f) => extname(f) === ".cedar").sort();
|
|
265
|
-
if (entries.length === 0) {
|
|
266
|
-
throw new Error(`No .cedar files found in: ${dirPath}`);
|
|
267
|
-
}
|
|
268
|
-
const sources = [];
|
|
269
|
-
for (const file of entries) {
|
|
270
|
-
const content = readFileSync3(join(dirPath, file), "utf-8");
|
|
271
|
-
sources.push(content);
|
|
272
|
-
}
|
|
273
|
-
const concatenated = sources.join("\n\n");
|
|
274
|
-
const digest = createHash2("sha256").update(concatenated).digest("hex").slice(0, 16);
|
|
275
|
-
return {
|
|
276
|
-
source: concatenated,
|
|
277
|
-
digest,
|
|
278
|
-
fileCount: entries.length,
|
|
279
|
-
files: entries
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function buildEntities(req) {
|
|
283
|
-
const agentId = req.agentId || req.tier;
|
|
284
|
-
return [
|
|
285
|
-
{
|
|
286
|
-
uid: { type: "Agent", id: agentId },
|
|
287
|
-
attrs: {
|
|
288
|
-
tier: req.tier,
|
|
289
|
-
...req.agentId ? { agent_id: req.agentId } : {}
|
|
290
|
-
},
|
|
291
|
-
parents: []
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
uid: { type: "Tool", id: req.tool },
|
|
295
|
-
attrs: {},
|
|
296
|
-
parents: []
|
|
297
|
-
}
|
|
298
|
-
];
|
|
299
|
-
}
|
|
300
|
-
function onEvalError(reason, failClosed, extra) {
|
|
301
|
-
return {
|
|
302
|
-
allowed: !failClosed,
|
|
303
|
-
reason: failClosed ? reason : `${reason} (observe mode; would DENY under enforcement)`,
|
|
304
|
-
metadata: { error: true, fail_closed: failClosed, would_deny: true, ...extra || {} }
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
async function evaluateCedar(policySet, req, schema, options) {
|
|
308
|
-
const failClosed = options?.failClosed ?? true;
|
|
309
|
-
const available = await ensureCedarWasm();
|
|
310
|
-
if (!available) {
|
|
311
|
-
return onEvalError("cedar_wasm_not_available", failClosed, { fallback: true });
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
const agentId = req.agentId || req.tier;
|
|
315
|
-
const context = {
|
|
316
|
-
tier: req.tier,
|
|
317
|
-
...req.context || {}
|
|
318
|
-
};
|
|
319
|
-
if (req.toolInput && Object.keys(req.toolInput).length > 0) {
|
|
320
|
-
context.input = req.toolInput;
|
|
321
|
-
}
|
|
322
|
-
const authRequest = {
|
|
323
|
-
principal: { type: "Agent", id: agentId },
|
|
324
|
-
action: { type: "Action", id: "MCP::Tool::call" },
|
|
325
|
-
resource: { type: "Tool", id: req.tool },
|
|
326
|
-
context
|
|
327
|
-
};
|
|
328
|
-
const entities = buildEntities(req);
|
|
329
|
-
const cedarSchema = schema?.schemaJson ?? null;
|
|
330
|
-
let result;
|
|
331
|
-
if (typeof cedarWasm.isAuthorized === "function") {
|
|
332
|
-
result = cedarWasm.isAuthorized({
|
|
333
|
-
policies: { staticPolicies: policySet.source },
|
|
334
|
-
entities,
|
|
335
|
-
principal: authRequest.principal,
|
|
336
|
-
action: authRequest.action,
|
|
337
|
-
resource: authRequest.resource,
|
|
338
|
-
context: authRequest.context,
|
|
339
|
-
schema: cedarSchema
|
|
340
|
-
});
|
|
341
|
-
} else if (typeof cedarWasm.checkAuthorization === "function") {
|
|
342
|
-
result = cedarWasm.checkAuthorization(
|
|
343
|
-
policySet.source,
|
|
344
|
-
JSON.stringify(entities),
|
|
345
|
-
JSON.stringify(authRequest)
|
|
346
|
-
);
|
|
347
|
-
} else {
|
|
348
|
-
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
349
|
-
if (typeof cedarEngine.isAuthorized === "function") {
|
|
350
|
-
result = cedarEngine.isAuthorized({
|
|
351
|
-
policies: { staticPolicies: policySet.source },
|
|
352
|
-
entities,
|
|
353
|
-
principal: authRequest.principal,
|
|
354
|
-
action: authRequest.action,
|
|
355
|
-
resource: authRequest.resource,
|
|
356
|
-
context: authRequest.context,
|
|
357
|
-
schema: cedarSchema
|
|
358
|
-
});
|
|
359
|
-
} else {
|
|
360
|
-
return onEvalError("cedar_wasm_api_unsupported", failClosed, { exports: Object.keys(cedarWasm) });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const parsed = parseWasmResult(result);
|
|
364
|
-
const policyErrors = extractPolicyErrors(result);
|
|
365
|
-
if (parsed.kind === "error") {
|
|
366
|
-
return onEvalError(`cedar_unparseable_result: ${parsed.diagnostics}`, failClosed);
|
|
367
|
-
}
|
|
368
|
-
if (policyErrors.length > 0) {
|
|
369
|
-
return onEvalError(
|
|
370
|
-
`cedar_policy_errored: ${policyErrors.length} policy error(s); decision is unsound`,
|
|
371
|
-
failClosed,
|
|
372
|
-
{ policy_errors: policyErrors.slice(0, 5), policy_digest: policySet.digest }
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
allowed: parsed.kind === "allow",
|
|
377
|
-
reason: parsed.kind === "allow" ? void 0 : `cedar_deny${parsed.diagnostics ? ": " + parsed.diagnostics : ""}`,
|
|
378
|
-
metadata: {
|
|
379
|
-
policy_digest: policySet.digest,
|
|
380
|
-
...parsed.matchedPolicies ? { matched_policies: parsed.matchedPolicies } : {}
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
} catch (err) {
|
|
384
|
-
return onEvalError(`cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`, failClosed);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
function parseWasmResult(result) {
|
|
388
|
-
if (!result) return { kind: "error", diagnostics: "null result from Cedar WASM" };
|
|
389
|
-
if (result.type === "failure") {
|
|
390
|
-
return { kind: "error", diagnostics: `cedar failure: ${JSON.stringify(result.errors ?? [])}` };
|
|
391
|
-
}
|
|
392
|
-
if (result.type === "success" && result.response) {
|
|
393
|
-
const dec = result.response.decision;
|
|
394
|
-
const reasons = result.response.diagnostics?.reason;
|
|
395
|
-
if (dec === "allow" || dec === "Allow") return { kind: "allow", matchedPolicies: reasons };
|
|
396
|
-
if (dec === "deny" || dec === "Deny") {
|
|
397
|
-
return { kind: "deny", diagnostics: result.response.diagnostics ? JSON.stringify(result.response.diagnostics) : void 0, matchedPolicies: reasons };
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (result.type === "allow" || result.decision === "Allow") return { kind: "allow" };
|
|
401
|
-
if (result.type === "deny" || result.decision === "Deny") return { kind: "deny" };
|
|
402
|
-
if (typeof result === "boolean") return result ? { kind: "allow" } : { kind: "deny" };
|
|
403
|
-
return { kind: "error", diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
404
|
-
}
|
|
405
|
-
function extractPolicyErrors(result) {
|
|
406
|
-
if (!result || typeof result !== "object") return [];
|
|
407
|
-
const raw = result.errors ?? result.response?.diagnostics?.errors ?? result.diagnostics?.errors ?? [];
|
|
408
|
-
if (!Array.isArray(raw)) return [];
|
|
409
|
-
return raw.map((e) => typeof e === "string" ? e : e?.message ?? e?.error ?? JSON.stringify(e)).filter(Boolean);
|
|
410
|
-
}
|
|
411
|
-
async function isCedarAvailable() {
|
|
412
|
-
return ensureCedarWasm();
|
|
413
|
-
}
|
|
414
|
-
function policySetFromSource(source, name = "inline") {
|
|
415
|
-
const digest = createHash2("sha256").update(source).digest("hex").slice(0, 16);
|
|
416
|
-
return { source, digest, fileCount: 1, files: [name] };
|
|
417
|
-
}
|
|
418
|
-
async function runEvaluatorSelfTest() {
|
|
419
|
-
const wasmAvailable = await isCedarAvailable();
|
|
420
|
-
const cases = [];
|
|
421
|
-
const run = async (name, expected, policy, context) => {
|
|
422
|
-
const d = await evaluateCedar(policy, { tool: "Bash", tier: "unknown", context }, void 0, { failClosed: true });
|
|
423
|
-
const actual = d.allowed ? "ALLOW" : "DENY";
|
|
424
|
-
cases.push({ name, expected, actual, pass: actual === expected, reason: d.reason });
|
|
425
|
-
};
|
|
426
|
-
if (!wasmAvailable) {
|
|
427
|
-
await run("engine unavailable denies", "DENY", policySetFromSource("permit(principal, action, resource);"), {});
|
|
428
|
-
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
429
|
-
}
|
|
430
|
-
const correct = policySetFromSource(
|
|
431
|
-
'forbid(principal, action, resource) when { ["rm", "dd", "mkfs"].contains(context.command) };\npermit(principal, action, resource);'
|
|
432
|
-
);
|
|
433
|
-
await run("forbid denies rm", "DENY", correct, { command: "rm" });
|
|
434
|
-
await run("permit allows ls", "ALLOW", correct, { command: "ls" });
|
|
435
|
-
const broken = policySetFromSource(
|
|
436
|
-
'forbid(principal, action, resource) when { context.command in ["rm", "dd"] };\npermit(principal, action, resource);'
|
|
437
|
-
);
|
|
438
|
-
await run("in-on-String forbid does not permit-all", "DENY", broken, { command: "rm" });
|
|
439
|
-
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
239
|
// src/http-server.ts
|
|
443
240
|
import { createServer } from "http";
|
|
444
|
-
import { readFileSync as
|
|
445
|
-
import { join
|
|
241
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
242
|
+
import { join } from "path";
|
|
446
243
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
447
244
|
var MAX_RECEIPTS = 100;
|
|
448
245
|
var ReceiptBuffer = class {
|
|
@@ -535,13 +332,13 @@ function handleHealth(res, startTime, config) {
|
|
|
535
332
|
}));
|
|
536
333
|
}
|
|
537
334
|
function handleStatus(res, logDir) {
|
|
538
|
-
const logPath =
|
|
539
|
-
if (!
|
|
335
|
+
const logPath = join(logDir, LOG_FILE);
|
|
336
|
+
if (!existsSync2(logPath)) {
|
|
540
337
|
res.writeHead(200);
|
|
541
338
|
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
542
339
|
return;
|
|
543
340
|
}
|
|
544
|
-
const raw =
|
|
341
|
+
const raw = readFileSync3(logPath, "utf-8");
|
|
545
342
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
546
343
|
const entries = [];
|
|
547
344
|
for (const line of lines) {
|
|
@@ -665,7 +462,7 @@ function handleListApprovals(res, approvalStore) {
|
|
|
665
462
|
}
|
|
666
463
|
|
|
667
464
|
// src/action-readback.ts
|
|
668
|
-
import { createHash as
|
|
465
|
+
import { createHash as createHash2 } from "crypto";
|
|
669
466
|
var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|session|token|private[_-]?key)/i;
|
|
670
467
|
var DESTINATION_KEYS = [
|
|
671
468
|
"path",
|
|
@@ -745,7 +542,7 @@ function buildActionReadback(tool, input) {
|
|
|
745
542
|
action,
|
|
746
543
|
destination,
|
|
747
544
|
payload_preview: payloadPreview,
|
|
748
|
-
payload_hash:
|
|
545
|
+
payload_hash: createHash2("sha256").update(canonical).digest("hex"),
|
|
749
546
|
payload_bytes: Buffer.byteLength(canonical, "utf-8"),
|
|
750
547
|
disclosed_fields: [...new Set(disclosedFields)].slice(0, 80),
|
|
751
548
|
redacted_fields: [...new Set(redactedFields)].slice(0, 80),
|
|
@@ -762,11 +559,6 @@ export {
|
|
|
762
559
|
signDecision,
|
|
763
560
|
getSignerInfo,
|
|
764
561
|
isSigningEnabled,
|
|
765
|
-
loadCedarPolicies,
|
|
766
|
-
evaluateCedar,
|
|
767
|
-
isCedarAvailable,
|
|
768
|
-
policySetFromSource,
|
|
769
|
-
runEvaluatorSelfTest,
|
|
770
562
|
ReceiptBuffer,
|
|
771
563
|
startStatusServer,
|
|
772
564
|
buildActionReadback
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
meetsMinTier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YM6SOJBR.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-G6X763MH.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/cedar-evaluator.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
4
|
+
import { join, extname } from "path";
|
|
5
|
+
var cedarWasm = null;
|
|
6
|
+
var loadAttempted = false;
|
|
7
|
+
async function ensureCedarWasm() {
|
|
8
|
+
if (cedarWasm) return true;
|
|
9
|
+
if (loadAttempted) return false;
|
|
10
|
+
loadAttempted = true;
|
|
11
|
+
try {
|
|
12
|
+
const moduleName = "@cedar-policy/cedar-wasm";
|
|
13
|
+
cedarWasm = await import(
|
|
14
|
+
/* @vite-ignore */
|
|
15
|
+
moduleName
|
|
16
|
+
);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function loadCedarPolicies(dirPath) {
|
|
23
|
+
if (!existsSync(dirPath)) {
|
|
24
|
+
throw new Error(`Cedar policy directory not found: ${dirPath}`);
|
|
25
|
+
}
|
|
26
|
+
const entries = readdirSync(dirPath).filter((f) => extname(f) === ".cedar").sort();
|
|
27
|
+
if (entries.length === 0) {
|
|
28
|
+
throw new Error(`No .cedar files found in: ${dirPath}`);
|
|
29
|
+
}
|
|
30
|
+
const sources = [];
|
|
31
|
+
for (const file of entries) {
|
|
32
|
+
const content = readFileSync(join(dirPath, file), "utf-8");
|
|
33
|
+
sources.push(content);
|
|
34
|
+
}
|
|
35
|
+
const concatenated = sources.join("\n\n");
|
|
36
|
+
const digest = createHash("sha256").update(concatenated).digest("hex").slice(0, 16);
|
|
37
|
+
return {
|
|
38
|
+
source: concatenated,
|
|
39
|
+
digest,
|
|
40
|
+
fileCount: entries.length,
|
|
41
|
+
files: entries
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildEntities(req) {
|
|
45
|
+
const agentId = req.agentId || req.tier;
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
uid: { type: "Agent", id: agentId },
|
|
49
|
+
attrs: {
|
|
50
|
+
tier: req.tier,
|
|
51
|
+
...req.agentId ? { agent_id: req.agentId } : {}
|
|
52
|
+
},
|
|
53
|
+
parents: []
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
uid: { type: "Tool", id: req.tool },
|
|
57
|
+
attrs: {},
|
|
58
|
+
parents: []
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function onEvalError(reason, failClosed, extra) {
|
|
63
|
+
return {
|
|
64
|
+
allowed: !failClosed,
|
|
65
|
+
reason: failClosed ? reason : `${reason} (observe mode; would DENY under enforcement)`,
|
|
66
|
+
metadata: { error: true, fail_closed: failClosed, would_deny: true, ...extra || {} }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function evaluateCedar(policySet, req, schema, options) {
|
|
70
|
+
const failClosed = options?.failClosed ?? true;
|
|
71
|
+
const available = await ensureCedarWasm();
|
|
72
|
+
if (!available) {
|
|
73
|
+
return onEvalError("cedar_wasm_not_available", failClosed, { fallback: true });
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const agentId = req.agentId || req.tier;
|
|
77
|
+
const context = {
|
|
78
|
+
tier: req.tier,
|
|
79
|
+
...req.context || {}
|
|
80
|
+
};
|
|
81
|
+
if (req.toolInput && Object.keys(req.toolInput).length > 0) {
|
|
82
|
+
context.input = req.toolInput;
|
|
83
|
+
}
|
|
84
|
+
const authRequest = {
|
|
85
|
+
principal: { type: "Agent", id: agentId },
|
|
86
|
+
action: { type: "Action", id: "MCP::Tool::call" },
|
|
87
|
+
resource: { type: "Tool", id: req.tool },
|
|
88
|
+
context
|
|
89
|
+
};
|
|
90
|
+
const entities = buildEntities(req);
|
|
91
|
+
const cedarSchema = schema?.schemaJson ?? null;
|
|
92
|
+
let result;
|
|
93
|
+
if (typeof cedarWasm.isAuthorized === "function") {
|
|
94
|
+
result = cedarWasm.isAuthorized({
|
|
95
|
+
policies: { staticPolicies: policySet.source },
|
|
96
|
+
entities,
|
|
97
|
+
principal: authRequest.principal,
|
|
98
|
+
action: authRequest.action,
|
|
99
|
+
resource: authRequest.resource,
|
|
100
|
+
context: authRequest.context,
|
|
101
|
+
schema: cedarSchema
|
|
102
|
+
});
|
|
103
|
+
} else if (typeof cedarWasm.checkAuthorization === "function") {
|
|
104
|
+
result = cedarWasm.checkAuthorization(
|
|
105
|
+
policySet.source,
|
|
106
|
+
JSON.stringify(entities),
|
|
107
|
+
JSON.stringify(authRequest)
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
111
|
+
if (typeof cedarEngine.isAuthorized === "function") {
|
|
112
|
+
result = cedarEngine.isAuthorized({
|
|
113
|
+
policies: { staticPolicies: policySet.source },
|
|
114
|
+
entities,
|
|
115
|
+
principal: authRequest.principal,
|
|
116
|
+
action: authRequest.action,
|
|
117
|
+
resource: authRequest.resource,
|
|
118
|
+
context: authRequest.context,
|
|
119
|
+
schema: cedarSchema
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
return onEvalError("cedar_wasm_api_unsupported", failClosed, { exports: Object.keys(cedarWasm) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const parsed = parseWasmResult(result);
|
|
126
|
+
const policyErrors = extractPolicyErrors(result);
|
|
127
|
+
if (parsed.kind === "error") {
|
|
128
|
+
return onEvalError(`cedar_unparseable_result: ${parsed.diagnostics}`, failClosed);
|
|
129
|
+
}
|
|
130
|
+
if (policyErrors.length > 0) {
|
|
131
|
+
return onEvalError(
|
|
132
|
+
`cedar_policy_errored: ${policyErrors.length} policy error(s); decision is unsound`,
|
|
133
|
+
failClosed,
|
|
134
|
+
{ policy_errors: policyErrors.slice(0, 5), policy_digest: policySet.digest }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
allowed: parsed.kind === "allow",
|
|
139
|
+
reason: parsed.kind === "allow" ? void 0 : `cedar_deny${parsed.diagnostics ? ": " + parsed.diagnostics : ""}`,
|
|
140
|
+
metadata: {
|
|
141
|
+
policy_digest: policySet.digest,
|
|
142
|
+
...parsed.matchedPolicies ? { matched_policies: parsed.matchedPolicies } : {}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return onEvalError(`cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`, failClosed);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function parseWasmResult(result) {
|
|
150
|
+
if (!result) return { kind: "error", diagnostics: "null result from Cedar WASM" };
|
|
151
|
+
if (result.type === "failure") {
|
|
152
|
+
return { kind: "error", diagnostics: `cedar failure: ${JSON.stringify(result.errors ?? [])}` };
|
|
153
|
+
}
|
|
154
|
+
if (result.type === "success" && result.response) {
|
|
155
|
+
const dec = result.response.decision;
|
|
156
|
+
const reasons = result.response.diagnostics?.reason;
|
|
157
|
+
if (dec === "allow" || dec === "Allow") return { kind: "allow", matchedPolicies: reasons };
|
|
158
|
+
if (dec === "deny" || dec === "Deny") {
|
|
159
|
+
return { kind: "deny", diagnostics: result.response.diagnostics ? JSON.stringify(result.response.diagnostics) : void 0, matchedPolicies: reasons };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (result.type === "allow" || result.decision === "Allow") return { kind: "allow" };
|
|
163
|
+
if (result.type === "deny" || result.decision === "Deny") return { kind: "deny" };
|
|
164
|
+
if (typeof result === "boolean") return result ? { kind: "allow" } : { kind: "deny" };
|
|
165
|
+
return { kind: "error", diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
166
|
+
}
|
|
167
|
+
function extractPolicyErrors(result) {
|
|
168
|
+
if (!result || typeof result !== "object") return [];
|
|
169
|
+
const raw = result.errors ?? result.response?.diagnostics?.errors ?? result.diagnostics?.errors ?? [];
|
|
170
|
+
if (!Array.isArray(raw)) return [];
|
|
171
|
+
return raw.map((e) => typeof e === "string" ? e : e?.message ?? e?.error ?? JSON.stringify(e)).filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
async function isCedarAvailable() {
|
|
174
|
+
return ensureCedarWasm();
|
|
175
|
+
}
|
|
176
|
+
function policySetFromSource(source, name = "inline") {
|
|
177
|
+
const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
178
|
+
return { source, digest, fileCount: 1, files: [name] };
|
|
179
|
+
}
|
|
180
|
+
async function runEvaluatorSelfTest() {
|
|
181
|
+
const wasmAvailable = await isCedarAvailable();
|
|
182
|
+
const cases = [];
|
|
183
|
+
const run = async (name, expected, policy, context) => {
|
|
184
|
+
const d = await evaluateCedar(policy, { tool: "Bash", tier: "unknown", context }, void 0, { failClosed: true });
|
|
185
|
+
const actual = d.allowed ? "ALLOW" : "DENY";
|
|
186
|
+
cases.push({ name, expected, actual, pass: actual === expected, reason: d.reason });
|
|
187
|
+
};
|
|
188
|
+
if (!wasmAvailable) {
|
|
189
|
+
await run("engine unavailable denies", "DENY", policySetFromSource("permit(principal, action, resource);"), {});
|
|
190
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
191
|
+
}
|
|
192
|
+
const correct = policySetFromSource(
|
|
193
|
+
'forbid(principal, action, resource) when { ["rm", "dd", "mkfs"].contains(context.command) };\npermit(principal, action, resource);'
|
|
194
|
+
);
|
|
195
|
+
await run("forbid denies rm", "DENY", correct, { command: "rm" });
|
|
196
|
+
await run("permit allows ls", "ALLOW", correct, { command: "ls" });
|
|
197
|
+
const broken = policySetFromSource(
|
|
198
|
+
'forbid(principal, action, resource) when { context.command in ["rm", "dd"] };\npermit(principal, action, resource);'
|
|
199
|
+
);
|
|
200
|
+
await run("in-on-String forbid does not permit-all", "DENY", broken, { command: "rm" });
|
|
201
|
+
return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
loadCedarPolicies,
|
|
206
|
+
evaluateCedar,
|
|
207
|
+
isCedarAvailable,
|
|
208
|
+
policySetFromSource,
|
|
209
|
+
runEvaluatorSelfTest
|
|
210
|
+
};
|
|
@@ -2,13 +2,15 @@ import {
|
|
|
2
2
|
ReceiptBuffer,
|
|
3
3
|
buildActionReadback,
|
|
4
4
|
checkRateLimit,
|
|
5
|
-
evaluateCedar,
|
|
6
5
|
getToolPolicy,
|
|
7
6
|
isSigningEnabled,
|
|
8
7
|
parseRateLimit,
|
|
9
8
|
signDecision,
|
|
10
9
|
startStatusServer
|
|
11
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-G6X763MH.mjs";
|
|
11
|
+
import {
|
|
12
|
+
evaluateCedar
|
|
13
|
+
} from "./chunk-MWXDXYWH.mjs";
|
|
12
14
|
|
|
13
15
|
// src/evidence-store.ts
|
|
14
16
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|