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.
- package/CHANGELOG.md +59 -0
- package/README.md +30 -0
- package/dist/chunk-3AVIMUNP.mjs +113 -0
- package/dist/{chunk-PB3TC7E3.mjs → chunk-622KQ4KW.mjs} +4 -2
- package/dist/{chunk-JQDVKZBN.mjs → chunk-HULPW5WR.mjs} +7 -2
- package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
- package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
- package/dist/chunk-MWXDXYWH.mjs +210 -0
- package/dist/{chunk-7MHK5RF4.mjs → chunk-SPDIFFGP.mjs} +2 -2
- package/dist/{chunk-XLJUZ4WO.mjs → chunk-VWGRAUWI.mjs} +35 -245
- package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
- package/dist/{chunk-WCZJGEBO.mjs → chunk-ZLZPSUCY.mjs} +29 -9
- package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
- package/dist/cli.js +550 -2921
- package/dist/cli.mjs +48 -35
- package/dist/demo-server.js +17 -14
- package/dist/demo-server.mjs +1 -1
- package/dist/hook-server.js +99 -374
- package/dist/hook-server.mjs +5 -6
- package/dist/{http-transport-HLSMVBI6.mjs → http-transport-JHGRFCYY.mjs} +4 -2
- package/dist/index.d.mts +95 -6
- package/dist/index.d.ts +95 -6
- package/dist/index.js +615 -3872
- package/dist/index.mjs +43 -1134
- package/dist/mcp-server.d.mts +24 -0
- package/dist/mcp-server.d.ts +24 -0
- package/dist/mcp-server.js +555 -0
- package/dist/mcp-server.mjs +279 -0
- package/dist/{report-5XCNW6FB.mjs → report-N5LPPSPF.mjs} +2 -1
- package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
- package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
- package/package.json +8 -7
- package/dist/chunk-AYNQIEN7.mjs +0 -10
- package/dist/chunk-D733KAPG.mjs +0 -252
- package/dist/chunk-FFVJL3KQ.mjs +0 -2039
- package/dist/chunk-JIDDQUSQ.mjs +0 -568
- package/dist/ed25519-SQA3S2RV.mjs +0 -39
- 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-
|
|
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
|
-
|
|
38
|
-
if (
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
} from "
|
|
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
|
|
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 =
|
|
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 =
|
|
318
|
-
const signatureBytes = ed25519.sign(messageHash,
|
|
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 =
|
|
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
|
|
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 =
|
|
394
|
+
const messageHash = sha2563(new TextEncoder().encode(jcs(payloadWithoutSig)));
|
|
471
395
|
try {
|
|
472
|
-
return ed25519.verify(base64urlDecode(sig.sig), messageHash,
|
|
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-
|
|
3
|
+
} from "./chunk-622KQ4KW.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-VWGRAUWI.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|