protect-mcp 0.9.3 → 0.9.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 +27 -0
- package/README.md +2 -1
- package/dist/{chunk-DX7QOA3E.mjs → chunk-H332ZNJ6.mjs} +1 -1
- package/dist/{chunk-HPKHMVZX.mjs → chunk-WWPQNIVF.mjs} +34 -1
- package/dist/{claim-3EBRPP5U.mjs → claim-RIXFOQM7.mjs} +93 -16
- package/dist/cli.js +294 -22
- package/dist/cli.mjs +171 -9
- package/dist/hook-server.js +34 -1
- package/dist/hook-server.mjs +2 -2
- package/dist/index.js +34 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.4: agent payments get receipts, records get heartbeats, anchors get names
|
|
4
|
+
|
|
5
|
+
The provenance layer reaches the agentic economy's payment rails (x402), the
|
|
6
|
+
record gains continuous completeness, and anchoring can carry an identity.
|
|
7
|
+
|
|
8
|
+
- **Payment receipts (x402 interop).** The gate now tags agent payments with a
|
|
9
|
+
signed `payment` capability, detected broadly across x402 wire shapes
|
|
10
|
+
(`paymentRequirements`, `X-PAYMENT`, EIP-3009 `transferWithAuthorization`)
|
|
11
|
+
and payment-shaped tools. Receipts carry minimum-disclosure payment facts:
|
|
12
|
+
amount (only when clearly readable in human units), asset, a HASHED recipient,
|
|
13
|
+
and the x402 scheme. `claim --no payment` proves no agent paid anything;
|
|
14
|
+
`claim --payment-under <cap>` proves every payment stayed under a cap, and an
|
|
15
|
+
amount the gate could not read counts as OVER (you cannot prove an amount you
|
|
16
|
+
could not read, so the claim cannot lie).
|
|
17
|
+
- **Record checkpoints (`anchor-record`).** Anchors the record's CURRENT
|
|
18
|
+
commitment (the same Merkle root a claim commits to, plus count and time
|
|
19
|
+
range) into the public log. Run it on a heartbeat and the record grows an
|
|
20
|
+
anchored history: a later claim whose root matches a checkpoint is provably
|
|
21
|
+
over the complete set as of that checkpoint. Skips when the record is
|
|
22
|
+
unchanged; writes a local `.protect-mcp-anchors.jsonl` history; only the
|
|
23
|
+
root, count, and time range leave the machine.
|
|
24
|
+
- **Pinned identity in the anchor loop.** `claim --anchor`, `anchor-record`,
|
|
25
|
+
and `verify-claim` now resolve the anchoring key against the public ScopeBlind
|
|
26
|
+
key directory: an enrolled key shows "anchored as <Org> (key pinned)", a
|
|
27
|
+
revoked key fails verification, an anonymous key points at enrollment. The
|
|
28
|
+
free anchor stays anonymous; the named identity is the paid upgrade.
|
|
29
|
+
|
|
3
30
|
## 0.9.3: the skeptic's tools sharpen
|
|
4
31
|
|
|
5
32
|
Two verification upgrades: the anchor check moves into the verifier, and the
|
package/README.md
CHANGED
|
@@ -373,7 +373,8 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
|
373
373
|
| `recommend` | Draft a reviewable JSON policy from observed local calls. Dry-run by default; use `--write` to create `protect-mcp.recommended.json`. |
|
|
374
374
|
| `registry` | Create an org identity, anchor receipt digests, and write a static verifier page. Hosted mode uploads digests only. |
|
|
375
375
|
| `record` | Open a local, searchable viewer over your receipts (`--live` streams as the agent runs): Ed25519 signatures verified in your browser against your gateway key, capability tags, a provenance tree, and one-click signed export. All local, nothing uploaded. |
|
|
376
|
-
| `claim` | Mint a signed, position-blind attestation of a predicate over the record (`--no <cap
|
|
376
|
+
| `claim` | Mint a signed, position-blind attestation of a predicate over the record (`--no <cap>` incl. `--no payment`, `--only <c1,c2>`, `--no-verdict <verdict>`, `--count <verdict>`, `--payment-under <cap>`), disclosing only decision categories. Add `--anchor` to record the claim digest in the public transparency log; enrolled keys anchor as a named org. |
|
|
377
|
+
| `anchor-record` | Checkpoint the record's Merkle root + count + time range into the public log (heartbeat-friendly: skips when unchanged). A later claim whose commitment matches an anchored checkpoint is provably over the complete record as of that checkpoint. |
|
|
377
378
|
| `verify-claim` | Verify a claim pack offline: signature, recomputed Merkle root, independently recomputed predicate, and the anchor sidecar when present (binds the anchored envelope to this exact claim, then confirms the public log holds it). `--check-anchor` requires the anchor; `--offline` skips the log hop. |
|
|
378
379
|
| `killer-demo` | Generate a complete shadow-mode to policy to approval to signed-receipt demo pack. |
|
|
379
380
|
| `verify-disclosure` | Verify a `scopeblind.selective_disclosure.v0` package and explain disclosed versus hidden fields. |
|
|
@@ -43,7 +43,11 @@ var RULES = [
|
|
|
43
43
|
{ cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
|
|
44
44
|
{ cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
|
|
45
45
|
{ cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
|
|
46
|
-
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
|
|
46
|
+
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
|
|
47
|
+
// Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
|
|
48
|
+
// only makes a `claim --no payment` harder to assert (conservative); a false
|
|
49
|
+
// negative would let a real payment escape the record's payment claims.
|
|
50
|
+
{ cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
|
|
47
51
|
];
|
|
48
52
|
function deriveCapabilities(tool, input) {
|
|
49
53
|
const t = String(tool || "").toLowerCase();
|
|
@@ -77,6 +81,33 @@ function deriveResource(input) {
|
|
|
77
81
|
}
|
|
78
82
|
return void 0;
|
|
79
83
|
}
|
|
84
|
+
function findField(input, names, depth = 0) {
|
|
85
|
+
if (depth > 4 || input === null || typeof input !== "object") return void 0;
|
|
86
|
+
const o = input;
|
|
87
|
+
const keys = Object.keys(o).sort();
|
|
88
|
+
for (const k of keys) {
|
|
89
|
+
if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
|
|
90
|
+
}
|
|
91
|
+
for (const k of keys) {
|
|
92
|
+
const v = findField(o[k], names, depth + 1);
|
|
93
|
+
if (v !== void 0) return v;
|
|
94
|
+
}
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
function derivePayment(tool, input) {
|
|
98
|
+
if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
|
|
99
|
+
const p = { amount: null, asset: null, recipient_digest: null };
|
|
100
|
+
const amt = findField(input, ["amount"]);
|
|
101
|
+
if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
|
|
102
|
+
else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
|
|
103
|
+
const asset = findField(input, ["asset", "currency", "token"]);
|
|
104
|
+
if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
|
|
105
|
+
const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
|
|
106
|
+
if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex(to.trim().toLowerCase());
|
|
107
|
+
const scheme = findField(input, ["scheme"]);
|
|
108
|
+
if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
|
|
109
|
+
return p;
|
|
110
|
+
}
|
|
80
111
|
function buildEnrichment(tool, input) {
|
|
81
112
|
const e = {
|
|
82
113
|
v: ENRICHMENT_VERSION,
|
|
@@ -85,6 +116,8 @@ function buildEnrichment(tool, input) {
|
|
|
85
116
|
};
|
|
86
117
|
const resource = deriveResource(input);
|
|
87
118
|
if (resource) e.resource = resource;
|
|
119
|
+
const payment = derivePayment(tool, input);
|
|
120
|
+
if (payment) e.payment = payment;
|
|
88
121
|
return e;
|
|
89
122
|
}
|
|
90
123
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canonicalJson
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WWPQNIVF.mjs";
|
|
4
4
|
import {
|
|
5
5
|
sha256
|
|
6
6
|
} from "./chunk-AYNQIEN7.mjs";
|
|
@@ -30,7 +30,13 @@ function receiptToLeaf(e) {
|
|
|
30
30
|
const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
31
31
|
const t = isFinite(ms) ? new Date(ms).toISOString() : "";
|
|
32
32
|
const d = sha256Hex(canonicalJson(e));
|
|
33
|
-
|
|
33
|
+
const leaf = { d, v, c, t };
|
|
34
|
+
const pay = enr && enr.payment;
|
|
35
|
+
if (pay && typeof pay === "object") {
|
|
36
|
+
const amt = pay.amount;
|
|
37
|
+
leaf.p = typeof amt === "number" && Number.isFinite(amt) ? amt : null;
|
|
38
|
+
}
|
|
39
|
+
return leaf;
|
|
34
40
|
}
|
|
35
41
|
function leafHash(leaf) {
|
|
36
42
|
return sha256Hex(canonicalJson(leaf));
|
|
@@ -63,6 +69,10 @@ function evaluate(pred, leaves) {
|
|
|
63
69
|
const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
|
|
64
70
|
return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
|
|
65
71
|
}
|
|
72
|
+
if (pred.kind === "payment_under") {
|
|
73
|
+
const matched2 = leaves.filter((l) => "p" in l && (l.p === null || l.p >= pred.cap)).length;
|
|
74
|
+
return { statement: `Every payment stayed under ${pred.cap} (unknown amounts count as over)`, holds: matched2 === 0, matched: matched2 };
|
|
75
|
+
}
|
|
66
76
|
const matched = leaves.filter((l) => l.v === pred.verdict).length;
|
|
67
77
|
return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
|
|
68
78
|
}
|
|
@@ -229,11 +239,9 @@ async function checkClaimAnchor(pack, sidecar, opts) {
|
|
|
229
239
|
return out;
|
|
230
240
|
}
|
|
231
241
|
}
|
|
232
|
-
async function
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const doFetch = opts.fetchImpl || globalThis.fetch;
|
|
236
|
-
if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
|
|
242
|
+
async function submitEnvelope(envelope, base, fetchImpl) {
|
|
243
|
+
const doFetch = fetchImpl || globalThis.fetch;
|
|
244
|
+
if (!doFetch) return { ok: false, error: "fetch_unavailable" };
|
|
237
245
|
const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
|
|
238
246
|
try {
|
|
239
247
|
const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
|
|
@@ -243,32 +251,101 @@ async function anchorClaim(pack, key, opts) {
|
|
|
243
251
|
});
|
|
244
252
|
const data = await resp.json().catch(() => null);
|
|
245
253
|
if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
|
|
246
|
-
return { ok: false,
|
|
254
|
+
return { ok: false, error: data && data.error || `http_${resp.status}` };
|
|
247
255
|
}
|
|
256
|
+
return { ok: true, seq: data.seq, anchored_at: data.anchored_at, already_anchored: !!data.already_anchored };
|
|
257
|
+
} catch {
|
|
258
|
+
return { ok: false, error: "network_error" };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function anchorClaim(pack, key, opts) {
|
|
262
|
+
const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
|
|
263
|
+
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
264
|
+
const out = await submitEnvelope(envelope, base, opts.fetchImpl);
|
|
265
|
+
if (!out.ok) return { ok: false, claim_digest: envelope.claim_digest, error: out.error, envelope };
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
claim_digest: envelope.claim_digest,
|
|
269
|
+
seq: out.seq,
|
|
270
|
+
entry_url: `${base}/fn/log/${out.seq}`,
|
|
271
|
+
anchored_at: out.anchored_at,
|
|
272
|
+
already_anchored: out.already_anchored,
|
|
273
|
+
envelope
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
var CHECKPOINT_SCHEMA = "scopeblind.protect-mcp.record-checkpoint.v1";
|
|
277
|
+
function buildRecordCheckpoint(receipts, key, issuedAt) {
|
|
278
|
+
const leaves = receipts.map(receiptToLeaf);
|
|
279
|
+
const times = leaves.map((l) => l.t).filter(Boolean).sort();
|
|
280
|
+
const signed = {
|
|
281
|
+
type: "evidence_pack",
|
|
282
|
+
schema: CHECKPOINT_SCHEMA,
|
|
283
|
+
anchors: "protect-mcp-record",
|
|
284
|
+
record_root: merkleRoot(leaves.map(leafHash)),
|
|
285
|
+
total: leaves.length,
|
|
286
|
+
from: times[0] || "",
|
|
287
|
+
to: times[times.length - 1] || "",
|
|
288
|
+
issued_at: issuedAt,
|
|
289
|
+
verification_key: key.publicKey,
|
|
290
|
+
disclosure: "internal"
|
|
291
|
+
};
|
|
292
|
+
const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
|
|
293
|
+
const digest = bytesToHex(hash);
|
|
294
|
+
const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
|
|
295
|
+
return { ...signed, signature, digest };
|
|
296
|
+
}
|
|
297
|
+
async function anchorRecordCheckpoint(receipts, key, opts) {
|
|
298
|
+
const checkpoint = buildRecordCheckpoint(receipts, key, opts.issuedAt);
|
|
299
|
+
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
300
|
+
const out = await submitEnvelope(checkpoint, base, opts.fetchImpl);
|
|
301
|
+
if (!out.ok) return { ok: false, record_root: checkpoint.record_root, total: checkpoint.total, checkpoint, error: out.error };
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
record_root: checkpoint.record_root,
|
|
305
|
+
total: checkpoint.total,
|
|
306
|
+
seq: out.seq,
|
|
307
|
+
entry_url: `${base}/fn/log/${out.seq}`,
|
|
308
|
+
anchored_at: out.anchored_at,
|
|
309
|
+
already_anchored: out.already_anchored,
|
|
310
|
+
checkpoint
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function lookupPinnedIdentity(publicKey, opts) {
|
|
314
|
+
const base = (opts && opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
315
|
+
const doFetch = opts && opts.fetchImpl || globalThis.fetch;
|
|
316
|
+
if (!doFetch || !/^[0-9a-f]{64}$/i.test(publicKey)) return null;
|
|
317
|
+
try {
|
|
318
|
+
const resp = await doFetch(`${base}/fn/log/keys/lookup/${publicKey.toLowerCase()}`, { headers: { accept: "application/json" } });
|
|
319
|
+
const data = await resp.json().catch(() => null);
|
|
320
|
+
if (!data || data.ok !== true) return null;
|
|
321
|
+
if (!data.found) return { found: false };
|
|
248
322
|
return {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
envelope
|
|
323
|
+
found: true,
|
|
324
|
+
name: data.name,
|
|
325
|
+
slug: data.slug,
|
|
326
|
+
kid: data.kid,
|
|
327
|
+
enrolled_at: data.enrolled_at,
|
|
328
|
+
revoked: !!(data.revoked || data.revoked_at)
|
|
256
329
|
};
|
|
257
330
|
} catch {
|
|
258
|
-
return
|
|
331
|
+
return null;
|
|
259
332
|
}
|
|
260
333
|
}
|
|
261
334
|
export {
|
|
262
335
|
ANCHOR_SCHEMA,
|
|
336
|
+
CHECKPOINT_SCHEMA,
|
|
263
337
|
CLAIM_TYPE,
|
|
264
338
|
DEFAULT_LOG,
|
|
265
339
|
anchorClaim,
|
|
340
|
+
anchorRecordCheckpoint,
|
|
266
341
|
buildAnchorEnvelope,
|
|
267
342
|
buildClaim,
|
|
343
|
+
buildRecordCheckpoint,
|
|
268
344
|
checkClaimAnchor,
|
|
269
345
|
claimDigest,
|
|
270
346
|
evaluate,
|
|
271
347
|
leafHash,
|
|
348
|
+
lookupPinnedIdentity,
|
|
272
349
|
merkleRoot,
|
|
273
350
|
receiptToLeaf,
|
|
274
351
|
verifyAnchorEnvelope,
|
package/dist/cli.js
CHANGED
|
@@ -5146,6 +5146,33 @@ function deriveResource(input) {
|
|
|
5146
5146
|
}
|
|
5147
5147
|
return void 0;
|
|
5148
5148
|
}
|
|
5149
|
+
function findField(input, names, depth = 0) {
|
|
5150
|
+
if (depth > 4 || input === null || typeof input !== "object") return void 0;
|
|
5151
|
+
const o = input;
|
|
5152
|
+
const keys = Object.keys(o).sort();
|
|
5153
|
+
for (const k of keys) {
|
|
5154
|
+
if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
|
|
5155
|
+
}
|
|
5156
|
+
for (const k of keys) {
|
|
5157
|
+
const v = findField(o[k], names, depth + 1);
|
|
5158
|
+
if (v !== void 0) return v;
|
|
5159
|
+
}
|
|
5160
|
+
return void 0;
|
|
5161
|
+
}
|
|
5162
|
+
function derivePayment(tool, input) {
|
|
5163
|
+
if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
|
|
5164
|
+
const p = { amount: null, asset: null, recipient_digest: null };
|
|
5165
|
+
const amt = findField(input, ["amount"]);
|
|
5166
|
+
if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
|
|
5167
|
+
else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
|
|
5168
|
+
const asset = findField(input, ["asset", "currency", "token"]);
|
|
5169
|
+
if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
|
|
5170
|
+
const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
|
|
5171
|
+
if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex2(to.trim().toLowerCase());
|
|
5172
|
+
const scheme = findField(input, ["scheme"]);
|
|
5173
|
+
if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
|
|
5174
|
+
return p;
|
|
5175
|
+
}
|
|
5149
5176
|
function buildEnrichment(tool, input) {
|
|
5150
5177
|
const e = {
|
|
5151
5178
|
v: ENRICHMENT_VERSION,
|
|
@@ -5154,6 +5181,8 @@ function buildEnrichment(tool, input) {
|
|
|
5154
5181
|
};
|
|
5155
5182
|
const resource = deriveResource(input);
|
|
5156
5183
|
if (resource) e.resource = resource;
|
|
5184
|
+
const payment = derivePayment(tool, input);
|
|
5185
|
+
if (payment) e.payment = payment;
|
|
5157
5186
|
return e;
|
|
5158
5187
|
}
|
|
5159
5188
|
var ENRICHMENT_VERSION, RULES;
|
|
@@ -5174,7 +5203,11 @@ var init_receipt_enrichment = __esm({
|
|
|
5174
5203
|
{ cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
|
|
5175
5204
|
{ cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
|
|
5176
5205
|
{ cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
|
|
5177
|
-
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
|
|
5206
|
+
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
|
|
5207
|
+
// Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
|
|
5208
|
+
// only makes a `claim --no payment` harder to assert (conservative); a false
|
|
5209
|
+
// negative would let a real payment escape the record's payment claims.
|
|
5210
|
+
{ cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
|
|
5178
5211
|
];
|
|
5179
5212
|
}
|
|
5180
5213
|
});
|
|
@@ -5183,15 +5216,19 @@ var init_receipt_enrichment = __esm({
|
|
|
5183
5216
|
var claim_exports = {};
|
|
5184
5217
|
__export(claim_exports, {
|
|
5185
5218
|
ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
|
|
5219
|
+
CHECKPOINT_SCHEMA: () => CHECKPOINT_SCHEMA,
|
|
5186
5220
|
CLAIM_TYPE: () => CLAIM_TYPE,
|
|
5187
5221
|
DEFAULT_LOG: () => DEFAULT_LOG,
|
|
5188
5222
|
anchorClaim: () => anchorClaim,
|
|
5223
|
+
anchorRecordCheckpoint: () => anchorRecordCheckpoint,
|
|
5189
5224
|
buildAnchorEnvelope: () => buildAnchorEnvelope,
|
|
5190
5225
|
buildClaim: () => buildClaim,
|
|
5226
|
+
buildRecordCheckpoint: () => buildRecordCheckpoint,
|
|
5191
5227
|
checkClaimAnchor: () => checkClaimAnchor,
|
|
5192
5228
|
claimDigest: () => claimDigest,
|
|
5193
5229
|
evaluate: () => evaluate,
|
|
5194
5230
|
leafHash: () => leafHash,
|
|
5231
|
+
lookupPinnedIdentity: () => lookupPinnedIdentity,
|
|
5195
5232
|
merkleRoot: () => merkleRoot,
|
|
5196
5233
|
receiptToLeaf: () => receiptToLeaf,
|
|
5197
5234
|
verifyAnchorEnvelope: () => verifyAnchorEnvelope,
|
|
@@ -5211,7 +5248,13 @@ function receiptToLeaf(e) {
|
|
|
5211
5248
|
const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
5212
5249
|
const t = isFinite(ms) ? new Date(ms).toISOString() : "";
|
|
5213
5250
|
const d = sha256Hex3(canonicalJson(e));
|
|
5214
|
-
|
|
5251
|
+
const leaf = { d, v, c, t };
|
|
5252
|
+
const pay = enr && enr.payment;
|
|
5253
|
+
if (pay && typeof pay === "object") {
|
|
5254
|
+
const amt = pay.amount;
|
|
5255
|
+
leaf.p = typeof amt === "number" && Number.isFinite(amt) ? amt : null;
|
|
5256
|
+
}
|
|
5257
|
+
return leaf;
|
|
5215
5258
|
}
|
|
5216
5259
|
function leafHash(leaf) {
|
|
5217
5260
|
return sha256Hex3(canonicalJson(leaf));
|
|
@@ -5244,6 +5287,10 @@ function evaluate(pred, leaves) {
|
|
|
5244
5287
|
const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
|
|
5245
5288
|
return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
|
|
5246
5289
|
}
|
|
5290
|
+
if (pred.kind === "payment_under") {
|
|
5291
|
+
const matched2 = leaves.filter((l) => "p" in l && (l.p === null || l.p >= pred.cap)).length;
|
|
5292
|
+
return { statement: `Every payment stayed under ${pred.cap} (unknown amounts count as over)`, holds: matched2 === 0, matched: matched2 };
|
|
5293
|
+
}
|
|
5247
5294
|
const matched = leaves.filter((l) => l.v === pred.verdict).length;
|
|
5248
5295
|
return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
|
|
5249
5296
|
}
|
|
@@ -5408,11 +5455,9 @@ async function checkClaimAnchor(pack, sidecar, opts) {
|
|
|
5408
5455
|
return out;
|
|
5409
5456
|
}
|
|
5410
5457
|
}
|
|
5411
|
-
async function
|
|
5412
|
-
const
|
|
5413
|
-
|
|
5414
|
-
const doFetch = opts.fetchImpl || globalThis.fetch;
|
|
5415
|
-
if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
|
|
5458
|
+
async function submitEnvelope(envelope, base, fetchImpl) {
|
|
5459
|
+
const doFetch = fetchImpl || globalThis.fetch;
|
|
5460
|
+
if (!doFetch) return { ok: false, error: "fetch_unavailable" };
|
|
5416
5461
|
const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
|
|
5417
5462
|
try {
|
|
5418
5463
|
const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
|
|
@@ -5422,22 +5467,86 @@ async function anchorClaim(pack, key, opts) {
|
|
|
5422
5467
|
});
|
|
5423
5468
|
const data = await resp.json().catch(() => null);
|
|
5424
5469
|
if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
|
|
5425
|
-
return { ok: false,
|
|
5470
|
+
return { ok: false, error: data && data.error || `http_${resp.status}` };
|
|
5426
5471
|
}
|
|
5472
|
+
return { ok: true, seq: data.seq, anchored_at: data.anchored_at, already_anchored: !!data.already_anchored };
|
|
5473
|
+
} catch {
|
|
5474
|
+
return { ok: false, error: "network_error" };
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
async function anchorClaim(pack, key, opts) {
|
|
5478
|
+
const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
|
|
5479
|
+
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
5480
|
+
const out = await submitEnvelope(envelope, base, opts.fetchImpl);
|
|
5481
|
+
if (!out.ok) return { ok: false, claim_digest: envelope.claim_digest, error: out.error, envelope };
|
|
5482
|
+
return {
|
|
5483
|
+
ok: true,
|
|
5484
|
+
claim_digest: envelope.claim_digest,
|
|
5485
|
+
seq: out.seq,
|
|
5486
|
+
entry_url: `${base}/fn/log/${out.seq}`,
|
|
5487
|
+
anchored_at: out.anchored_at,
|
|
5488
|
+
already_anchored: out.already_anchored,
|
|
5489
|
+
envelope
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
function buildRecordCheckpoint(receipts, key, issuedAt) {
|
|
5493
|
+
const leaves = receipts.map(receiptToLeaf);
|
|
5494
|
+
const times = leaves.map((l) => l.t).filter(Boolean).sort();
|
|
5495
|
+
const signed = {
|
|
5496
|
+
type: "evidence_pack",
|
|
5497
|
+
schema: CHECKPOINT_SCHEMA,
|
|
5498
|
+
anchors: "protect-mcp-record",
|
|
5499
|
+
record_root: merkleRoot(leaves.map(leafHash)),
|
|
5500
|
+
total: leaves.length,
|
|
5501
|
+
from: times[0] || "",
|
|
5502
|
+
to: times[times.length - 1] || "",
|
|
5503
|
+
issued_at: issuedAt,
|
|
5504
|
+
verification_key: key.publicKey,
|
|
5505
|
+
disclosure: "internal"
|
|
5506
|
+
};
|
|
5507
|
+
const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
|
|
5508
|
+
const digest = bytesToHex(hash);
|
|
5509
|
+
const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
|
|
5510
|
+
return { ...signed, signature, digest };
|
|
5511
|
+
}
|
|
5512
|
+
async function anchorRecordCheckpoint(receipts, key, opts) {
|
|
5513
|
+
const checkpoint = buildRecordCheckpoint(receipts, key, opts.issuedAt);
|
|
5514
|
+
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
5515
|
+
const out = await submitEnvelope(checkpoint, base, opts.fetchImpl);
|
|
5516
|
+
if (!out.ok) return { ok: false, record_root: checkpoint.record_root, total: checkpoint.total, checkpoint, error: out.error };
|
|
5517
|
+
return {
|
|
5518
|
+
ok: true,
|
|
5519
|
+
record_root: checkpoint.record_root,
|
|
5520
|
+
total: checkpoint.total,
|
|
5521
|
+
seq: out.seq,
|
|
5522
|
+
entry_url: `${base}/fn/log/${out.seq}`,
|
|
5523
|
+
anchored_at: out.anchored_at,
|
|
5524
|
+
already_anchored: out.already_anchored,
|
|
5525
|
+
checkpoint
|
|
5526
|
+
};
|
|
5527
|
+
}
|
|
5528
|
+
async function lookupPinnedIdentity(publicKey, opts) {
|
|
5529
|
+
const base = (opts && opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
5530
|
+
const doFetch = opts && opts.fetchImpl || globalThis.fetch;
|
|
5531
|
+
if (!doFetch || !/^[0-9a-f]{64}$/i.test(publicKey)) return null;
|
|
5532
|
+
try {
|
|
5533
|
+
const resp = await doFetch(`${base}/fn/log/keys/lookup/${publicKey.toLowerCase()}`, { headers: { accept: "application/json" } });
|
|
5534
|
+
const data = await resp.json().catch(() => null);
|
|
5535
|
+
if (!data || data.ok !== true) return null;
|
|
5536
|
+
if (!data.found) return { found: false };
|
|
5427
5537
|
return {
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
envelope
|
|
5538
|
+
found: true,
|
|
5539
|
+
name: data.name,
|
|
5540
|
+
slug: data.slug,
|
|
5541
|
+
kid: data.kid,
|
|
5542
|
+
enrolled_at: data.enrolled_at,
|
|
5543
|
+
revoked: !!(data.revoked || data.revoked_at)
|
|
5435
5544
|
};
|
|
5436
5545
|
} catch {
|
|
5437
|
-
return
|
|
5546
|
+
return null;
|
|
5438
5547
|
}
|
|
5439
5548
|
}
|
|
5440
|
-
var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
|
|
5549
|
+
var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG, CHECKPOINT_SCHEMA;
|
|
5441
5550
|
var init_claim = __esm({
|
|
5442
5551
|
"src/claim.ts"() {
|
|
5443
5552
|
"use strict";
|
|
@@ -5448,6 +5557,7 @@ var init_claim = __esm({
|
|
|
5448
5557
|
CLAIM_TYPE = "scopeblind.claim.v1";
|
|
5449
5558
|
ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
|
|
5450
5559
|
DEFAULT_LOG = "https://scopeblind.com";
|
|
5560
|
+
CHECKPOINT_SCHEMA = "scopeblind.protect-mcp.record-checkpoint.v1";
|
|
5451
5561
|
}
|
|
5452
5562
|
});
|
|
5453
5563
|
|
|
@@ -8711,8 +8821,12 @@ Commands:
|
|
|
8711
8821
|
digest Generate a human-readable summary of agent activity
|
|
8712
8822
|
receipts Show recent persisted signed receipts
|
|
8713
8823
|
record Open a local, searchable view of your record in the browser
|
|
8714
|
-
claim Attest a signed, position-blind claim over your record (e.g. no egress
|
|
8715
|
-
|
|
8824
|
+
claim Attest a signed, position-blind claim over your record (e.g. no egress,
|
|
8825
|
+
no payment, every payment under a cap)
|
|
8826
|
+
verify-claim Verify a claim attestation offline (signature + predicate + commitment
|
|
8827
|
+
+ the anchor sidecar and issuer identity when present)
|
|
8828
|
+
anchor-record Checkpoint the record's Merkle root + count into the public log
|
|
8829
|
+
(heartbeat-friendly: skips when unchanged; only hashes leave)
|
|
8716
8830
|
bundle Export an offline-verifiable audit bundle
|
|
8717
8831
|
|
|
8718
8832
|
Examples:
|
|
@@ -10889,20 +11003,23 @@ async function handleClaim(argv) {
|
|
|
10889
11003
|
const di = argv.indexOf("--dir");
|
|
10890
11004
|
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
10891
11005
|
let predicate = null;
|
|
10892
|
-
const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count");
|
|
11006
|
+
const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count"), puIdx = argv.indexOf("--payment-under");
|
|
10893
11007
|
if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
|
|
10894
11008
|
else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
|
|
10895
11009
|
else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
|
|
10896
11010
|
else if (cvIdx !== -1 && argv[cvIdx + 1]) predicate = { kind: "count_verdict", verdict: argv[cvIdx + 1] };
|
|
11011
|
+
else if (puIdx !== -1 && argv[puIdx + 1] && isFinite(parseFloat(argv[puIdx + 1]))) predicate = { kind: "payment_under", cap: parseFloat(argv[puIdx + 1]) };
|
|
10897
11012
|
if (!predicate) {
|
|
10898
11013
|
process.stderr.write(`
|
|
10899
11014
|
${bold("protect-mcp claim")}
|
|
10900
11015
|
|
|
10901
11016
|
Attest a signed, position-blind claim over your record:
|
|
10902
|
-
--no <capability> no action used it, e.g. ${dim("--no net.egress")}
|
|
11017
|
+
--no <capability> no action used it, e.g. ${dim("--no net.egress")} or ${dim("--no payment")}
|
|
10903
11018
|
--only <c1,c2,...> all actions confined to these capabilities
|
|
10904
11019
|
--no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
|
|
10905
11020
|
--count <verdict> how many, e.g. ${dim("--count blocked")}
|
|
11021
|
+
--payment-under <cap> every agent payment stayed under the cap (amounts the
|
|
11022
|
+
gate could not read count as OVER, so this cannot lie)
|
|
10906
11023
|
--anchor also record the claim digest in the public append-only
|
|
10907
11024
|
log so a counterparty can trust it is complete (only the
|
|
10908
11025
|
hash is sent; your record stays local)
|
|
@@ -11005,8 +11122,18 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
11005
11122
|
`);
|
|
11006
11123
|
process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
|
|
11007
11124
|
`);
|
|
11008
|
-
|
|
11125
|
+
const { lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
11126
|
+
const who = await lookupPinnedIdentity2(key.publicKey, { log: logBase });
|
|
11127
|
+
if (who && who.found && !who.revoked) {
|
|
11128
|
+
process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory" + (who.enrolled_at ? ", enrolled " + who.enrolled_at.slice(0, 10) : "") + ")")}
|
|
11129
|
+
`);
|
|
11130
|
+
} else if (who && who.found && who.revoked) {
|
|
11131
|
+
process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
|
|
11132
|
+
`);
|
|
11133
|
+
} else {
|
|
11134
|
+
process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). To anchor as a named org a counterparty can pin, see")} ${bold("scopeblind.com/enroll")}
|
|
11009
11135
|
`);
|
|
11136
|
+
}
|
|
11010
11137
|
} else {
|
|
11011
11138
|
process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
|
|
11012
11139
|
`);
|
|
@@ -11016,6 +11143,132 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
11016
11143
|
`);
|
|
11017
11144
|
process.exit(0);
|
|
11018
11145
|
}
|
|
11146
|
+
async function handleAnchorRecord(argv) {
|
|
11147
|
+
const { readFileSync: readFileSync11, existsSync: existsSync9, appendFileSync: appendFileSync3 } = await import("fs");
|
|
11148
|
+
const { join: join8 } = await import("path");
|
|
11149
|
+
const { anchorRecordCheckpoint: anchorRecordCheckpoint2, buildRecordCheckpoint: buildRecordCheckpoint2, lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
11150
|
+
let dir = process.cwd();
|
|
11151
|
+
const di = argv.indexOf("--dir");
|
|
11152
|
+
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
11153
|
+
const li = argv.indexOf("--log");
|
|
11154
|
+
const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
|
|
11155
|
+
const keyPath = join8(dir, "keys", "gateway.json");
|
|
11156
|
+
if (!existsSync9(keyPath)) {
|
|
11157
|
+
process.stderr.write(`
|
|
11158
|
+
${bold("protect-mcp anchor-record")}
|
|
11159
|
+
|
|
11160
|
+
No signing key at ${keyPath}. A checkpoint must be signed. Run ${bold("npx protect-mcp init")} first.
|
|
11161
|
+
|
|
11162
|
+
`);
|
|
11163
|
+
process.exit(1);
|
|
11164
|
+
return;
|
|
11165
|
+
}
|
|
11166
|
+
let key;
|
|
11167
|
+
try {
|
|
11168
|
+
key = JSON.parse(readFileSync11(keyPath, "utf-8"));
|
|
11169
|
+
} catch {
|
|
11170
|
+
process.stderr.write(`
|
|
11171
|
+
protect-mcp anchor-record: ${keyPath} is not valid JSON.
|
|
11172
|
+
|
|
11173
|
+
`);
|
|
11174
|
+
process.exit(1);
|
|
11175
|
+
return;
|
|
11176
|
+
}
|
|
11177
|
+
if (!key.privateKey || !key.publicKey) {
|
|
11178
|
+
process.stderr.write(`
|
|
11179
|
+
protect-mcp anchor-record: ${keyPath} is missing privateKey/publicKey.
|
|
11180
|
+
|
|
11181
|
+
`);
|
|
11182
|
+
process.exit(1);
|
|
11183
|
+
return;
|
|
11184
|
+
}
|
|
11185
|
+
const recPath = join8(dir, ".protect-mcp-receipts.jsonl");
|
|
11186
|
+
if (!existsSync9(recPath)) {
|
|
11187
|
+
process.stderr.write(`
|
|
11188
|
+
${bold("protect-mcp anchor-record")}
|
|
11189
|
+
|
|
11190
|
+
No signed receipts in ${dir}. Run the gate with signing on, then try again.
|
|
11191
|
+
|
|
11192
|
+
`);
|
|
11193
|
+
process.exit(0);
|
|
11194
|
+
return;
|
|
11195
|
+
}
|
|
11196
|
+
const receipts = readFileSync11(recPath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
11197
|
+
try {
|
|
11198
|
+
return JSON.parse(l);
|
|
11199
|
+
} catch {
|
|
11200
|
+
return null;
|
|
11201
|
+
}
|
|
11202
|
+
}).filter((x) => x !== null);
|
|
11203
|
+
if (!receipts.length) {
|
|
11204
|
+
process.stderr.write(`
|
|
11205
|
+
protect-mcp anchor-record: no readable receipts in ${recPath}.
|
|
11206
|
+
|
|
11207
|
+
`);
|
|
11208
|
+
process.exit(0);
|
|
11209
|
+
return;
|
|
11210
|
+
}
|
|
11211
|
+
const claimKey = { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" };
|
|
11212
|
+
const historyPath = join8(dir, ".protect-mcp-anchors.jsonl");
|
|
11213
|
+
const preview = buildRecordCheckpoint2(receipts, claimKey, "preview");
|
|
11214
|
+
if (!argv.includes("--force") && existsSync9(historyPath)) {
|
|
11215
|
+
const lines = readFileSync11(historyPath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
11216
|
+
const last = lines.length ? (() => {
|
|
11217
|
+
try {
|
|
11218
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
11219
|
+
} catch {
|
|
11220
|
+
return null;
|
|
11221
|
+
}
|
|
11222
|
+
})() : null;
|
|
11223
|
+
if (last && last.record_root === preview.record_root && last.total === preview.total) {
|
|
11224
|
+
process.stdout.write(`
|
|
11225
|
+
${bold("\u{1F6E1}\uFE0F Record checkpoint")}
|
|
11226
|
+
`);
|
|
11227
|
+
process.stdout.write(` Unchanged since entry ${bold("#" + last.seq)} ${dim("(" + last.total + " receipts, anchored " + (last.anchored_at || "") + ")")}. Nothing new to anchor.
|
|
11228
|
+
`);
|
|
11229
|
+
process.stdout.write(` ${dim("Use --force to re-anchor anyway.")}
|
|
11230
|
+
|
|
11231
|
+
`);
|
|
11232
|
+
process.exit(0);
|
|
11233
|
+
return;
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
const res = await anchorRecordCheckpoint2(receipts, claimKey, { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
11237
|
+
process.stdout.write(`
|
|
11238
|
+
${bold("\u{1F6E1}\uFE0F Record checkpoint")}
|
|
11239
|
+
`);
|
|
11240
|
+
process.stdout.write(` ${res.total} receipts ${dim("\xB7")} root ${dim(res.record_root.slice(0, 16) + "\u2026")} ${dim("(" + res.checkpoint.from.slice(0, 10) + " \u2192 " + res.checkpoint.to.slice(0, 10) + ")")}
|
|
11241
|
+
`);
|
|
11242
|
+
if (!res.ok) {
|
|
11243
|
+
process.stdout.write(` ${yellow("Anchor failed")} ${dim("(" + (res.error || "unavailable") + "). Nothing was recorded; try again.")}
|
|
11244
|
+
|
|
11245
|
+
`);
|
|
11246
|
+
process.exit(1);
|
|
11247
|
+
return;
|
|
11248
|
+
}
|
|
11249
|
+
appendFileSync3(historyPath, JSON.stringify({ schema: res.checkpoint.schema, seq: res.seq, anchored_at: res.anchored_at, total: res.total, record_root: res.record_root, entry_url: res.entry_url, digest: res.checkpoint.digest }) + "\n");
|
|
11250
|
+
process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)} ${dim(res.entry_url || "")}
|
|
11251
|
+
`);
|
|
11252
|
+
process.stdout.write(` ${dim("Only the root, count, and time range were sent. History: " + historyPath)}
|
|
11253
|
+
`);
|
|
11254
|
+
const who = await lookupPinnedIdentity2(claimKey.publicKey, { log: logBase });
|
|
11255
|
+
if (who && who.found && !who.revoked) {
|
|
11256
|
+
process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory)")}
|
|
11257
|
+
`);
|
|
11258
|
+
} else if (who && who.found && who.revoked) {
|
|
11259
|
+
process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
|
|
11260
|
+
`);
|
|
11261
|
+
} else {
|
|
11262
|
+
process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). Named identity: scopeblind.com/enroll")}
|
|
11263
|
+
`);
|
|
11264
|
+
}
|
|
11265
|
+
process.stdout.write(` ${dim("A claim whose commitment matches this root is provably over the complete record as of")}
|
|
11266
|
+
`);
|
|
11267
|
+
process.stdout.write(` ${dim("this checkpoint. Run this on a heartbeat (e.g. cron) to keep the anchored history growing.")}
|
|
11268
|
+
|
|
11269
|
+
`);
|
|
11270
|
+
process.exit(0);
|
|
11271
|
+
}
|
|
11019
11272
|
async function handleVerifyClaim(argv) {
|
|
11020
11273
|
const { readFileSync: readFileSync11, existsSync: existsSync9 } = await import("fs");
|
|
11021
11274
|
const { verifyClaim: verifyClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
@@ -11102,6 +11355,21 @@ ${bold("protect-mcp verify-claim")}
|
|
|
11102
11355
|
process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
|
|
11103
11356
|
`);
|
|
11104
11357
|
}
|
|
11358
|
+
if (!argv.includes("--offline") && sidecar.envelope) {
|
|
11359
|
+
const { lookupPinnedIdentity: lookupPinnedIdentity2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
11360
|
+
const who = await lookupPinnedIdentity2(sidecar.envelope.verification_key, {});
|
|
11361
|
+
if (who && who.found && !who.revoked) {
|
|
11362
|
+
process.stdout.write(` ${green("\u2713")} issuer key pinned to ${bold(who.name || who.slug || "an enrolled org")} ${dim("(ScopeBlind key directory)")}
|
|
11363
|
+
`);
|
|
11364
|
+
} else if (who && who.found && who.revoked) {
|
|
11365
|
+
anchorOk = false;
|
|
11366
|
+
process.stdout.write(` ${red("\u2717")} issuer key is REVOKED in the ScopeBlind key directory
|
|
11367
|
+
`);
|
|
11368
|
+
} else if (who && !who.found) {
|
|
11369
|
+
process.stdout.write(` ${dim("issuer key not enrolled (anonymous issuer); named identities pin via scopeblind.com/enroll")}
|
|
11370
|
+
`);
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11105
11373
|
}
|
|
11106
11374
|
} else if (requireAnchor) {
|
|
11107
11375
|
anchorOk = false;
|
|
@@ -12629,6 +12897,10 @@ async function main() {
|
|
|
12629
12897
|
await handleVerifyClaim(args.slice(1));
|
|
12630
12898
|
return;
|
|
12631
12899
|
}
|
|
12900
|
+
if (args[0] === "anchor-record") {
|
|
12901
|
+
await handleAnchorRecord(args.slice(1));
|
|
12902
|
+
return;
|
|
12903
|
+
}
|
|
12632
12904
|
if (args[0] === "init-hooks") {
|
|
12633
12905
|
await handleInitHooks(args.slice(1));
|
|
12634
12906
|
process.exit(0);
|
package/dist/cli.mjs
CHANGED
|
@@ -100,8 +100,12 @@ Commands:
|
|
|
100
100
|
digest Generate a human-readable summary of agent activity
|
|
101
101
|
receipts Show recent persisted signed receipts
|
|
102
102
|
record Open a local, searchable view of your record in the browser
|
|
103
|
-
claim Attest a signed, position-blind claim over your record (e.g. no egress
|
|
104
|
-
|
|
103
|
+
claim Attest a signed, position-blind claim over your record (e.g. no egress,
|
|
104
|
+
no payment, every payment under a cap)
|
|
105
|
+
verify-claim Verify a claim attestation offline (signature + predicate + commitment
|
|
106
|
+
+ the anchor sidecar and issuer identity when present)
|
|
107
|
+
anchor-record Checkpoint the record's Merkle root + count into the public log
|
|
108
|
+
(heartbeat-friendly: skips when unchanged; only hashes leave)
|
|
105
109
|
bundle Export an offline-verifiable audit bundle
|
|
106
110
|
|
|
107
111
|
Examples:
|
|
@@ -2273,25 +2277,28 @@ if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(functio
|
|
|
2273
2277
|
async function handleClaim(argv) {
|
|
2274
2278
|
const { readFileSync, existsSync, writeFileSync } = await import("fs");
|
|
2275
2279
|
const { join } = await import("path");
|
|
2276
|
-
const { buildClaim } = await import("./claim-
|
|
2280
|
+
const { buildClaim } = await import("./claim-RIXFOQM7.mjs");
|
|
2277
2281
|
let dir = process.cwd();
|
|
2278
2282
|
const di = argv.indexOf("--dir");
|
|
2279
2283
|
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
2280
2284
|
let predicate = null;
|
|
2281
|
-
const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count");
|
|
2285
|
+
const noIdx = argv.indexOf("--no"), onlyIdx = argv.indexOf("--only"), nvIdx = argv.indexOf("--no-verdict"), cvIdx = argv.indexOf("--count"), puIdx = argv.indexOf("--payment-under");
|
|
2282
2286
|
if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
|
|
2283
2287
|
else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
|
|
2284
2288
|
else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
|
|
2285
2289
|
else if (cvIdx !== -1 && argv[cvIdx + 1]) predicate = { kind: "count_verdict", verdict: argv[cvIdx + 1] };
|
|
2290
|
+
else if (puIdx !== -1 && argv[puIdx + 1] && isFinite(parseFloat(argv[puIdx + 1]))) predicate = { kind: "payment_under", cap: parseFloat(argv[puIdx + 1]) };
|
|
2286
2291
|
if (!predicate) {
|
|
2287
2292
|
process.stderr.write(`
|
|
2288
2293
|
${bold("protect-mcp claim")}
|
|
2289
2294
|
|
|
2290
2295
|
Attest a signed, position-blind claim over your record:
|
|
2291
|
-
--no <capability> no action used it, e.g. ${dim("--no net.egress")}
|
|
2296
|
+
--no <capability> no action used it, e.g. ${dim("--no net.egress")} or ${dim("--no payment")}
|
|
2292
2297
|
--only <c1,c2,...> all actions confined to these capabilities
|
|
2293
2298
|
--no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
|
|
2294
2299
|
--count <verdict> how many, e.g. ${dim("--count blocked")}
|
|
2300
|
+
--payment-under <cap> every agent payment stayed under the cap (amounts the
|
|
2301
|
+
gate could not read count as OVER, so this cannot lie)
|
|
2295
2302
|
--anchor also record the claim digest in the public append-only
|
|
2296
2303
|
log so a counterparty can trust it is complete (only the
|
|
2297
2304
|
hash is sent; your record stays local)
|
|
@@ -2374,7 +2381,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
2374
2381
|
process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
|
|
2375
2382
|
`);
|
|
2376
2383
|
if (argv.indexOf("--anchor") !== -1) {
|
|
2377
|
-
const { anchorClaim } = await import("./claim-
|
|
2384
|
+
const { anchorClaim } = await import("./claim-RIXFOQM7.mjs");
|
|
2378
2385
|
const li = argv.indexOf("--log");
|
|
2379
2386
|
const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
|
|
2380
2387
|
process.stdout.write(`
|
|
@@ -2394,8 +2401,18 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
2394
2401
|
`);
|
|
2395
2402
|
process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
|
|
2396
2403
|
`);
|
|
2397
|
-
|
|
2404
|
+
const { lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
|
|
2405
|
+
const who = await lookupPinnedIdentity(key.publicKey, { log: logBase });
|
|
2406
|
+
if (who && who.found && !who.revoked) {
|
|
2407
|
+
process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory" + (who.enrolled_at ? ", enrolled " + who.enrolled_at.slice(0, 10) : "") + ")")}
|
|
2398
2408
|
`);
|
|
2409
|
+
} else if (who && who.found && who.revoked) {
|
|
2410
|
+
process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
|
|
2411
|
+
`);
|
|
2412
|
+
} else {
|
|
2413
|
+
process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). To anchor as a named org a counterparty can pin, see")} ${bold("scopeblind.com/enroll")}
|
|
2414
|
+
`);
|
|
2415
|
+
}
|
|
2399
2416
|
} else {
|
|
2400
2417
|
process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
|
|
2401
2418
|
`);
|
|
@@ -2405,9 +2422,135 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
2405
2422
|
`);
|
|
2406
2423
|
process.exit(0);
|
|
2407
2424
|
}
|
|
2425
|
+
async function handleAnchorRecord(argv) {
|
|
2426
|
+
const { readFileSync, existsSync, appendFileSync } = await import("fs");
|
|
2427
|
+
const { join } = await import("path");
|
|
2428
|
+
const { anchorRecordCheckpoint, buildRecordCheckpoint, lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
|
|
2429
|
+
let dir = process.cwd();
|
|
2430
|
+
const di = argv.indexOf("--dir");
|
|
2431
|
+
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
2432
|
+
const li = argv.indexOf("--log");
|
|
2433
|
+
const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
|
|
2434
|
+
const keyPath = join(dir, "keys", "gateway.json");
|
|
2435
|
+
if (!existsSync(keyPath)) {
|
|
2436
|
+
process.stderr.write(`
|
|
2437
|
+
${bold("protect-mcp anchor-record")}
|
|
2438
|
+
|
|
2439
|
+
No signing key at ${keyPath}. A checkpoint must be signed. Run ${bold("npx protect-mcp init")} first.
|
|
2440
|
+
|
|
2441
|
+
`);
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
let key;
|
|
2446
|
+
try {
|
|
2447
|
+
key = JSON.parse(readFileSync(keyPath, "utf-8"));
|
|
2448
|
+
} catch {
|
|
2449
|
+
process.stderr.write(`
|
|
2450
|
+
protect-mcp anchor-record: ${keyPath} is not valid JSON.
|
|
2451
|
+
|
|
2452
|
+
`);
|
|
2453
|
+
process.exit(1);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
if (!key.privateKey || !key.publicKey) {
|
|
2457
|
+
process.stderr.write(`
|
|
2458
|
+
protect-mcp anchor-record: ${keyPath} is missing privateKey/publicKey.
|
|
2459
|
+
|
|
2460
|
+
`);
|
|
2461
|
+
process.exit(1);
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const recPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
2465
|
+
if (!existsSync(recPath)) {
|
|
2466
|
+
process.stderr.write(`
|
|
2467
|
+
${bold("protect-mcp anchor-record")}
|
|
2468
|
+
|
|
2469
|
+
No signed receipts in ${dir}. Run the gate with signing on, then try again.
|
|
2470
|
+
|
|
2471
|
+
`);
|
|
2472
|
+
process.exit(0);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
const receipts = readFileSync(recPath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
2476
|
+
try {
|
|
2477
|
+
return JSON.parse(l);
|
|
2478
|
+
} catch {
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
}).filter((x) => x !== null);
|
|
2482
|
+
if (!receipts.length) {
|
|
2483
|
+
process.stderr.write(`
|
|
2484
|
+
protect-mcp anchor-record: no readable receipts in ${recPath}.
|
|
2485
|
+
|
|
2486
|
+
`);
|
|
2487
|
+
process.exit(0);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const claimKey = { privateKey: key.privateKey, publicKey: key.publicKey, kid: key.kid || "gateway", issuer: "protect-mcp" };
|
|
2491
|
+
const historyPath = join(dir, ".protect-mcp-anchors.jsonl");
|
|
2492
|
+
const preview = buildRecordCheckpoint(receipts, claimKey, "preview");
|
|
2493
|
+
if (!argv.includes("--force") && existsSync(historyPath)) {
|
|
2494
|
+
const lines = readFileSync(historyPath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
2495
|
+
const last = lines.length ? (() => {
|
|
2496
|
+
try {
|
|
2497
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
2498
|
+
} catch {
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
})() : null;
|
|
2502
|
+
if (last && last.record_root === preview.record_root && last.total === preview.total) {
|
|
2503
|
+
process.stdout.write(`
|
|
2504
|
+
${bold("\u{1F6E1}\uFE0F Record checkpoint")}
|
|
2505
|
+
`);
|
|
2506
|
+
process.stdout.write(` Unchanged since entry ${bold("#" + last.seq)} ${dim("(" + last.total + " receipts, anchored " + (last.anchored_at || "") + ")")}. Nothing new to anchor.
|
|
2507
|
+
`);
|
|
2508
|
+
process.stdout.write(` ${dim("Use --force to re-anchor anyway.")}
|
|
2509
|
+
|
|
2510
|
+
`);
|
|
2511
|
+
process.exit(0);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
const res = await anchorRecordCheckpoint(receipts, claimKey, { log: logBase, issuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2516
|
+
process.stdout.write(`
|
|
2517
|
+
${bold("\u{1F6E1}\uFE0F Record checkpoint")}
|
|
2518
|
+
`);
|
|
2519
|
+
process.stdout.write(` ${res.total} receipts ${dim("\xB7")} root ${dim(res.record_root.slice(0, 16) + "\u2026")} ${dim("(" + res.checkpoint.from.slice(0, 10) + " \u2192 " + res.checkpoint.to.slice(0, 10) + ")")}
|
|
2520
|
+
`);
|
|
2521
|
+
if (!res.ok) {
|
|
2522
|
+
process.stdout.write(` ${yellow("Anchor failed")} ${dim("(" + (res.error || "unavailable") + "). Nothing was recorded; try again.")}
|
|
2523
|
+
|
|
2524
|
+
`);
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
appendFileSync(historyPath, JSON.stringify({ schema: res.checkpoint.schema, seq: res.seq, anchored_at: res.anchored_at, total: res.total, record_root: res.record_root, entry_url: res.entry_url, digest: res.checkpoint.digest }) + "\n");
|
|
2529
|
+
process.stdout.write(` ${green("Anchored")} as log entry ${bold("#" + res.seq)} ${dim(res.entry_url || "")}
|
|
2530
|
+
`);
|
|
2531
|
+
process.stdout.write(` ${dim("Only the root, count, and time range were sent. History: " + historyPath)}
|
|
2532
|
+
`);
|
|
2533
|
+
const who = await lookupPinnedIdentity(claimKey.publicKey, { log: logBase });
|
|
2534
|
+
if (who && who.found && !who.revoked) {
|
|
2535
|
+
process.stdout.write(` ${green("Identity:")} anchored as ${bold(who.name || who.slug || "enrolled org")} ${dim("(key pinned in the ScopeBlind directory)")}
|
|
2536
|
+
`);
|
|
2537
|
+
} else if (who && who.found && who.revoked) {
|
|
2538
|
+
process.stdout.write(` ${red("Identity: this key is REVOKED in the ScopeBlind directory.")}
|
|
2539
|
+
`);
|
|
2540
|
+
} else {
|
|
2541
|
+
process.stdout.write(` ${dim("Identity: anonymous (key not enrolled). Named identity: scopeblind.com/enroll")}
|
|
2542
|
+
`);
|
|
2543
|
+
}
|
|
2544
|
+
process.stdout.write(` ${dim("A claim whose commitment matches this root is provably over the complete record as of")}
|
|
2545
|
+
`);
|
|
2546
|
+
process.stdout.write(` ${dim("this checkpoint. Run this on a heartbeat (e.g. cron) to keep the anchored history growing.")}
|
|
2547
|
+
|
|
2548
|
+
`);
|
|
2549
|
+
process.exit(0);
|
|
2550
|
+
}
|
|
2408
2551
|
async function handleVerifyClaim(argv) {
|
|
2409
2552
|
const { readFileSync, existsSync } = await import("fs");
|
|
2410
|
-
const { verifyClaim } = await import("./claim-
|
|
2553
|
+
const { verifyClaim } = await import("./claim-RIXFOQM7.mjs");
|
|
2411
2554
|
const file = argv.find((a) => !a.startsWith("--"));
|
|
2412
2555
|
if (!file || !existsSync(file)) {
|
|
2413
2556
|
process.stderr.write(`
|
|
@@ -2459,7 +2602,7 @@ ${bold("protect-mcp verify-claim")}
|
|
|
2459
2602
|
const requireAnchor = argv.includes("--check-anchor");
|
|
2460
2603
|
let anchorOk = true;
|
|
2461
2604
|
if (existsSync(sidecarPath)) {
|
|
2462
|
-
const { checkClaimAnchor } = await import("./claim-
|
|
2605
|
+
const { checkClaimAnchor } = await import("./claim-RIXFOQM7.mjs");
|
|
2463
2606
|
let sidecar = null;
|
|
2464
2607
|
try {
|
|
2465
2608
|
sidecar = JSON.parse(readFileSync(sidecarPath, "utf-8"));
|
|
@@ -2491,6 +2634,21 @@ ${bold("protect-mcp verify-claim")}
|
|
|
2491
2634
|
process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
|
|
2492
2635
|
`);
|
|
2493
2636
|
}
|
|
2637
|
+
if (!argv.includes("--offline") && sidecar.envelope) {
|
|
2638
|
+
const { lookupPinnedIdentity } = await import("./claim-RIXFOQM7.mjs");
|
|
2639
|
+
const who = await lookupPinnedIdentity(sidecar.envelope.verification_key, {});
|
|
2640
|
+
if (who && who.found && !who.revoked) {
|
|
2641
|
+
process.stdout.write(` ${green("\u2713")} issuer key pinned to ${bold(who.name || who.slug || "an enrolled org")} ${dim("(ScopeBlind key directory)")}
|
|
2642
|
+
`);
|
|
2643
|
+
} else if (who && who.found && who.revoked) {
|
|
2644
|
+
anchorOk = false;
|
|
2645
|
+
process.stdout.write(` ${red("\u2717")} issuer key is REVOKED in the ScopeBlind key directory
|
|
2646
|
+
`);
|
|
2647
|
+
} else if (who && !who.found) {
|
|
2648
|
+
process.stdout.write(` ${dim("issuer key not enrolled (anonymous issuer); named identities pin via scopeblind.com/enroll")}
|
|
2649
|
+
`);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2494
2652
|
}
|
|
2495
2653
|
} else if (requireAnchor) {
|
|
2496
2654
|
anchorOk = false;
|
|
@@ -4018,6 +4176,10 @@ async function main() {
|
|
|
4018
4176
|
await handleVerifyClaim(args.slice(1));
|
|
4019
4177
|
return;
|
|
4020
4178
|
}
|
|
4179
|
+
if (args[0] === "anchor-record") {
|
|
4180
|
+
await handleAnchorRecord(args.slice(1));
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4021
4183
|
if (args[0] === "init-hooks") {
|
|
4022
4184
|
await handleInitHooks(args.slice(1));
|
|
4023
4185
|
process.exit(0);
|
package/dist/hook-server.js
CHANGED
|
@@ -1082,7 +1082,11 @@ var RULES = [
|
|
|
1082
1082
|
{ cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
|
|
1083
1083
|
{ cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
|
|
1084
1084
|
{ cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
|
|
1085
|
-
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
|
|
1085
|
+
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
|
|
1086
|
+
// Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
|
|
1087
|
+
// only makes a `claim --no payment` harder to assert (conservative); a false
|
|
1088
|
+
// negative would let a real payment escape the record's payment claims.
|
|
1089
|
+
{ cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
|
|
1086
1090
|
];
|
|
1087
1091
|
function deriveCapabilities(tool, input) {
|
|
1088
1092
|
const t = String(tool || "").toLowerCase();
|
|
@@ -1116,6 +1120,33 @@ function deriveResource(input) {
|
|
|
1116
1120
|
}
|
|
1117
1121
|
return void 0;
|
|
1118
1122
|
}
|
|
1123
|
+
function findField(input, names, depth = 0) {
|
|
1124
|
+
if (depth > 4 || input === null || typeof input !== "object") return void 0;
|
|
1125
|
+
const o = input;
|
|
1126
|
+
const keys = Object.keys(o).sort();
|
|
1127
|
+
for (const k of keys) {
|
|
1128
|
+
if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
|
|
1129
|
+
}
|
|
1130
|
+
for (const k of keys) {
|
|
1131
|
+
const v = findField(o[k], names, depth + 1);
|
|
1132
|
+
if (v !== void 0) return v;
|
|
1133
|
+
}
|
|
1134
|
+
return void 0;
|
|
1135
|
+
}
|
|
1136
|
+
function derivePayment(tool, input) {
|
|
1137
|
+
if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
|
|
1138
|
+
const p = { amount: null, asset: null, recipient_digest: null };
|
|
1139
|
+
const amt = findField(input, ["amount"]);
|
|
1140
|
+
if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
|
|
1141
|
+
else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
|
|
1142
|
+
const asset = findField(input, ["asset", "currency", "token"]);
|
|
1143
|
+
if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
|
|
1144
|
+
const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
|
|
1145
|
+
if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex(to.trim().toLowerCase());
|
|
1146
|
+
const scheme = findField(input, ["scheme"]);
|
|
1147
|
+
if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
|
|
1148
|
+
return p;
|
|
1149
|
+
}
|
|
1119
1150
|
function buildEnrichment(tool, input) {
|
|
1120
1151
|
const e = {
|
|
1121
1152
|
v: ENRICHMENT_VERSION,
|
|
@@ -1124,6 +1155,8 @@ function buildEnrichment(tool, input) {
|
|
|
1124
1155
|
};
|
|
1125
1156
|
const resource = deriveResource(input);
|
|
1126
1157
|
if (resource) e.resource = resource;
|
|
1158
|
+
const payment = derivePayment(tool, input);
|
|
1159
|
+
if (payment) e.payment = payment;
|
|
1127
1160
|
return e;
|
|
1128
1161
|
}
|
|
1129
1162
|
|
package/dist/hook-server.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -40405,7 +40405,11 @@ var RULES = [
|
|
|
40405
40405
|
{ cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
|
|
40406
40406
|
{ cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
|
|
40407
40407
|
{ cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
|
|
40408
|
-
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
|
|
40408
|
+
{ cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
|
|
40409
|
+
// Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
|
|
40410
|
+
// only makes a `claim --no payment` harder to assert (conservative); a false
|
|
40411
|
+
// negative would let a real payment escape the record's payment claims.
|
|
40412
|
+
{ cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
|
|
40409
40413
|
];
|
|
40410
40414
|
function deriveCapabilities(tool, input) {
|
|
40411
40415
|
const t = String(tool || "").toLowerCase();
|
|
@@ -40439,6 +40443,33 @@ function deriveResource(input) {
|
|
|
40439
40443
|
}
|
|
40440
40444
|
return void 0;
|
|
40441
40445
|
}
|
|
40446
|
+
function findField(input, names, depth = 0) {
|
|
40447
|
+
if (depth > 4 || input === null || typeof input !== "object") return void 0;
|
|
40448
|
+
const o = input;
|
|
40449
|
+
const keys = Object.keys(o).sort();
|
|
40450
|
+
for (const k of keys) {
|
|
40451
|
+
if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
|
|
40452
|
+
}
|
|
40453
|
+
for (const k of keys) {
|
|
40454
|
+
const v = findField(o[k], names, depth + 1);
|
|
40455
|
+
if (v !== void 0) return v;
|
|
40456
|
+
}
|
|
40457
|
+
return void 0;
|
|
40458
|
+
}
|
|
40459
|
+
function derivePayment(tool, input) {
|
|
40460
|
+
if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
|
|
40461
|
+
const p = { amount: null, asset: null, recipient_digest: null };
|
|
40462
|
+
const amt = findField(input, ["amount"]);
|
|
40463
|
+
if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
|
|
40464
|
+
else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
|
|
40465
|
+
const asset = findField(input, ["asset", "currency", "token"]);
|
|
40466
|
+
if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
|
|
40467
|
+
const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
|
|
40468
|
+
if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex(to.trim().toLowerCase());
|
|
40469
|
+
const scheme = findField(input, ["scheme"]);
|
|
40470
|
+
if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
|
|
40471
|
+
return p;
|
|
40472
|
+
}
|
|
40442
40473
|
function buildEnrichment(tool, input) {
|
|
40443
40474
|
const e = {
|
|
40444
40475
|
v: ENRICHMENT_VERSION,
|
|
@@ -40447,6 +40478,8 @@ function buildEnrichment(tool, input) {
|
|
|
40447
40478
|
};
|
|
40448
40479
|
const resource = deriveResource(input);
|
|
40449
40480
|
if (resource) e.resource = resource;
|
|
40481
|
+
const payment = derivePayment(tool, input);
|
|
40482
|
+
if (payment) e.payment = payment;
|
|
40450
40483
|
return e;
|
|
40451
40484
|
}
|
|
40452
40485
|
|
package/dist/index.mjs
CHANGED
|
@@ -54,8 +54,8 @@ import {
|
|
|
54
54
|
forwardReceipt,
|
|
55
55
|
getScopeBlindBridge,
|
|
56
56
|
startHookServer
|
|
57
|
-
} from "./chunk-
|
|
58
|
-
import "./chunk-
|
|
57
|
+
} from "./chunk-H332ZNJ6.mjs";
|
|
58
|
+
import "./chunk-WWPQNIVF.mjs";
|
|
59
59
|
import {
|
|
60
60
|
sha256 as sha2562
|
|
61
61
|
} from "./chunk-AYNQIEN7.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"mcpName": "com.scopeblind/protect-mcp",
|
|
5
5
|
"description": "Fail-closed Cedar policy gate + Ed25519 signed receipts for AI agent tool calls. Denies on any policy error, proves the gate is live with a startup self-test, and turns every decision into a local searchable record you own. The open gate behind Legate by ScopeBlind. scopeblind.com",
|
|
6
6
|
"main": "dist/index.js",
|