protect-mcp 0.9.7 → 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/dist/chunk-3AVIMUNP.mjs +113 -0
- package/dist/{chunk-YM6SOJBR.mjs → chunk-622KQ4KW.mjs} +1 -1
- 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-MOXINIMB.mjs → chunk-SPDIFFGP.mjs} +2 -2
- package/dist/{chunk-G6X763MH.mjs → chunk-VWGRAUWI.mjs} +28 -30
- package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
- package/dist/{chunk-AUUAVWQM.mjs → chunk-ZLZPSUCY.mjs} +24 -6
- package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
- package/dist/cli.js +289 -2936
- package/dist/cli.mjs +37 -31
- 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 +4 -6
- package/dist/{http-transport-VHD3YBT5.mjs → http-transport-JHGRFCYY.mjs} +3 -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 +37 -1130
- package/dist/mcp-server.js +123 -13
- package/dist/mcp-server.mjs +23 -13
- 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 +5 -5
- 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
|
|
@@ -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
|
+
};
|
|
@@ -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();
|
|
@@ -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";
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeSbIssuerKid,
|
|
3
|
+
createReceiptEnvelope
|
|
4
|
+
} from "./chunk-3AVIMUNP.mjs";
|
|
5
|
+
|
|
1
6
|
// src/policy.ts
|
|
2
7
|
import { createHash } from "crypto";
|
|
3
8
|
import { readFileSync } from "fs";
|
|
@@ -77,13 +82,11 @@ function checkRateLimit(key, limit, store) {
|
|
|
77
82
|
// src/signing.ts
|
|
78
83
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
79
84
|
var signerState = null;
|
|
80
|
-
var artifactsModule = null;
|
|
81
85
|
var signingConfigured = false;
|
|
82
86
|
var signingInitError = null;
|
|
83
87
|
async function initSigning(config) {
|
|
84
88
|
const warnings = [];
|
|
85
89
|
signerState = null;
|
|
86
|
-
artifactsModule = null;
|
|
87
90
|
signingConfigured = Boolean(config && config.enabled !== false);
|
|
88
91
|
signingInitError = null;
|
|
89
92
|
if (!config || config.enabled === false) {
|
|
@@ -112,32 +115,22 @@ async function initSigning(config) {
|
|
|
112
115
|
warnings.push(`signing: ${signingInitError}`);
|
|
113
116
|
return warnings;
|
|
114
117
|
}
|
|
115
|
-
try {
|
|
116
|
-
const moduleName = "@veritasacta/artifacts";
|
|
117
|
-
artifactsModule = await import(
|
|
118
|
-
/* @vite-ignore */
|
|
119
|
-
moduleName
|
|
120
|
-
);
|
|
121
|
-
} catch {
|
|
122
|
-
signingInitError = "@veritasacta/artifacts not available";
|
|
123
|
-
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
124
|
-
return warnings;
|
|
125
|
-
}
|
|
126
118
|
try {
|
|
127
119
|
signerState = {
|
|
128
120
|
privateKey: keyData.privateKey,
|
|
129
121
|
publicKey: keyData.publicKey,
|
|
130
|
-
|
|
122
|
+
// kid is opaque per draft-02; existing key files keep their explicit kid,
|
|
123
|
+
// and keys without one get the s2.1.1 RECOMMENDED sb:issuer format.
|
|
124
|
+
kid: keyData.kid || computeSbIssuerKid(keyData.publicKey),
|
|
131
125
|
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
132
126
|
};
|
|
133
127
|
} catch (err) {
|
|
134
128
|
signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
|
|
135
|
-
artifactsModule = null;
|
|
136
129
|
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
137
130
|
}
|
|
138
131
|
return warnings;
|
|
139
132
|
}
|
|
140
|
-
function signDecision(entry) {
|
|
133
|
+
function signDecision(entry, prevReceiptHash) {
|
|
141
134
|
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
142
135
|
if (signingConfigured && signingInitError) {
|
|
143
136
|
return {
|
|
@@ -148,7 +141,7 @@ function signDecision(entry) {
|
|
|
148
141
|
error: signingInitError
|
|
149
142
|
};
|
|
150
143
|
}
|
|
151
|
-
if (signingConfigured &&
|
|
144
|
+
if (signingConfigured && !signerState) {
|
|
152
145
|
const error = "signing was configured but no signer is ready";
|
|
153
146
|
return {
|
|
154
147
|
ok: false,
|
|
@@ -158,21 +151,24 @@ function signDecision(entry) {
|
|
|
158
151
|
error
|
|
159
152
|
};
|
|
160
153
|
}
|
|
161
|
-
if (!signerState
|
|
154
|
+
if (!signerState) {
|
|
162
155
|
return { ok: false, signed: null, artifact_type: "none" };
|
|
163
156
|
}
|
|
164
157
|
try {
|
|
165
158
|
const payload = {
|
|
166
|
-
|
|
159
|
+
// draft-02 s3.1 access-decision fields
|
|
160
|
+
type: "protectmcp:decision",
|
|
161
|
+
tool_name: entry.tool,
|
|
167
162
|
decision: entry.decision,
|
|
168
|
-
|
|
163
|
+
reason: entry.reason_code,
|
|
169
164
|
policy_digest: entry.policy_digest,
|
|
165
|
+
// Extension fields (signed alongside the s3.1 core)
|
|
170
166
|
scope: entry.request_id,
|
|
171
167
|
// request scope
|
|
172
168
|
mode: entry.mode,
|
|
173
169
|
request_id: entry.request_id,
|
|
174
170
|
// Spec version: ties every receipt to the IETF standard
|
|
175
|
-
spec: "draft-farley-acta-signed-receipts-
|
|
171
|
+
spec: "draft-farley-acta-signed-receipts-02",
|
|
176
172
|
// Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
|
|
177
173
|
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
178
174
|
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
@@ -185,6 +181,10 @@ function signDecision(entry) {
|
|
|
185
181
|
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
186
182
|
public_key: signerState.publicKey
|
|
187
183
|
};
|
|
184
|
+
if (signerState.issuer && signerState.issuer !== signerState.kid) {
|
|
185
|
+
payload.issuer_name = signerState.issuer;
|
|
186
|
+
}
|
|
187
|
+
if (prevReceiptHash) payload.previousReceiptHash = prevReceiptHash;
|
|
188
188
|
if (entry.tier) payload.tier = entry.tier;
|
|
189
189
|
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
190
190
|
if (entry.rate_limit_remaining !== void 0) {
|
|
@@ -199,19 +199,17 @@ function signDecision(entry) {
|
|
|
199
199
|
if (entry.enrichment) payload.enrichment = entry.enrichment;
|
|
200
200
|
if (entry.action_readback) payload.action_readback = entry.action_readback;
|
|
201
201
|
if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
|
|
202
|
-
const result =
|
|
203
|
-
artifactType,
|
|
202
|
+
const result = createReceiptEnvelope(
|
|
204
203
|
payload,
|
|
205
204
|
signerState.privateKey,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
issuer: signerState.issuer
|
|
209
|
-
}
|
|
205
|
+
signerState.kid,
|
|
206
|
+
Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : void 0
|
|
210
207
|
);
|
|
211
208
|
return {
|
|
212
209
|
ok: true,
|
|
213
|
-
signed: JSON.stringify(result.
|
|
214
|
-
artifact_type: artifactType
|
|
210
|
+
signed: JSON.stringify(result.envelope),
|
|
211
|
+
artifact_type: artifactType,
|
|
212
|
+
receipt_hash: result.hash
|
|
215
213
|
};
|
|
216
214
|
} catch (err) {
|
|
217
215
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
@@ -233,7 +231,7 @@ function getSignerInfo() {
|
|
|
233
231
|
};
|
|
234
232
|
}
|
|
235
233
|
function isSigningEnabled() {
|
|
236
|
-
return signingConfigured && signingInitError === null && signerState !== null
|
|
234
|
+
return signingConfigured && signingInitError === null && signerState !== null;
|
|
237
235
|
}
|
|
238
236
|
|
|
239
237
|
// src/http-server.ts
|
|
@@ -35158,20 +35158,23 @@ Contents: Hello from protect-mcp demo server!`;
|
|
|
35158
35158
|
}
|
|
35159
35159
|
return "";
|
|
35160
35160
|
}
|
|
35161
|
-
var
|
|
35162
|
-
|
|
35163
|
-
const
|
|
35164
|
-
|
|
35165
|
-
|
|
35166
|
-
|
|
35167
|
-
|
|
35168
|
-
|
|
35169
|
-
|
|
35170
|
-
|
|
35171
|
-
|
|
35172
|
-
|
|
35173
|
-
}
|
|
35174
|
-
|
|
35161
|
+
var isMainModule = /demo-server\.(js|cjs|mjs|ts)$/.test(process.argv[1] || "");
|
|
35162
|
+
if (isMainModule) {
|
|
35163
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
35164
|
+
rl.on("line", (line) => {
|
|
35165
|
+
const trimmed = line.trim();
|
|
35166
|
+
if (!trimmed) return;
|
|
35167
|
+
try {
|
|
35168
|
+
const request = JSON.parse(trimmed);
|
|
35169
|
+
const response = handleRequest(request);
|
|
35170
|
+
if (response) {
|
|
35171
|
+
process.stdout.write(response + "\n");
|
|
35172
|
+
}
|
|
35173
|
+
} catch {
|
|
35174
|
+
}
|
|
35175
|
+
});
|
|
35176
|
+
process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
|
|
35177
|
+
}
|
|
35175
35178
|
function createSandboxServer() {
|
|
35176
35179
|
const { McpServer } = require_mcp();
|
|
35177
35180
|
const { z } = require_zod();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildEnrichment
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KRKZ2YX7.mjs";
|
|
4
4
|
import {
|
|
5
5
|
ReceiptBuffer,
|
|
6
6
|
buildActionReadback,
|
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
loadPolicy,
|
|
13
13
|
parseRateLimit,
|
|
14
14
|
signDecision
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-VWGRAUWI.mjs";
|
|
16
16
|
import {
|
|
17
17
|
evaluateCedar,
|
|
18
18
|
isCedarAvailable,
|
|
19
19
|
loadCedarPolicies
|
|
20
20
|
} from "./chunk-MWXDXYWH.mjs";
|
|
21
|
+
import {
|
|
22
|
+
receiptHash
|
|
23
|
+
} from "./chunk-3AVIMUNP.mjs";
|
|
21
24
|
|
|
22
25
|
// src/hook-server.ts
|
|
23
26
|
import { createServer } from "http";
|
|
@@ -181,6 +184,16 @@ var DEFAULT_PORT = 9377;
|
|
|
181
184
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
182
185
|
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
183
186
|
var PAYLOAD_HASH_THRESHOLD = 1024;
|
|
187
|
+
function resumeReceiptChain(receiptFilePath) {
|
|
188
|
+
try {
|
|
189
|
+
if (!existsSync(receiptFilePath)) return null;
|
|
190
|
+
const lines = readFileSync(receiptFilePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
191
|
+
if (lines.length === 0) return null;
|
|
192
|
+
return receiptHash(JSON.parse(lines[lines.length - 1]));
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
184
197
|
function detectSwarmContext() {
|
|
185
198
|
const teamName = process.env.CLAUDE_CODE_TEAM_NAME;
|
|
186
199
|
const agentId = process.env.CLAUDE_CODE_AGENT_ID;
|
|
@@ -683,10 +696,11 @@ function emitDecisionLog(state, entry) {
|
|
|
683
696
|
} catch {
|
|
684
697
|
}
|
|
685
698
|
if (isSigningEnabled()) {
|
|
686
|
-
const signed = signDecision(log);
|
|
699
|
+
const signed = signDecision(log, state.lastReceiptHash || void 0);
|
|
687
700
|
if (signed.signed) {
|
|
688
701
|
try {
|
|
689
702
|
appendFileSync(state.receiptFilePath, signed.signed + "\n");
|
|
703
|
+
if (signed.receipt_hash) state.lastReceiptHash = signed.receipt_hash;
|
|
690
704
|
} catch {
|
|
691
705
|
}
|
|
692
706
|
state.receiptBuffer.add(log.request_id, signed.signed);
|
|
@@ -701,16 +715,19 @@ function emitDecisionLog(state, entry) {
|
|
|
701
715
|
`);
|
|
702
716
|
}
|
|
703
717
|
} else if (signed.error) {
|
|
704
|
-
const
|
|
718
|
+
const tombstoneObj = {
|
|
705
719
|
type: "scopeblind.signing_failure.v1",
|
|
706
720
|
request_id: log.request_id,
|
|
707
721
|
tool: log.tool,
|
|
708
722
|
decision: log.decision,
|
|
709
723
|
error: signed.error,
|
|
710
|
-
at: new Date(log.timestamp).toISOString()
|
|
711
|
-
|
|
724
|
+
at: new Date(log.timestamp).toISOString(),
|
|
725
|
+
...state.lastReceiptHash ? { previousReceiptHash: state.lastReceiptHash } : {}
|
|
726
|
+
};
|
|
727
|
+
const tombstone = JSON.stringify(tombstoneObj);
|
|
712
728
|
try {
|
|
713
729
|
appendFileSync(state.receiptFilePath, tombstone + "\n");
|
|
730
|
+
state.lastReceiptHash = receiptHash(tombstoneObj);
|
|
714
731
|
} catch {
|
|
715
732
|
}
|
|
716
733
|
process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
|
|
@@ -826,6 +843,7 @@ async function startHookServer(options = {}) {
|
|
|
826
843
|
policyDigest,
|
|
827
844
|
logFilePath: join(process.cwd(), LOG_FILE),
|
|
828
845
|
receiptFilePath: join(process.cwd(), RECEIPTS_FILE),
|
|
846
|
+
lastReceiptHash: resumeReceiptChain(join(process.cwd(), RECEIPTS_FILE)),
|
|
829
847
|
permissionSuggestions: /* @__PURE__ */ new Map(),
|
|
830
848
|
configAlerts: []
|
|
831
849
|
};
|