protect-mcp 0.9.6 → 0.10.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +30 -0
  3. package/dist/chunk-3AVIMUNP.mjs +113 -0
  4. package/dist/{chunk-PB3TC7E3.mjs → chunk-622KQ4KW.mjs} +4 -2
  5. package/dist/{chunk-JQDVKZBN.mjs → chunk-HULPW5WR.mjs} +7 -2
  6. package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
  7. package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
  8. package/dist/chunk-MWXDXYWH.mjs +210 -0
  9. package/dist/{chunk-7MHK5RF4.mjs → chunk-SPDIFFGP.mjs} +2 -2
  10. package/dist/{chunk-XLJUZ4WO.mjs → chunk-VWGRAUWI.mjs} +35 -245
  11. package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
  12. package/dist/{chunk-WCZJGEBO.mjs → chunk-ZLZPSUCY.mjs} +29 -9
  13. package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
  14. package/dist/cli.js +550 -2921
  15. package/dist/cli.mjs +48 -35
  16. package/dist/demo-server.js +17 -14
  17. package/dist/demo-server.mjs +1 -1
  18. package/dist/hook-server.js +99 -374
  19. package/dist/hook-server.mjs +5 -6
  20. package/dist/{http-transport-HLSMVBI6.mjs → http-transport-JHGRFCYY.mjs} +4 -2
  21. package/dist/index.d.mts +95 -6
  22. package/dist/index.d.ts +95 -6
  23. package/dist/index.js +615 -3872
  24. package/dist/index.mjs +43 -1134
  25. package/dist/mcp-server.d.mts +24 -0
  26. package/dist/mcp-server.d.ts +24 -0
  27. package/dist/mcp-server.js +555 -0
  28. package/dist/mcp-server.mjs +279 -0
  29. package/dist/{report-5XCNW6FB.mjs → report-N5LPPSPF.mjs} +2 -1
  30. package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
  31. package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
  32. package/package.json +8 -7
  33. package/dist/chunk-AYNQIEN7.mjs +0 -10
  34. package/dist/chunk-D733KAPG.mjs +0 -252
  35. package/dist/chunk-FFVJL3KQ.mjs +0 -2039
  36. package/dist/chunk-JIDDQUSQ.mjs +0 -568
  37. package/dist/ed25519-SQA3S2RV.mjs +0 -39
  38. package/dist/utils-6AYZFE5A.mjs +0 -77
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0: receipts migrate to the draft-02 Acta envelope
4
+
5
+ Receipts are now emitted in the envelope the IETF draft actually specifies
6
+ (draft-farley-acta-signed-receipts-02): a two-field
7
+ `{ payload, signature: { alg: "EdDSA", kid, sig } }` envelope, with the
8
+ access-decision fields from section 3.1 (`type: "protectmcp:decision"`,
9
+ `tool_name`, `decision`, `reason`, `policy_digest`), `issuer_id` equal to
10
+ `signature.kid` (section 2.2), the signature computed as PureEdDSA directly
11
+ over the JCS bytes of the payload with no pre-hash (section 5.6), and new
12
+ keys defaulting to the section 2.1.1 recommended
13
+ `sb:issuer:<base58-fingerprint>` kid format (existing key files keep their
14
+ explicit kid). Previously the package emitted an internal envelope shape
15
+ that predated the published draft.
16
+
17
+ Receipt logs are now hash-chained per section 5.7: each receipt's
18
+ `previousReceiptHash` is the bare-hex SHA-256 of the JCS bytes of the
19
+ previous log line exactly as written, signature included. The hook server
20
+ resumes the chain across restarts from the tail of the existing log, and
21
+ signing-failure tombstones participate in the chain so an unsigned gap
22
+ cannot be silently dropped.
23
+
24
+ Verification is dual-shape everywhere (the `verify_receipt` MCP tool, the
25
+ `serve --enforce` self-test, the embedded record viewer, and the exported
26
+ `verifyReceipt` API): receipts written by protect-mcp 0.9.x and earlier
27
+ (flat v1 artifacts and structured v2 envelopes with a top-level signature
28
+ string) continue to verify, and a mixed pre/post-migration log replays
29
+ cleanly, including the chain link that spans the boundary. Verified against
30
+ 3,352 real receipts from a production gate log: all verify. The published
31
+ `@veritasacta/verify` CLI (0.9.2) verifies the new envelopes as-is via its
32
+ passport path; no verifier upgrade is required.
33
+
34
+ New module `acta-envelope` (exported from the package root):
35
+ `createReceiptEnvelope`, `verifyReceipt`, `receiptHash`, `receiptIdentity`,
36
+ `computeSbIssuerKid`. `signDecision` accepts an optional previous-chain-hash
37
+ argument and returns the emitted receipt's chain hash. `@noble/curves` and
38
+ `@noble/hashes` moved from optional to regular dependencies.
39
+
40
+ Fixed: importing the package as a library no longer attaches the demo
41
+ server's stdin JSON-RPC listener (it now only starts when demo-server is
42
+ the process entry point). Previously any `require("protect-mcp")` kept the
43
+ host process's event loop alive and answered JSON-RPC on stdout.
44
+
45
+ Note for strict draft conformance: `decision` may be "require_approval" for
46
+ held-for-co-sign flows, an extension value beyond the section 3.1 set
47
+ (allow, deny, rate_limit), pending a revision of the draft.
48
+
49
+
50
+ ## 0.9.7: the gate as an MCP server
51
+
52
+ `protect-mcp mcp` boots a stdio MCP server exposing the gate itself as four
53
+ tools, so an agent can call the gate directly instead of only through the
54
+ PreToolUse/PostToolUse hooks: `evaluate_action` (Cedar decision, fail-closed),
55
+ `sign_decision` (Ed25519 receipt, byte-compatible with the runtime gate's),
56
+ `verify_receipt` (offline check), and `self_test` (proves a known-forbidden
57
+ action is denied and a signed receipt round-trips). All four are read-only
58
+ and annotated with what they return. A bare-spawnable bin,
59
+ `protect-mcp-mcp`, is included alongside the `mcp` subcommand so any MCP
60
+ host or registry can point at either.
61
+
3
62
  ## 0.9.6: policy you can see and change
4
63
 
5
64
  The gate is default-deny and fail-closed, but a deny used to be a dead end: an
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
@@ -0,0 +1,113 @@
1
+ // src/acta-envelope.ts
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { sha256 } from "@noble/hashes/sha256";
4
+ import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
5
+ function canonicalize(obj) {
6
+ return JSON.stringify(obj, (_key, value) => {
7
+ if (value && typeof value === "object" && !Array.isArray(value)) {
8
+ const sorted = {};
9
+ for (const k of Object.keys(value).sort()) {
10
+ if (!/^[\x20-\x7E]*$/.test(k)) {
11
+ throw new Error(`Non-ASCII key "${k}" in receipt payload. Only ASCII keys are permitted.`);
12
+ }
13
+ sorted[k] = value[k];
14
+ }
15
+ return sorted;
16
+ }
17
+ return value;
18
+ });
19
+ }
20
+ function receiptHash(obj) {
21
+ return bytesToHex(sha256(utf8ToBytes(canonicalize(obj))));
22
+ }
23
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
24
+ function base58(bytes) {
25
+ let n = BigInt("0x" + bytesToHex(bytes));
26
+ let out = "";
27
+ while (n > 0n) {
28
+ out = B58_ALPHABET[Number(n % 58n)] + out;
29
+ n /= 58n;
30
+ }
31
+ for (const b of bytes) {
32
+ if (b === 0) out = "1" + out;
33
+ else break;
34
+ }
35
+ return out;
36
+ }
37
+ function computeSbIssuerKid(publicKeyHex) {
38
+ return `sb:issuer:${base58(hexToBytes(publicKeyHex)).slice(0, 12)}`;
39
+ }
40
+ function createReceiptEnvelope(fields, privateKeyHex, kid, issuedAt) {
41
+ if (!fields.type) throw new Error("receipt payload requires a type");
42
+ if (!kid) throw new Error("kid is required");
43
+ const payload = {
44
+ ...fields,
45
+ issued_at: fields.issued_at || issuedAt || (/* @__PURE__ */ new Date()).toISOString(),
46
+ issuer_id: kid
47
+ };
48
+ const sig = bytesToHex(ed25519.sign(utf8ToBytes(canonicalize(payload)), hexToBytes(privateKeyHex)));
49
+ const envelope = { payload, signature: { alg: "EdDSA", kid, sig } };
50
+ return { envelope, hash: receiptHash(envelope) };
51
+ }
52
+ function verifyReceipt(envelope, publicKeyHex) {
53
+ try {
54
+ if (!envelope || typeof envelope !== "object") {
55
+ return { valid: false, shape: null, error: "not_an_object" };
56
+ }
57
+ const env = envelope;
58
+ const signature = env.signature;
59
+ if (signature && typeof signature === "object" && !Array.isArray(signature)) {
60
+ const sigObj = signature;
61
+ if (sigObj.alg !== "EdDSA") {
62
+ return { valid: false, shape: "acta-02", error: `unsupported_alg:${String(sigObj.alg)}` };
63
+ }
64
+ if (typeof sigObj.sig !== "string" || !env.payload || typeof env.payload !== "object") {
65
+ return { valid: false, shape: "acta-02", error: "malformed_envelope" };
66
+ }
67
+ const message = utf8ToBytes(canonicalize(env.payload));
68
+ const valid = ed25519.verify(hexToBytes(sigObj.sig), message, hexToBytes(publicKeyHex));
69
+ return valid ? { valid: true, shape: "acta-02", hash: receiptHash(env) } : { valid: false, shape: "acta-02", error: "invalid_signature" };
70
+ }
71
+ if (typeof signature === "string") {
72
+ const rest = {};
73
+ for (const k of Object.keys(env)) if (k !== "signature") rest[k] = env[k];
74
+ const message = utf8ToBytes(canonicalize(rest));
75
+ const valid = ed25519.verify(hexToBytes(signature), message, hexToBytes(publicKeyHex));
76
+ const shape = env.v === 2 ? "legacy-v2" : "legacy-v1";
77
+ return valid ? { valid: true, shape, hash: receiptHash(env) } : { valid: false, shape, error: "invalid_signature" };
78
+ }
79
+ return { valid: false, shape: null, error: "missing_signature" };
80
+ } catch (err) {
81
+ return {
82
+ valid: false,
83
+ shape: null,
84
+ error: `verification_error:${err instanceof Error ? err.message : "unknown"}`
85
+ };
86
+ }
87
+ }
88
+ function receiptIdentity(envelope) {
89
+ if (!envelope || typeof envelope !== "object") return { kid: null, issuer: null, type: null };
90
+ const env = envelope;
91
+ if (env.signature && typeof env.signature === "object") {
92
+ const payload = env.payload || {};
93
+ const sig = env.signature;
94
+ return {
95
+ kid: typeof sig.kid === "string" ? sig.kid : null,
96
+ issuer: typeof payload.issuer_id === "string" ? payload.issuer_id : typeof payload.issuer_name === "string" ? payload.issuer_name : null,
97
+ type: typeof payload.type === "string" ? payload.type : null
98
+ };
99
+ }
100
+ return {
101
+ kid: typeof env.kid === "string" ? env.kid : null,
102
+ issuer: typeof env.issuer === "string" ? env.issuer : null,
103
+ type: typeof env.type === "string" ? env.type : null
104
+ };
105
+ }
106
+
107
+ export {
108
+ receiptHash,
109
+ computeSbIssuerKid,
110
+ createReceiptEnvelope,
111
+ verifyReceipt,
112
+ receiptIdentity
113
+ };
@@ -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-XLJUZ4WO.mjs";
10
+ } from "./chunk-VWGRAUWI.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";
@@ -1,3 +1,7 @@
1
+ import {
2
+ receiptIdentity
3
+ } from "./chunk-3AVIMUNP.mjs";
4
+
1
5
  // src/report.ts
2
6
  import { readFileSync, existsSync } from "fs";
3
7
  function generateReport(logPath, receiptPath, periodDays) {
@@ -34,8 +38,9 @@ function generateReport(logPath, receiptPath, periodDays) {
34
38
  const parsed = JSON.parse(trimmed);
35
39
  if (parsed.signature) {
36
40
  receiptsSigned++;
37
- if (parsed.kid && !signerKid) signerKid = parsed.kid;
38
- if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
41
+ const identity = receiptIdentity(parsed);
42
+ if (identity.kid && !signerKid) signerKid = identity.kid;
43
+ if (identity.issuer && !signerIssuer) signerIssuer = identity.issuer;
39
44
  }
40
45
  } catch {
41
46
  }
@@ -1,22 +1,11 @@
1
- import {
2
- sha256
3
- } from "./chunk-AYNQIEN7.mjs";
4
- import {
5
- ed25519
6
- } from "./chunk-FFVJL3KQ.mjs";
7
- import {
8
- Hash,
9
- abytes,
10
- aexists,
11
- ahash,
12
- bytesToHex,
13
- clean,
14
- hexToBytes,
15
- randomBytes,
16
- toBytes
17
- } from "./chunk-D733KAPG.mjs";
1
+ // src/signing-committed.ts
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { sha256 as sha2563 } from "@noble/hashes/sha256";
4
+ import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3, randomBytes as randomBytes2 } from "@noble/hashes/utils";
18
5
 
19
6
  // src/commitments/merkle.ts
7
+ import { sha256 } from "@noble/hashes/sha256";
8
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
20
9
  var DOMAIN_LEAF = 0;
21
10
  var DOMAIN_INTERNAL = 1;
22
11
  function hashLeaf(leafBytes) {
@@ -127,75 +116,10 @@ function largestPowerOfTwoLessThan(n) {
127
116
  return k;
128
117
  }
129
118
 
130
- // node_modules/@noble/hashes/esm/hmac.js
131
- var HMAC = class extends Hash {
132
- constructor(hash, _key) {
133
- super();
134
- this.finished = false;
135
- this.destroyed = false;
136
- ahash(hash);
137
- const key = toBytes(_key);
138
- this.iHash = hash.create();
139
- if (typeof this.iHash.update !== "function")
140
- throw new Error("Expected instance of class which extends utils.Hash");
141
- this.blockLen = this.iHash.blockLen;
142
- this.outputLen = this.iHash.outputLen;
143
- const blockLen = this.blockLen;
144
- const pad = new Uint8Array(blockLen);
145
- pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
146
- for (let i = 0; i < pad.length; i++)
147
- pad[i] ^= 54;
148
- this.iHash.update(pad);
149
- this.oHash = hash.create();
150
- for (let i = 0; i < pad.length; i++)
151
- pad[i] ^= 54 ^ 92;
152
- this.oHash.update(pad);
153
- clean(pad);
154
- }
155
- update(buf) {
156
- aexists(this);
157
- this.iHash.update(buf);
158
- return this;
159
- }
160
- digestInto(out) {
161
- aexists(this);
162
- abytes(out, this.outputLen);
163
- this.finished = true;
164
- this.iHash.digestInto(out);
165
- this.oHash.update(out);
166
- this.oHash.digestInto(out);
167
- this.destroy();
168
- }
169
- digest() {
170
- const out = new Uint8Array(this.oHash.outputLen);
171
- this.digestInto(out);
172
- return out;
173
- }
174
- _cloneInto(to) {
175
- to || (to = Object.create(Object.getPrototypeOf(this), {}));
176
- const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
177
- to = to;
178
- to.finished = finished;
179
- to.destroyed = destroyed;
180
- to.blockLen = blockLen;
181
- to.outputLen = outputLen;
182
- to.oHash = oHash._cloneInto(to.oHash);
183
- to.iHash = iHash._cloneInto(to.iHash);
184
- return to;
185
- }
186
- clone() {
187
- return this._cloneInto();
188
- }
189
- destroy() {
190
- this.destroyed = true;
191
- this.oHash.destroy();
192
- this.iHash.destroy();
193
- }
194
- };
195
- var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
196
- hmac.create = (hash, key) => new HMAC(hash, key);
197
-
198
119
  // src/commitments/primitives.ts
120
+ import { sha256 as sha2562 } from "@noble/hashes/sha256";
121
+ import { hmac } from "@noble/hashes/hmac";
122
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2, randomBytes } from "@noble/hashes/utils";
199
123
  function jcs(value) {
200
124
  if (value === null || value === void 0) return "null";
201
125
  if (typeof value === "boolean" || typeof value === "number")
@@ -257,7 +181,7 @@ function leavesFromFields(fields) {
257
181
 
258
182
  // src/signing-committed.ts
259
183
  function freshSalt() {
260
- return randomBytes(32);
184
+ return randomBytes2(32);
261
185
  }
262
186
  function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
263
187
  const allFields = {
@@ -297,7 +221,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
297
221
  const { sorted, leafBytes } = leavesFromFields(committedFields);
298
222
  const leafHashes = leafBytes.map(hashLeaf);
299
223
  const root = merkleRoot(leafHashes);
300
- committedFieldsRoot = bytesToHex(root);
224
+ committedFieldsRoot = bytesToHex3(root);
301
225
  sorted.forEach((f, i) => {
302
226
  openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
303
227
  });
@@ -314,8 +238,8 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
314
238
  payload.committed_field_names = committedFields.map((f) => f.name);
315
239
  }
316
240
  const canonical = jcs(payload);
317
- const messageHash = sha256(new TextEncoder().encode(canonical));
318
- const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
241
+ const messageHash = sha2563(new TextEncoder().encode(canonical));
242
+ const signatureBytes = ed25519.sign(messageHash, hexToBytes3(signingKey));
319
243
  const signedReceipt = {
320
244
  ...payload,
321
245
  signature: {
@@ -328,7 +252,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
328
252
  }
329
253
  };
330
254
  const signedJson = JSON.stringify(signedReceipt);
331
- const receiptHash = bytesToHex(sha256(new TextEncoder().encode(jcs(signedReceipt))));
255
+ const receiptHash = bytesToHex3(sha2563(new TextEncoder().encode(jcs(signedReceipt))));
332
256
  return {
333
257
  signed: signedJson,
334
258
  artifact_type: "decision_receipt_committed_v1",
@@ -457,7 +381,7 @@ function committedFieldNamesFromReceipt(receipt, openings) {
457
381
  return Array.from(new Set(names)).sort();
458
382
  }
459
383
  function receiptHashHex(receipt) {
460
- return bytesToHex(sha256(new TextEncoder().encode(jcs(receipt))));
384
+ return bytesToHex3(sha2563(new TextEncoder().encode(jcs(receipt))));
461
385
  }
462
386
  function verifyCommittedReceiptSignature(receipt) {
463
387
  const signature = receipt.signature;
@@ -467,16 +391,15 @@ function verifyCommittedReceiptSignature(receipt) {
467
391
  return null;
468
392
  }
469
393
  const { signature: _signature, ...payloadWithoutSig } = receipt;
470
- const messageHash = sha256(new TextEncoder().encode(jcs(payloadWithoutSig)));
394
+ const messageHash = sha2563(new TextEncoder().encode(jcs(payloadWithoutSig)));
471
395
  try {
472
- return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
396
+ return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes3(sig.public_key));
473
397
  } catch {
474
398
  return false;
475
399
  }
476
400
  }
477
401
 
478
402
  export {
479
- hmac,
480
403
  signCommittedDecision,
481
404
  discloseField,
482
405
  createSelectiveDisclosurePackage,
@@ -1,11 +1,6 @@
1
- import {
2
- sha256
3
- } from "./chunk-AYNQIEN7.mjs";
4
- import {
5
- bytesToHex
6
- } from "./chunk-D733KAPG.mjs";
7
-
8
1
  // src/receipt-enrichment.ts
2
+ import { sha256 } from "@noble/hashes/sha256";
3
+ import { bytesToHex } from "@noble/hashes/utils";
9
4
  var ENRICHMENT_VERSION = 2;
10
5
  function canonicalJson(value) {
11
6
  const seen = /* @__PURE__ */ new WeakSet();
@@ -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
+ };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-PB3TC7E3.mjs";
3
+ } from "./chunk-622KQ4KW.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-XLJUZ4WO.mjs";
8
+ } from "./chunk-VWGRAUWI.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";