protect-mcp 0.7.2 → 0.7.4
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 +69 -0
- package/README.md +157 -9
- package/dist/{bundle-XTR3YMPO.mjs → bundle-YUUHAG7O.mjs} +1 -1
- package/dist/chunk-36UID5WY.mjs +662 -0
- package/dist/{chunk-546U3A7R.mjs → chunk-D2RDY2JR.mjs} +92 -1
- package/dist/chunk-F2FKQ4XN.mjs +410 -0
- package/dist/{chunk-J6L4XCTE.mjs → chunk-KPSICBAJ.mjs} +75 -0
- package/dist/{chunk-X63ELMU4.mjs → chunk-NVJHGXXG.mjs} +37 -7
- package/dist/{chunk-5JXFV37Y.mjs → chunk-PM2ZO57M.mjs} +10 -2
- package/dist/{chunk-OHUTUFTC.mjs → chunk-UBZJ3VI2.mjs} +19 -15
- package/dist/cli.js +4034 -502
- package/dist/cli.mjs +2028 -9
- package/dist/demo-server.d.mts +3 -0
- package/dist/demo-server.d.ts +3 -0
- package/dist/demo-server.js +75 -0
- package/dist/demo-server.mjs +1 -1
- package/dist/hook-server.js +144 -25
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-R5AO7X6D.mjs → http-transport-4GMMRPW7.mjs} +2 -2
- package/dist/index.d.mts +115 -1
- package/dist/index.d.ts +115 -1
- package/dist/index.js +988 -80
- package/dist/index.mjs +48 -249
- package/dist/receipt-registry-6CAOY6RP.mjs +279 -0
- package/dist/signing-committed-MMLJ6OGG.mjs +15 -0
- package/package.json +1 -1
- package/dist/chunk-ZBKJANP7.mjs +0 -145
|
@@ -190,6 +190,7 @@ function signDecision(entry) {
|
|
|
190
190
|
if (entry.timing) payload.timing = entry.timing;
|
|
191
191
|
if (entry.swarm) payload.swarm = entry.swarm;
|
|
192
192
|
if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
|
|
193
|
+
if (entry.action_readback) payload.action_readback = entry.action_readback;
|
|
193
194
|
if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
|
|
194
195
|
const result = artifactsModule.createSignedArtifact(
|
|
195
196
|
artifactType,
|
|
@@ -656,6 +657,95 @@ function handleListApprovals(res, approvalStore) {
|
|
|
656
657
|
res.end(JSON.stringify({ grants }));
|
|
657
658
|
}
|
|
658
659
|
|
|
660
|
+
// src/action-readback.ts
|
|
661
|
+
import { createHash as createHash3 } from "crypto";
|
|
662
|
+
var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|session|token|private[_-]?key)/i;
|
|
663
|
+
var DESTINATION_KEYS = [
|
|
664
|
+
"path",
|
|
665
|
+
"file_path",
|
|
666
|
+
"filePath",
|
|
667
|
+
"url",
|
|
668
|
+
"uri",
|
|
669
|
+
"endpoint",
|
|
670
|
+
"host",
|
|
671
|
+
"hostname",
|
|
672
|
+
"repo",
|
|
673
|
+
"repository",
|
|
674
|
+
"branch",
|
|
675
|
+
"channel",
|
|
676
|
+
"to",
|
|
677
|
+
"recipient",
|
|
678
|
+
"symbol",
|
|
679
|
+
"account",
|
|
680
|
+
"bucket",
|
|
681
|
+
"database",
|
|
682
|
+
"table",
|
|
683
|
+
"service"
|
|
684
|
+
];
|
|
685
|
+
function stableStringify(value) {
|
|
686
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
687
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
688
|
+
const obj = value;
|
|
689
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
690
|
+
}
|
|
691
|
+
function redact(value, path = [], redacted = [], disclosed = [], depth = 0) {
|
|
692
|
+
if (depth > 4) return "[truncated-depth]";
|
|
693
|
+
if (value === null || value === void 0) return value;
|
|
694
|
+
if (typeof value !== "object") {
|
|
695
|
+
if (path.length > 0) disclosed.push(path.join("."));
|
|
696
|
+
if (typeof value === "string" && value.length > 240) return `${value.slice(0, 240)}...`;
|
|
697
|
+
return value;
|
|
698
|
+
}
|
|
699
|
+
if (Array.isArray(value)) {
|
|
700
|
+
return value.slice(0, 20).map((item, idx) => redact(item, [...path, String(idx)], redacted, disclosed, depth + 1));
|
|
701
|
+
}
|
|
702
|
+
const out = {};
|
|
703
|
+
for (const [key, child] of Object.entries(value)) {
|
|
704
|
+
const childPath = [...path, key];
|
|
705
|
+
if (SECRET_KEY_RE.test(key)) {
|
|
706
|
+
redacted.push(childPath.join("."));
|
|
707
|
+
out[key] = "[redacted]";
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
out[key] = redact(child, childPath, redacted, disclosed, depth + 1);
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
function firstStringValue(input, keys) {
|
|
715
|
+
for (const key of keys) {
|
|
716
|
+
const value = input[key];
|
|
717
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
718
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
719
|
+
}
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
function actionFor(tool, input) {
|
|
723
|
+
const explicit = firstStringValue(input, ["action", "operation", "method", "verb", "command"]);
|
|
724
|
+
if (explicit) return explicit.length > 90 ? `${explicit.slice(0, 90)}...` : explicit;
|
|
725
|
+
return tool;
|
|
726
|
+
}
|
|
727
|
+
function buildActionReadback(tool, input) {
|
|
728
|
+
const normalized = input && typeof input === "object" && !Array.isArray(input) ? input : { value: input };
|
|
729
|
+
const canonical = stableStringify(normalized);
|
|
730
|
+
const redactedFields = [];
|
|
731
|
+
const disclosedFields = [];
|
|
732
|
+
const payloadPreview = redact(normalized, [], redactedFields, disclosedFields);
|
|
733
|
+
const action = actionFor(tool, normalized);
|
|
734
|
+
const destination = firstStringValue(normalized, DESTINATION_KEYS);
|
|
735
|
+
const summary = destination ? `${tool} -> ${destination}` : `${tool} request`;
|
|
736
|
+
return {
|
|
737
|
+
tool,
|
|
738
|
+
action,
|
|
739
|
+
destination,
|
|
740
|
+
payload_preview: payloadPreview,
|
|
741
|
+
payload_hash: createHash3("sha256").update(canonical).digest("hex"),
|
|
742
|
+
payload_bytes: Buffer.byteLength(canonical, "utf-8"),
|
|
743
|
+
disclosed_fields: [...new Set(disclosedFields)].slice(0, 80),
|
|
744
|
+
redacted_fields: [...new Set(redactedFields)].slice(0, 80),
|
|
745
|
+
summary
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
659
749
|
export {
|
|
660
750
|
loadPolicy,
|
|
661
751
|
getToolPolicy,
|
|
@@ -671,5 +761,6 @@ export {
|
|
|
671
761
|
policySetFromSource,
|
|
672
762
|
runEvaluatorSelfTest,
|
|
673
763
|
ReceiptBuffer,
|
|
674
|
-
startStatusServer
|
|
764
|
+
startStatusServer,
|
|
765
|
+
buildActionReadback
|
|
675
766
|
};
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ed25519,
|
|
3
|
+
sha256
|
|
4
|
+
} from "./chunk-LYKNULYU.mjs";
|
|
5
|
+
import {
|
|
6
|
+
bytesToHex,
|
|
7
|
+
hexToBytes,
|
|
8
|
+
randomBytes
|
|
9
|
+
} from "./chunk-D733KAPG.mjs";
|
|
10
|
+
|
|
11
|
+
// node_modules/@noble/hashes/esm/sha256.js
|
|
12
|
+
var sha2562 = sha256;
|
|
13
|
+
|
|
14
|
+
// src/commitments/merkle.ts
|
|
15
|
+
var DOMAIN_LEAF = 0;
|
|
16
|
+
var DOMAIN_INTERNAL = 1;
|
|
17
|
+
function hashLeaf(leafBytes) {
|
|
18
|
+
const buf = new Uint8Array(leafBytes.length + 1);
|
|
19
|
+
buf[0] = DOMAIN_LEAF;
|
|
20
|
+
buf.set(leafBytes, 1);
|
|
21
|
+
return sha2562(buf);
|
|
22
|
+
}
|
|
23
|
+
function hashInternal(left, right) {
|
|
24
|
+
const buf = new Uint8Array(left.length + right.length + 1);
|
|
25
|
+
buf[0] = DOMAIN_INTERNAL;
|
|
26
|
+
buf.set(left, 1);
|
|
27
|
+
buf.set(right, 1 + left.length);
|
|
28
|
+
return sha2562(buf);
|
|
29
|
+
}
|
|
30
|
+
function merkleRoot(leafHashes) {
|
|
31
|
+
if (leafHashes.length === 0) {
|
|
32
|
+
throw new Error("merkleRoot: cannot compute root of empty leaf set");
|
|
33
|
+
}
|
|
34
|
+
if (leafHashes.length === 1) {
|
|
35
|
+
return leafHashes[0];
|
|
36
|
+
}
|
|
37
|
+
const n = leafHashes.length;
|
|
38
|
+
const k = largestPowerOfTwoLessThan(n);
|
|
39
|
+
const left = merkleRoot(leafHashes.slice(0, k));
|
|
40
|
+
const right = merkleRoot(leafHashes.slice(k));
|
|
41
|
+
return hashInternal(left, right);
|
|
42
|
+
}
|
|
43
|
+
function generateProof(leafHashes, index) {
|
|
44
|
+
if (leafHashes.length === 0) {
|
|
45
|
+
throw new Error("generateProof: empty tree");
|
|
46
|
+
}
|
|
47
|
+
if (index < 0 || index >= leafHashes.length) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`generateProof: index ${index} out of range [0, ${leafHashes.length})`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const siblings = [];
|
|
53
|
+
collectPath(leafHashes, index, siblings);
|
|
54
|
+
return {
|
|
55
|
+
index,
|
|
56
|
+
treeSize: leafHashes.length,
|
|
57
|
+
siblings: siblings.map((s) => bytesToHex(s))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function collectPath(leaves, index, out) {
|
|
61
|
+
if (leaves.length === 1) return;
|
|
62
|
+
const n = leaves.length;
|
|
63
|
+
const k = largestPowerOfTwoLessThan(n);
|
|
64
|
+
if (index < k) {
|
|
65
|
+
collectPath(leaves.slice(0, k), index, out);
|
|
66
|
+
out.push(merkleRoot(leaves.slice(k)));
|
|
67
|
+
} else {
|
|
68
|
+
collectPath(leaves.slice(k), index - k, out);
|
|
69
|
+
out.push(merkleRoot(leaves.slice(0, k)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function verifyProof(expectedRootHex, leafHash, proof) {
|
|
73
|
+
if (proof.index < 0 || proof.index >= proof.treeSize) return false;
|
|
74
|
+
if (proof.treeSize === 1) {
|
|
75
|
+
return proof.siblings.length === 0 && bytesToHex(leafHash).toLowerCase() === expectedRootHex.toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = reconstructRoot(
|
|
80
|
+
leafHash,
|
|
81
|
+
proof.index,
|
|
82
|
+
proof.treeSize,
|
|
83
|
+
proof.siblings
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return bytesToHex(result).toLowerCase() === expectedRootHex.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
function reconstructRoot(leafHash, index, treeSize, siblings) {
|
|
91
|
+
if (treeSize === 1) {
|
|
92
|
+
if (siblings.length !== 0) {
|
|
93
|
+
throw new Error("reconstructRoot: extra siblings at single-leaf level");
|
|
94
|
+
}
|
|
95
|
+
return leafHash;
|
|
96
|
+
}
|
|
97
|
+
if (siblings.length === 0) {
|
|
98
|
+
throw new Error("reconstructRoot: ran out of siblings before single-leaf");
|
|
99
|
+
}
|
|
100
|
+
const k = largestPowerOfTwoLessThan(treeSize);
|
|
101
|
+
const outermostSibling = hexToBytes(siblings[siblings.length - 1]);
|
|
102
|
+
const innerSiblings = siblings.slice(0, -1);
|
|
103
|
+
if (index < k) {
|
|
104
|
+
const leftHash = reconstructRoot(leafHash, index, k, innerSiblings);
|
|
105
|
+
return hashInternal(leftHash, outermostSibling);
|
|
106
|
+
} else {
|
|
107
|
+
const rightHash = reconstructRoot(
|
|
108
|
+
leafHash,
|
|
109
|
+
index - k,
|
|
110
|
+
treeSize - k,
|
|
111
|
+
innerSiblings
|
|
112
|
+
);
|
|
113
|
+
return hashInternal(outermostSibling, rightHash);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function largestPowerOfTwoLessThan(n) {
|
|
117
|
+
if (n < 2) {
|
|
118
|
+
throw new Error(`largestPowerOfTwoLessThan: n must be >= 2 (got ${n})`);
|
|
119
|
+
}
|
|
120
|
+
let k = 1;
|
|
121
|
+
while (k * 2 < n) k *= 2;
|
|
122
|
+
return k;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/commitments/primitives.ts
|
|
126
|
+
function jcs(value) {
|
|
127
|
+
if (value === null || value === void 0) return "null";
|
|
128
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
129
|
+
return JSON.stringify(value);
|
|
130
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
return "[" + value.map(jcs).join(",") + "]";
|
|
133
|
+
const obj = value;
|
|
134
|
+
const keys = Object.keys(obj).sort();
|
|
135
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + jcs(obj[k])).join(",") + "}";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/commitments/leaf.ts
|
|
139
|
+
function base64urlNoPad(bytes) {
|
|
140
|
+
const std = typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : btoa(String.fromCharCode(...bytes));
|
|
141
|
+
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
142
|
+
}
|
|
143
|
+
function base64urlDecode(s) {
|
|
144
|
+
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
145
|
+
const padded = std + "=".repeat((4 - std.length % 4) % 4);
|
|
146
|
+
if (typeof Buffer !== "undefined") {
|
|
147
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
148
|
+
}
|
|
149
|
+
const bin = atob(padded);
|
|
150
|
+
const out = new Uint8Array(bin.length);
|
|
151
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function encodeLeaf(field) {
|
|
155
|
+
const obj = {
|
|
156
|
+
name: field.name,
|
|
157
|
+
salt: base64urlNoPad(field.salt),
|
|
158
|
+
value: field.value
|
|
159
|
+
};
|
|
160
|
+
const canonical = jcs(obj);
|
|
161
|
+
return new TextEncoder().encode(canonical);
|
|
162
|
+
}
|
|
163
|
+
function sortFields(fields) {
|
|
164
|
+
const encoder = new TextEncoder();
|
|
165
|
+
const decorated = fields.map((f) => ({
|
|
166
|
+
field: f,
|
|
167
|
+
nameBytes: encoder.encode(f.name)
|
|
168
|
+
}));
|
|
169
|
+
decorated.sort((a, b) => compareBytes(a.nameBytes, b.nameBytes));
|
|
170
|
+
return decorated.map((d) => d.field);
|
|
171
|
+
}
|
|
172
|
+
function compareBytes(a, b) {
|
|
173
|
+
const len = Math.min(a.length, b.length);
|
|
174
|
+
for (let i = 0; i < len; i++) {
|
|
175
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
176
|
+
}
|
|
177
|
+
return a.length - b.length;
|
|
178
|
+
}
|
|
179
|
+
function leavesFromFields(fields) {
|
|
180
|
+
const sorted = sortFields(fields);
|
|
181
|
+
const leafBytes = sorted.map(encodeLeaf);
|
|
182
|
+
return { sorted, leafBytes };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/signing-committed.ts
|
|
186
|
+
function freshSalt() {
|
|
187
|
+
return randomBytes(32);
|
|
188
|
+
}
|
|
189
|
+
function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
|
|
190
|
+
const allFields = {
|
|
191
|
+
tool: entry.tool,
|
|
192
|
+
decision: entry.decision,
|
|
193
|
+
reason_code: entry.reason_code,
|
|
194
|
+
policy_digest: entry.policy_digest,
|
|
195
|
+
scope: entry.request_id,
|
|
196
|
+
mode: entry.mode,
|
|
197
|
+
request_id: entry.request_id
|
|
198
|
+
};
|
|
199
|
+
if (entry.tier) allFields.tier = entry.tier;
|
|
200
|
+
if (entry.credential_ref) allFields.credential_ref = entry.credential_ref;
|
|
201
|
+
if (entry.rate_limit_remaining !== void 0) {
|
|
202
|
+
allFields.rate_limit_remaining = entry.rate_limit_remaining;
|
|
203
|
+
}
|
|
204
|
+
if (entry.policy_engine) allFields.policy_engine = entry.policy_engine;
|
|
205
|
+
if (entry.hook_event) allFields.hook_event = entry.hook_event;
|
|
206
|
+
if (entry.sandbox_state) allFields.sandbox_state = entry.sandbox_state;
|
|
207
|
+
if (entry.timing) allFields.timing = entry.timing;
|
|
208
|
+
if (entry.swarm) allFields.swarm = entry.swarm;
|
|
209
|
+
if (entry.payload_digest) allFields.payload_digest = entry.payload_digest;
|
|
210
|
+
if (entry.deny_iteration) allFields.deny_iteration = entry.deny_iteration;
|
|
211
|
+
const committedFields = [];
|
|
212
|
+
const cleartextFields = {};
|
|
213
|
+
const openings = {};
|
|
214
|
+
for (const [name, value] of Object.entries(allFields)) {
|
|
215
|
+
if (committedFieldNames.includes(name)) {
|
|
216
|
+
const salt = freshSalt();
|
|
217
|
+
committedFields.push({ name, salt, value });
|
|
218
|
+
} else {
|
|
219
|
+
cleartextFields[name] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let committedFieldsRoot = null;
|
|
223
|
+
if (committedFields.length > 0) {
|
|
224
|
+
const { sorted, leafBytes } = leavesFromFields(committedFields);
|
|
225
|
+
const leafHashes = leafBytes.map(hashLeaf);
|
|
226
|
+
const root = merkleRoot(leafHashes);
|
|
227
|
+
committedFieldsRoot = bytesToHex(root);
|
|
228
|
+
sorted.forEach((f, i) => {
|
|
229
|
+
openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const payload = {
|
|
233
|
+
type: "scopeblind.receipt.committed.v1",
|
|
234
|
+
spec: "draft-farley-acta-signed-receipts-01",
|
|
235
|
+
issuer_certification: "self-signed",
|
|
236
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
|
+
...cleartextFields
|
|
238
|
+
};
|
|
239
|
+
if (committedFieldsRoot !== null) {
|
|
240
|
+
payload.committed_fields_root = committedFieldsRoot;
|
|
241
|
+
payload.committed_field_names = committedFields.map((f) => f.name);
|
|
242
|
+
}
|
|
243
|
+
const canonical = jcs(payload);
|
|
244
|
+
const messageHash = sha2562(new TextEncoder().encode(canonical));
|
|
245
|
+
const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
|
|
246
|
+
const signedReceipt = {
|
|
247
|
+
...payload,
|
|
248
|
+
signature: {
|
|
249
|
+
alg: "EdDSA",
|
|
250
|
+
kid,
|
|
251
|
+
issuer,
|
|
252
|
+
sig: base64urlNoPad(signatureBytes),
|
|
253
|
+
public_key: publicKey
|
|
254
|
+
// hex
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const signedJson = JSON.stringify(signedReceipt);
|
|
258
|
+
const receiptHash = bytesToHex(sha2562(new TextEncoder().encode(jcs(signedReceipt))));
|
|
259
|
+
return {
|
|
260
|
+
signed: signedJson,
|
|
261
|
+
artifact_type: "decision_receipt_committed_v1",
|
|
262
|
+
openings,
|
|
263
|
+
receipt_hash: receiptHash
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function discloseField(receiptHash, fieldName, openings) {
|
|
267
|
+
const o = openings[fieldName];
|
|
268
|
+
if (!o) {
|
|
269
|
+
throw new Error(`disclose: no opening recorded for field "${fieldName}"`);
|
|
270
|
+
}
|
|
271
|
+
const fields = Object.values(openings).map((op) => ({
|
|
272
|
+
name: op.name,
|
|
273
|
+
salt: op.salt,
|
|
274
|
+
value: op.value
|
|
275
|
+
}));
|
|
276
|
+
const { leafBytes } = leavesFromFields(fields);
|
|
277
|
+
const leafHashes = leafBytes.map(hashLeaf);
|
|
278
|
+
const proof = generateProof(leafHashes, o.index);
|
|
279
|
+
return {
|
|
280
|
+
parent_receipt_hash: receiptHash,
|
|
281
|
+
name: fieldName,
|
|
282
|
+
value: o.value,
|
|
283
|
+
salt: base64urlNoPad(o.salt),
|
|
284
|
+
proof
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function createSelectiveDisclosurePackage(receipt, fieldNames, openings) {
|
|
288
|
+
const receiptHash = receiptHashHex(receipt);
|
|
289
|
+
const committedFieldsRoot = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
290
|
+
if (!committedFieldsRoot) {
|
|
291
|
+
throw new Error("selective disclosure requires a committed receipt with committed_fields_root");
|
|
292
|
+
}
|
|
293
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, openings);
|
|
294
|
+
const uniqueFields = Array.from(new Set(fieldNames));
|
|
295
|
+
for (const fieldName of uniqueFields) {
|
|
296
|
+
if (!committedFieldNames.includes(fieldName)) {
|
|
297
|
+
throw new Error(`selective disclosure: field "${fieldName}" is not committed by this receipt`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const disclosures = uniqueFields.map((fieldName) => discloseField(receiptHash, fieldName, openings));
|
|
301
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !uniqueFields.includes(fieldName));
|
|
302
|
+
return {
|
|
303
|
+
type: "scopeblind.selective_disclosure.v0",
|
|
304
|
+
version: 0,
|
|
305
|
+
parent_receipt_hash: receiptHash,
|
|
306
|
+
committed_fields_root: committedFieldsRoot,
|
|
307
|
+
disclosed_fields: uniqueFields,
|
|
308
|
+
hidden_fields: hiddenFields,
|
|
309
|
+
disclosures,
|
|
310
|
+
verifier_explanation: {
|
|
311
|
+
summary: "This package opens selected committed receipt fields and leaves the rest hidden.",
|
|
312
|
+
disclosed: uniqueFields.length ? `Disclosed fields: ${uniqueFields.join(", ")}.` : "No fields were disclosed.",
|
|
313
|
+
hidden: hiddenFields.length ? `Hidden committed fields: ${hiddenFields.join(", ")}. Their salted commitments remain bound to the signed receipt root.` : "No committed fields remain hidden.",
|
|
314
|
+
limitation: "Selective Disclosure v0 uses salted SHA-256 commitments and Merkle proofs. It is not a full zero-knowledge proof."
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function verifySelectiveDisclosurePackage(receipt, disclosure) {
|
|
319
|
+
const errors = [];
|
|
320
|
+
if (disclosure.type !== "scopeblind.selective_disclosure.v0") {
|
|
321
|
+
errors.push("disclosure.type is not scopeblind.selective_disclosure.v0");
|
|
322
|
+
}
|
|
323
|
+
const actualReceiptHash = receiptHashHex(receipt);
|
|
324
|
+
const receiptHashValid = disclosure.parent_receipt_hash === actualReceiptHash;
|
|
325
|
+
if (!receiptHashValid) {
|
|
326
|
+
errors.push("parent_receipt_hash does not match the supplied receipt");
|
|
327
|
+
}
|
|
328
|
+
const root = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
329
|
+
const commitmentRootValid = Boolean(root) && disclosure.committed_fields_root === root;
|
|
330
|
+
if (!commitmentRootValid) {
|
|
331
|
+
errors.push("committed_fields_root does not match the supplied receipt");
|
|
332
|
+
}
|
|
333
|
+
const signatureValid = verifyCommittedReceiptSignature(receipt);
|
|
334
|
+
if (signatureValid === false) {
|
|
335
|
+
errors.push("receipt signature failed verification");
|
|
336
|
+
}
|
|
337
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, {});
|
|
338
|
+
const disclosed = /* @__PURE__ */ new Set();
|
|
339
|
+
for (const item of disclosure.disclosures || []) {
|
|
340
|
+
if (item.parent_receipt_hash !== disclosure.parent_receipt_hash) {
|
|
341
|
+
errors.push(`disclosure for "${item.name}" targets a different receipt hash`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!committedFieldNames.includes(item.name)) {
|
|
345
|
+
errors.push(`field "${item.name}" is not listed in committed_field_names`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const leafBytes = encodeLeaf({
|
|
349
|
+
name: item.name,
|
|
350
|
+
salt: base64urlDecode(item.salt),
|
|
351
|
+
value: item.value
|
|
352
|
+
});
|
|
353
|
+
const ok = root ? verifyProof(root, hashLeaf(leafBytes), item.proof) : false;
|
|
354
|
+
if (!ok) {
|
|
355
|
+
errors.push(`field "${item.name}" failed Merkle inclusion verification`);
|
|
356
|
+
} else {
|
|
357
|
+
disclosed.add(item.name);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const disclosedFields = Array.from(disclosed);
|
|
361
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !disclosed.has(fieldName));
|
|
362
|
+
const valid = errors.length === 0 && receiptHashValid && commitmentRootValid && signatureValid !== false;
|
|
363
|
+
const explanation = [
|
|
364
|
+
valid ? "Selective disclosure verified: the disclosed fields open to the signed receipt commitment root." : "Selective disclosure failed verification.",
|
|
365
|
+
signatureValid === true ? "Receipt signature verified against the embedded Ed25519 public key." : signatureValid === null ? "Receipt signature was not checked because the committed receipt did not carry an embedded Ed25519 signature object." : "Receipt signature did not verify.",
|
|
366
|
+
disclosedFields.length ? `Disclosed fields: ${disclosedFields.join(", ")}.` : "No fields were disclosed.",
|
|
367
|
+
hiddenFields.length ? `Hidden fields: ${hiddenFields.join(", ")}. These remain private but bound to the same commitment root.` : "No committed fields remain hidden.",
|
|
368
|
+
"Limitation: this is salted commitment disclosure, not full zero-knowledge."
|
|
369
|
+
];
|
|
370
|
+
return {
|
|
371
|
+
valid,
|
|
372
|
+
receipt_hash_valid: receiptHashValid,
|
|
373
|
+
signature_valid: signatureValid,
|
|
374
|
+
commitment_root_valid: commitmentRootValid,
|
|
375
|
+
disclosed_fields: disclosedFields,
|
|
376
|
+
hidden_fields: hiddenFields,
|
|
377
|
+
errors,
|
|
378
|
+
explanation
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function committedFieldNamesFromReceipt(receipt, openings) {
|
|
382
|
+
const fromReceipt = Array.isArray(receipt.committed_field_names) ? receipt.committed_field_names.filter((fieldName) => typeof fieldName === "string") : [];
|
|
383
|
+
const names = fromReceipt.length ? fromReceipt : Object.keys(openings);
|
|
384
|
+
return Array.from(new Set(names)).sort();
|
|
385
|
+
}
|
|
386
|
+
function receiptHashHex(receipt) {
|
|
387
|
+
return bytesToHex(sha2562(new TextEncoder().encode(jcs(receipt))));
|
|
388
|
+
}
|
|
389
|
+
function verifyCommittedReceiptSignature(receipt) {
|
|
390
|
+
const signature = receipt.signature;
|
|
391
|
+
if (!signature || typeof signature !== "object") return null;
|
|
392
|
+
const sig = signature;
|
|
393
|
+
if (sig.alg !== "EdDSA" || typeof sig.sig !== "string" || typeof sig.public_key !== "string") {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const { signature: _signature, ...payloadWithoutSig } = receipt;
|
|
397
|
+
const messageHash = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
|
|
398
|
+
try {
|
|
399
|
+
return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export {
|
|
406
|
+
signCommittedDecision,
|
|
407
|
+
discloseField,
|
|
408
|
+
createSelectiveDisclosurePackage,
|
|
409
|
+
verifySelectiveDisclosurePackage
|
|
410
|
+
};
|
|
@@ -35040,6 +35040,48 @@ var TOOLS = [
|
|
|
35040
35040
|
},
|
|
35041
35041
|
required: ["environment"]
|
|
35042
35042
|
}
|
|
35043
|
+
},
|
|
35044
|
+
{
|
|
35045
|
+
name: "github_create_pr",
|
|
35046
|
+
description: "Create a GitHub pull request",
|
|
35047
|
+
inputSchema: {
|
|
35048
|
+
type: "object",
|
|
35049
|
+
properties: {
|
|
35050
|
+
repo: { type: "string", description: "Repository name" },
|
|
35051
|
+
branch: { type: "string", description: "Source branch" },
|
|
35052
|
+
title: { type: "string", description: "Pull request title" }
|
|
35053
|
+
},
|
|
35054
|
+
required: ["repo", "branch", "title"]
|
|
35055
|
+
}
|
|
35056
|
+
},
|
|
35057
|
+
{
|
|
35058
|
+
name: "send_email",
|
|
35059
|
+
description: "Send an email",
|
|
35060
|
+
inputSchema: {
|
|
35061
|
+
type: "object",
|
|
35062
|
+
properties: {
|
|
35063
|
+
to: { type: "string", description: "Recipient email address" },
|
|
35064
|
+
subject: { type: "string", description: "Subject line" },
|
|
35065
|
+
body: { type: "string", description: "Message body" }
|
|
35066
|
+
},
|
|
35067
|
+
required: ["to", "subject"]
|
|
35068
|
+
}
|
|
35069
|
+
},
|
|
35070
|
+
{
|
|
35071
|
+
name: "pms_book_fill",
|
|
35072
|
+
description: "Book a fill into the mock portfolio management system",
|
|
35073
|
+
inputSchema: {
|
|
35074
|
+
type: "object",
|
|
35075
|
+
properties: {
|
|
35076
|
+
account: { type: "string", description: "Portfolio or fund account" },
|
|
35077
|
+
symbol: { type: "string", description: "Instrument symbol" },
|
|
35078
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
35079
|
+
quantity: { type: "number" },
|
|
35080
|
+
price: { type: "number" },
|
|
35081
|
+
strategy: { type: "string" }
|
|
35082
|
+
},
|
|
35083
|
+
required: ["account", "symbol", "side", "quantity", "price"]
|
|
35084
|
+
}
|
|
35043
35085
|
}
|
|
35044
35086
|
];
|
|
35045
35087
|
function handleRequest(request) {
|
|
@@ -35087,6 +35129,15 @@ Contents: Hello from protect-mcp demo server!`;
|
|
|
35087
35129
|
case "deploy":
|
|
35088
35130
|
resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
|
|
35089
35131
|
break;
|
|
35132
|
+
case "github_create_pr":
|
|
35133
|
+
resultText = `[demo] Created PR in ${args.repo || "scopeblind/demo"} from ${args.branch || "agent/demo"}: ${args.title || "Agent change"}`;
|
|
35134
|
+
break;
|
|
35135
|
+
case "send_email":
|
|
35136
|
+
resultText = `[demo] Drafted email to ${args.to || "pm@example.com"}: ${args.subject || "Agent update"}`;
|
|
35137
|
+
break;
|
|
35138
|
+
case "pms_book_fill":
|
|
35139
|
+
resultText = `[demo] Booked ${args.side || "BUY"} ${args.quantity || 0} ${args.symbol || "AAPL"} @ ${args.price || 0} into ${args.account || "Demo Fund"} (${args.strategy || "Unassigned"})`;
|
|
35140
|
+
break;
|
|
35090
35141
|
default:
|
|
35091
35142
|
resultText = `[demo] Unknown tool: ${toolName}`;
|
|
35092
35143
|
}
|
|
@@ -35159,6 +35210,30 @@ function createSandboxServer() {
|
|
|
35159
35210
|
{ environment: z.enum(["staging", "production"]).describe("Target environment"), reason: z.string().optional().describe("Deployment reason") },
|
|
35160
35211
|
async (args) => ({ content: [{ type: "text", text: `[demo] Deployed to ${args.environment}` }] })
|
|
35161
35212
|
);
|
|
35213
|
+
server.tool("github_create_pr", "Create a GitHub pull request", {
|
|
35214
|
+
repo: z.string(),
|
|
35215
|
+
branch: z.string(),
|
|
35216
|
+
title: z.string()
|
|
35217
|
+
}, async (args) => ({
|
|
35218
|
+
content: [{ type: "text", text: `[demo] PR created in ${args.repo} from ${args.branch}: ${args.title}` }]
|
|
35219
|
+
}));
|
|
35220
|
+
server.tool("send_email", "Send an email", {
|
|
35221
|
+
to: z.string(),
|
|
35222
|
+
subject: z.string(),
|
|
35223
|
+
body: z.string().optional()
|
|
35224
|
+
}, async (args) => ({
|
|
35225
|
+
content: [{ type: "text", text: `[demo] Email prepared for ${args.to}: ${args.subject}` }]
|
|
35226
|
+
}));
|
|
35227
|
+
server.tool("pms_book_fill", "Book a fill into a mock PMS", {
|
|
35228
|
+
account: z.string(),
|
|
35229
|
+
symbol: z.string(),
|
|
35230
|
+
side: z.enum(["BUY", "SELL"]),
|
|
35231
|
+
quantity: z.number(),
|
|
35232
|
+
price: z.number(),
|
|
35233
|
+
strategy: z.string().optional()
|
|
35234
|
+
}, async (args) => ({
|
|
35235
|
+
content: [{ type: "text", text: `[demo] Booked ${args.side} ${args.quantity} ${args.symbol} @ ${args.price} into ${args.account}` }]
|
|
35236
|
+
}));
|
|
35162
35237
|
return server;
|
|
35163
35238
|
}
|
|
35164
35239
|
|