protect-mcp 0.9.2 → 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 +49 -0
- package/README.md +41 -0
- package/dist/{chunk-CXW2EIRM.mjs → chunk-7MHK5RF4.mjs} +2 -2
- package/dist/{chunk-744JMCY4.mjs → chunk-H332ZNJ6.mjs} +2 -2
- package/dist/{chunk-GHR65WVD.mjs → chunk-PB3TC7E3.mjs} +1 -1
- package/dist/{chunk-KMNXHGGT.mjs → chunk-WWPQNIVF.mjs} +35 -2
- package/dist/{chunk-IDUH2O4Q.mjs → chunk-XLJUZ4WO.mjs} +7 -1
- package/dist/{claim-TUDH2WPB.mjs → claim-RIXFOQM7.mjs} +168 -16
- package/dist/cli.js +471 -33
- package/dist/cli.mjs +268 -21
- package/dist/hook-server.js +42 -3
- package/dist/hook-server.mjs +3 -3
- package/dist/{http-transport-D7C64PIA.mjs → http-transport-HLSMVBI6.mjs} +2 -2
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +42 -3
- package/dist/index.mjs +5 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -456,7 +456,13 @@ function signDecision(entry) {
|
|
|
456
456
|
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
457
457
|
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
458
458
|
// - uncertified = unsigned receipt (shadow mode, no signing configured)
|
|
459
|
-
issuer_certification: signerState ? "self-signed" : "uncertified"
|
|
459
|
+
issuer_certification: signerState ? "self-signed" : "uncertified",
|
|
460
|
+
// The signer's PUBLIC key, inside the signed payload, so a receipt is
|
|
461
|
+
// self-contained: any verifier (including the record viewer, in-browser)
|
|
462
|
+
// can check the signature without a side channel. Binding the key inside
|
|
463
|
+
// the signature means it cannot be swapped without breaking the signature;
|
|
464
|
+
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
465
|
+
public_key: signerState.publicKey
|
|
460
466
|
};
|
|
461
467
|
if (entry.tier) payload.tier = entry.tier;
|
|
462
468
|
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
@@ -5140,6 +5146,33 @@ function deriveResource(input) {
|
|
|
5140
5146
|
}
|
|
5141
5147
|
return void 0;
|
|
5142
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
|
+
}
|
|
5143
5176
|
function buildEnrichment(tool, input) {
|
|
5144
5177
|
const e = {
|
|
5145
5178
|
v: ENRICHMENT_VERSION,
|
|
@@ -5148,6 +5181,8 @@ function buildEnrichment(tool, input) {
|
|
|
5148
5181
|
};
|
|
5149
5182
|
const resource = deriveResource(input);
|
|
5150
5183
|
if (resource) e.resource = resource;
|
|
5184
|
+
const payment = derivePayment(tool, input);
|
|
5185
|
+
if (payment) e.payment = payment;
|
|
5151
5186
|
return e;
|
|
5152
5187
|
}
|
|
5153
5188
|
var ENRICHMENT_VERSION, RULES;
|
|
@@ -5156,7 +5191,7 @@ var init_receipt_enrichment = __esm({
|
|
|
5156
5191
|
"use strict";
|
|
5157
5192
|
init_sha256();
|
|
5158
5193
|
init_utils();
|
|
5159
|
-
ENRICHMENT_VERSION =
|
|
5194
|
+
ENRICHMENT_VERSION = 2;
|
|
5160
5195
|
RULES = [
|
|
5161
5196
|
{ cap: "exec.shell", tool: /bash|shell|exec|terminal|run_command|command/ },
|
|
5162
5197
|
{ cap: "fs.read", tool: /(^|[_.])(read|cat|glob|grep|search|ls|view|list_files|open)/ },
|
|
@@ -5168,7 +5203,11 @@ var init_receipt_enrichment = __esm({
|
|
|
5168
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/ },
|
|
5169
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/ },
|
|
5170
5205
|
{ cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
|
|
5171
|
-
{ 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/ }
|
|
5172
5211
|
];
|
|
5173
5212
|
}
|
|
5174
5213
|
});
|
|
@@ -5177,16 +5216,22 @@ var init_receipt_enrichment = __esm({
|
|
|
5177
5216
|
var claim_exports = {};
|
|
5178
5217
|
__export(claim_exports, {
|
|
5179
5218
|
ANCHOR_SCHEMA: () => ANCHOR_SCHEMA,
|
|
5219
|
+
CHECKPOINT_SCHEMA: () => CHECKPOINT_SCHEMA,
|
|
5180
5220
|
CLAIM_TYPE: () => CLAIM_TYPE,
|
|
5181
5221
|
DEFAULT_LOG: () => DEFAULT_LOG,
|
|
5182
5222
|
anchorClaim: () => anchorClaim,
|
|
5223
|
+
anchorRecordCheckpoint: () => anchorRecordCheckpoint,
|
|
5183
5224
|
buildAnchorEnvelope: () => buildAnchorEnvelope,
|
|
5184
5225
|
buildClaim: () => buildClaim,
|
|
5226
|
+
buildRecordCheckpoint: () => buildRecordCheckpoint,
|
|
5227
|
+
checkClaimAnchor: () => checkClaimAnchor,
|
|
5185
5228
|
claimDigest: () => claimDigest,
|
|
5186
5229
|
evaluate: () => evaluate,
|
|
5187
5230
|
leafHash: () => leafHash,
|
|
5231
|
+
lookupPinnedIdentity: () => lookupPinnedIdentity,
|
|
5188
5232
|
merkleRoot: () => merkleRoot,
|
|
5189
5233
|
receiptToLeaf: () => receiptToLeaf,
|
|
5234
|
+
verifyAnchorEnvelope: () => verifyAnchorEnvelope,
|
|
5190
5235
|
verifyClaim: () => verifyClaim
|
|
5191
5236
|
});
|
|
5192
5237
|
function sha256Hex3(input) {
|
|
@@ -5203,7 +5248,13 @@ function receiptToLeaf(e) {
|
|
|
5203
5248
|
const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
5204
5249
|
const t = isFinite(ms) ? new Date(ms).toISOString() : "";
|
|
5205
5250
|
const d = sha256Hex3(canonicalJson(e));
|
|
5206
|
-
|
|
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;
|
|
5207
5258
|
}
|
|
5208
5259
|
function leafHash(leaf) {
|
|
5209
5260
|
return sha256Hex3(canonicalJson(leaf));
|
|
@@ -5236,6 +5287,10 @@ function evaluate(pred, leaves) {
|
|
|
5236
5287
|
const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
|
|
5237
5288
|
return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
|
|
5238
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
|
+
}
|
|
5239
5294
|
const matched = leaves.filter((l) => l.v === pred.verdict).length;
|
|
5240
5295
|
return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
|
|
5241
5296
|
}
|
|
@@ -5327,11 +5382,82 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
|
|
|
5327
5382
|
const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
|
|
5328
5383
|
return { ...signed, signature, digest };
|
|
5329
5384
|
}
|
|
5330
|
-
|
|
5331
|
-
const
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5385
|
+
function verifyAnchorEnvelope(pack, envelope) {
|
|
5386
|
+
const reasons = [];
|
|
5387
|
+
if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
|
|
5388
|
+
return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
|
|
5389
|
+
}
|
|
5390
|
+
const expected = claimDigest(pack);
|
|
5391
|
+
if (envelope.claim_digest !== expected) {
|
|
5392
|
+
reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
|
|
5393
|
+
}
|
|
5394
|
+
if (envelope.record_root !== pack.record.root) {
|
|
5395
|
+
reasons.push("anchored envelope commits to a different record root");
|
|
5396
|
+
}
|
|
5397
|
+
if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
|
|
5398
|
+
reasons.push("anchor was signed by a different key than the claim issuer");
|
|
5399
|
+
}
|
|
5400
|
+
try {
|
|
5401
|
+
const { signature, digest, ...signed } = envelope;
|
|
5402
|
+
const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
|
|
5403
|
+
if (bytesToHex(hash) !== String(digest).toLowerCase()) {
|
|
5404
|
+
reasons.push("envelope digest does not match its contents");
|
|
5405
|
+
} else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
|
|
5406
|
+
reasons.push("envelope signature does not verify");
|
|
5407
|
+
}
|
|
5408
|
+
} catch {
|
|
5409
|
+
reasons.push("envelope signature does not verify");
|
|
5410
|
+
}
|
|
5411
|
+
return { ok: reasons.length === 0, reasons };
|
|
5412
|
+
}
|
|
5413
|
+
async function checkClaimAnchor(pack, sidecar, opts) {
|
|
5414
|
+
const reasons = [];
|
|
5415
|
+
const envelope = sidecar && sidecar.envelope;
|
|
5416
|
+
if (!envelope) {
|
|
5417
|
+
return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
|
|
5418
|
+
}
|
|
5419
|
+
const local = verifyAnchorEnvelope(pack, envelope);
|
|
5420
|
+
reasons.push(...local.reasons);
|
|
5421
|
+
const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
5422
|
+
const out = {
|
|
5423
|
+
local_ok: local.ok,
|
|
5424
|
+
log_ok: null,
|
|
5425
|
+
seq: sidecar.seq,
|
|
5426
|
+
anchored_at: sidecar.anchored_at,
|
|
5427
|
+
entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
|
|
5428
|
+
reasons
|
|
5429
|
+
};
|
|
5430
|
+
if (opts?.offline) return out;
|
|
5431
|
+
const doFetch = opts?.fetchImpl || globalThis.fetch;
|
|
5432
|
+
if (!doFetch) return out;
|
|
5433
|
+
try {
|
|
5434
|
+
const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
|
|
5435
|
+
const data = await resp.json().catch(() => null);
|
|
5436
|
+
if (!resp.ok || !data) {
|
|
5437
|
+
out.log_ok = null;
|
|
5438
|
+
return out;
|
|
5439
|
+
}
|
|
5440
|
+
if (data.anchored !== true) {
|
|
5441
|
+
out.log_ok = false;
|
|
5442
|
+
out.reasons.push("the public log does not contain this anchor digest");
|
|
5443
|
+
return out;
|
|
5444
|
+
}
|
|
5445
|
+
if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
|
|
5446
|
+
out.log_ok = false;
|
|
5447
|
+
out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
|
|
5448
|
+
return out;
|
|
5449
|
+
}
|
|
5450
|
+
out.log_ok = true;
|
|
5451
|
+
if (typeof data.seq === "number") out.seq = data.seq;
|
|
5452
|
+
return out;
|
|
5453
|
+
} catch {
|
|
5454
|
+
out.log_ok = null;
|
|
5455
|
+
return out;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
async function submitEnvelope(envelope, base, fetchImpl) {
|
|
5459
|
+
const doFetch = fetchImpl || globalThis.fetch;
|
|
5460
|
+
if (!doFetch) return { ok: false, error: "fetch_unavailable" };
|
|
5335
5461
|
const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
|
|
5336
5462
|
try {
|
|
5337
5463
|
const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
|
|
@@ -5341,22 +5467,86 @@ async function anchorClaim(pack, key, opts) {
|
|
|
5341
5467
|
});
|
|
5342
5468
|
const data = await resp.json().catch(() => null);
|
|
5343
5469
|
if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
|
|
5344
|
-
return { ok: false,
|
|
5470
|
+
return { ok: false, error: data && data.error || `http_${resp.status}` };
|
|
5345
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 };
|
|
5346
5537
|
return {
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
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)
|
|
5354
5544
|
};
|
|
5355
5545
|
} catch {
|
|
5356
|
-
return
|
|
5546
|
+
return null;
|
|
5357
5547
|
}
|
|
5358
5548
|
}
|
|
5359
|
-
var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG;
|
|
5549
|
+
var CLAIM_TYPE, ANCHOR_SCHEMA, DEFAULT_LOG, CHECKPOINT_SCHEMA;
|
|
5360
5550
|
var init_claim = __esm({
|
|
5361
5551
|
"src/claim.ts"() {
|
|
5362
5552
|
"use strict";
|
|
@@ -5367,6 +5557,7 @@ var init_claim = __esm({
|
|
|
5367
5557
|
CLAIM_TYPE = "scopeblind.claim.v1";
|
|
5368
5558
|
ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
|
|
5369
5559
|
DEFAULT_LOG = "https://scopeblind.com";
|
|
5560
|
+
CHECKPOINT_SCHEMA = "scopeblind.protect-mcp.record-checkpoint.v1";
|
|
5370
5561
|
}
|
|
5371
5562
|
});
|
|
5372
5563
|
|
|
@@ -8630,8 +8821,12 @@ Commands:
|
|
|
8630
8821
|
digest Generate a human-readable summary of agent activity
|
|
8631
8822
|
receipts Show recent persisted signed receipts
|
|
8632
8823
|
record Open a local, searchable view of your record in the browser
|
|
8633
|
-
claim Attest a signed, position-blind claim over your record (e.g. no egress
|
|
8634
|
-
|
|
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)
|
|
8635
8830
|
bundle Export an offline-verifiable audit bundle
|
|
8636
8831
|
|
|
8637
8832
|
Examples:
|
|
@@ -10563,6 +10758,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
10563
10758
|
return null;
|
|
10564
10759
|
}
|
|
10565
10760
|
}).filter((x) => x !== null).map(mapRecordEntry);
|
|
10761
|
+
let pinnedKey = "";
|
|
10762
|
+
let pinnedKid = "";
|
|
10763
|
+
try {
|
|
10764
|
+
const kd = JSON.parse(readFileSync11(join8(dir, "keys", "gateway.json"), "utf-8"));
|
|
10765
|
+
if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
|
|
10766
|
+
pinnedKey = kd.publicKey;
|
|
10767
|
+
pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
|
|
10768
|
+
}
|
|
10769
|
+
} catch {
|
|
10770
|
+
}
|
|
10566
10771
|
const openTarget = (target) => {
|
|
10567
10772
|
if (argv.includes("--no-open")) return;
|
|
10568
10773
|
const platform = process.platform;
|
|
@@ -10590,7 +10795,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
10590
10795
|
res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
|
|
10591
10796
|
return;
|
|
10592
10797
|
}
|
|
10593
|
-
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
|
|
10798
|
+
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
10594
10799
|
const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
|
|
10595
10800
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
10596
10801
|
res.end(page);
|
|
@@ -10615,7 +10820,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
|
|
|
10615
10820
|
return;
|
|
10616
10821
|
}
|
|
10617
10822
|
const recs = readRecs(chosen);
|
|
10618
|
-
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
|
|
10823
|
+
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
10619
10824
|
const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
|
|
10620
10825
|
const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
|
|
10621
10826
|
writeFileSync4(out, html);
|
|
@@ -10681,6 +10886,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
10681
10886
|
.badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
|
|
10682
10887
|
.badge.sgn{background:var(--gb);color:var(--g)}
|
|
10683
10888
|
.badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
|
|
10889
|
+
.badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
|
|
10890
|
+
.badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
|
|
10891
|
+
.stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
|
|
10684
10892
|
.dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
10685
10893
|
.when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
10686
10894
|
.det{margin-top:8px;padding-top:8px;border-top:1px solid var(--line);font-size:12px;color:var(--soft);font-family:ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all;display:none}
|
|
@@ -10727,6 +10935,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
10727
10935
|
</div>
|
|
10728
10936
|
<script>
|
|
10729
10937
|
var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
|
|
10938
|
+
// In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
|
|
10939
|
+
// preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
|
|
10940
|
+
// signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
|
|
10941
|
+
// public half, injected by the CLI) = authenticity; key embedded in the receipt
|
|
10942
|
+
// payload (0.9.3+) = self-consistency. Everything runs locally.
|
|
10943
|
+
var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
|
|
10944
|
+
function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
|
|
10945
|
+
function hexb(h){h=String(h||"");var a=new Uint8Array(h.length>>1);for(var i=0;i<a.length;i++)a[i]=parseInt(h.substr(i*2,2),16);return a}
|
|
10946
|
+
function canon(v){return JSON.stringify(v,function(k,x){if(x&&typeof x==="object"&&!Array.isArray(x)){var s={},ks=Object.keys(x).sort();for(var i=0;i<ks.length;i++)s[ks[i]]=x[ks[i]];return s}return x})}
|
|
10947
|
+
async function edv(sig,msg,pub){var key=await crypto.subtle.importKey("raw",hexb(pub),{name:"Ed25519"},false,["verify"]);return crypto.subtle.verify({name:"Ed25519"},key,hexb(sig),msg)}
|
|
10948
|
+
async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
|
|
10949
|
+
var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
|
|
10950
|
+
var msg=new TextEncoder().encode(canon(rest));
|
|
10951
|
+
var pin=String(META.pinned_key||"").toLowerCase();
|
|
10952
|
+
var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
|
|
10953
|
+
if(!/^[0-9a-f]{64}$/.test(emb))emb="";
|
|
10954
|
+
if(pin){if(await edv(raw.signature,msg,pin))return"ok";if(emb&&emb!==pin&&await edv(raw.signature,msg,emb))return"foreign";return"bad"}
|
|
10955
|
+
if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
|
|
10956
|
+
return"nokey"}
|
|
10957
|
+
function vsum(){var s={ok:0,bad:0,foreign:0,nokey:0};RECORDS.forEach(function(r){if(!r.signed)return;var v=VSTATE[vkey(r)];if(v&&s[v]!==undefined)s[v]++});return s}
|
|
10958
|
+
async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
|
|
10959
|
+
try{var rows=RECORDS.slice(0,1500);for(var i=0;i<rows.length;i++){var r=rows[i],kk=vkey(r);if(VSTATE[kk])continue;
|
|
10960
|
+
try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
|
|
10961
|
+
finally{VDONE=true;VBUSY=false;render()}}
|
|
10730
10962
|
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
10731
10963
|
function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
|
|
10732
10964
|
function when(ts){if(!ts)return"";var d=new Date(ts);return d.toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+d.toLocaleTimeString(undefined,{hour:"2-digit",minute:"2-digit"})}
|
|
@@ -10740,8 +10972,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
|
|
|
10740
10972
|
function exportMd(){var rows=filtered();if(!rows.length)return;var c=counts(rows);var head=["# Agent decision record","",rows.length+" decisions from "+META.file,c.allowed+" allowed, "+c.held+" held, "+c.blocked+" blocked, "+c.signed+" signed.","","Generated locally by protect-mcp. These are signed receipts; verify offline with npx @veritasacta/verify (our code removed).","","| When | Decision | Tool | Reason | Hook | Signed |","|---|---|---|---|---|---|"];var body=rows.slice(0,3000).map(function(r){return "| "+(r.ts||"")+" | "+vlabel(r.verdict)+" | "+String(r.tool||"").replace(/\\|/g,"/")+" | "+String(r.reason||"").replace(/\\|/g,"/")+" | "+(r.hook||"")+" | "+(r.signed?"yes":"no")+" |"});dl("protect-mcp-record-"+stamp()+".md",head.concat(body).join(NL)+NL,"text/markdown")}
|
|
10741
10973
|
function copyAttest(){var a=document.getElementById("attest");var cmd=a?a.getAttribute("data-cmd"):"";try{navigator.clipboard&&cmd&&navigator.clipboard.writeText(cmd)}catch(e){}var b=document.getElementById("cpa");if(b){var t=b.textContent;b.textContent="Copied";setTimeout(function(){b.textContent=t},1200)}}
|
|
10742
10974
|
function copyVerify(){var cmd="npx @veritasacta/verify";try{navigator.clipboard&&navigator.clipboard.writeText(cmd)}catch(e){}var b=document.getElementById("cpv");if(b){var t=b.textContent;b.textContent="Copied";setTimeout(function(){b.textContent=t},1200)}}
|
|
10743
|
-
function renderStats(){var c=counts(RECORDS);var p=[];p.push('<span class="stat"><b>'+RECORDS.length+'</b> decisions</span>');p.push('<span class="stat"><span class="dot g"></span>'+c.allowed+' allowed</span>');if(c.held)p.push('<span class="stat"><span class="dot a"></span>'+c.held+' held</span>');p.push('<span class="stat"><span class="dot r"></span>'+c.blocked+' blocked</span>');
|
|
10744
|
-
|
|
10975
|
+
function renderStats(){var c=counts(RECORDS);var p=[];p.push('<span class="stat"><b>'+RECORDS.length+'</b> decisions</span>');p.push('<span class="stat"><span class="dot g"></span>'+c.allowed+' allowed</span>');if(c.held)p.push('<span class="stat"><span class="dot a"></span>'+c.held+' held</span>');p.push('<span class="stat"><span class="dot r"></span>'+c.blocked+' blocked</span>');var st;if(!c.signed){st='0 signed, verifiable offline'}else if(VUNSUP||!(window.crypto&&crypto.subtle)){st=c.signed+' signed, verifiable offline <span class="dim2">(in-browser check unavailable here; run npx protect-mcp receipts)</span>'}else if(!VDONE){st=c.signed+' signed \xB7 verifying in your browser\u2026'}else{var s=vsum();st=s.ok+' of '+c.signed+' signatures verified in your browser';if(s.foreign)st+=' <span class="warnk">\xB7 '+s.foreign+' signed by an unpinned key</span>';if(s.bad)st+=' <span class="badk">\xB7 '+s.bad+' INVALID</span>';if(s.nokey)st+=' <span class="dim2">\xB7 '+s.nokey+' need a key to check</span>'}
|
|
10976
|
+
p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
|
|
10977
|
+
function renderList(rows){var html="";rows.slice(0,800).forEach(function(r){var vs=VSTATE[vkey(r)];var sig=!r.signed?'<span class="badge log">log</span>':vs==="ok"?'<span class="badge sgn">\u2713 verified</span>':vs==="bad"?'<span class="badge vbad">\u2717 invalid signature</span>':vs==="foreign"?'<span class="badge vfor">signed \xB7 unpinned key</span>':'<span class="badge sgn">signed</span>';var dg=r.digest?'<span class="dg">'+esc(String(r.digest).slice(0,10))+'</span>':'';var ct=(r.caps||[]).map(function(c){return '<span class="cap">'+esc(c)+'</span>'}).join('');var rk="row:"+(r.id||"")+"|"+(r.ts||"");html+='<div class="row '+r.verdict+(OPEN[rk]?" open":"")+'" data-k="'+esc(rk)+'"><div class="top"><span class="pill '+r.verdict+'">'+vlabel(r.verdict)+"</span><b>"+esc(r.tool)+'</b><span class="tag">'+esc(r.reason)+"</span>"+ct+(r.hook?'<span class="tag">'+esc(r.hook)+"</span>":"")+sig+dg+'<span class="when">'+esc(when(r.ts))+'</span></div><div class="det">'+esc(JSON.stringify(r.raw||r,null,2))+"</div></div>"});document.getElementById("list").innerHTML=html||'<p style="color:#8a837a">No records match.</p>';}
|
|
10745
10978
|
function isLifecycle(r){var h=r.hook||"";return h==="SessionStart"||h==="SessionEnd"||h==="Stop"||h==="SubagentStart"||h==="SubagentStop"||h==="TaskCreated"||h==="TaskCompleted"||h==="ConfigChange"||h==="Notification"||h==="PreCompact";}
|
|
10746
10979
|
function buildTree(rows){var ags={},order=[];rows.forEach(function(r){var a=r.agent||"main agent";if(!ags[a]){ags[a]={name:a,byId:{},items:[],caps:{},blocked:0,actions:0};order.push(a);}var g=ags[a];(r.caps||[]).forEach(function(c){g.caps[c]=(g.caps[c]||0)+1;});if(isLifecycle(r)){g.items.push({t:"e",ts:r.ts,r:r});return;}var id=r.id||("_"+r.ts);var n=g.byId[id];if(!n){n={t:"a",id:id,tool:r.tool,verdict:r.verdict,caps:(r.caps||[]).slice(),ts:r.ts,dur:0,signed:!!r.signed,raw:r.raw};g.byId[id]=n;g.items.push(n);g.actions++;}if(r.hook==="PostToolUse"){if(r.dur)n.dur=r.dur;if(!n.raw)n.raw=r.raw;}else{n.verdict=r.verdict;if((r.caps||[]).length)n.caps=r.caps.slice();n.raw=r.raw;n.ts=r.ts;}if(r.signed)n.signed=true;});order.forEach(function(a){var g=ags[a];g.blocked=g.items.filter(function(it){return it.t==="a"&&it.verdict==="blocked";}).length;g.items.sort(function(x,y){return (x.ts<y.ts)?-1:1;});});return order.map(function(a){return ags[a];});}
|
|
10747
10980
|
function renderTree(ags){if(!ags.length){document.getElementById("list").innerHTML='<p style="color:#8a837a">No records match.</p>';return;}var html="",N=0;ags.forEach(function(g,gi){var capstr=Object.keys(g.caps).sort(function(a,b){return g.caps[b]-g.caps[a];}).slice(0,5).map(function(c){return '<span class="cap">'+esc(c)+'</span>';}).join('');var ak="ag:"+g.name;var op=(OPEN.hasOwnProperty(ak)?OPEN[ak]:(ags.length===1||gi===0))?" open":"";html+='<div class="agent'+op+'" data-k="'+esc(ak)+'"><div class="ahead"><span class="atwist">\u25B8</span><b>'+esc(g.name)+'</b><span class="acount">'+g.actions+' action'+(g.actions===1?'':'s')+'</span>'+(g.blocked?'<span class="badge blk">'+g.blocked+' blocked</span>':'')+capstr+'</div><div class="akids">';g.items.forEach(function(it){if(N++>1500)return;if(it.t==="e"){var r=it.r;html+='<div class="ev"><span class="evdot"></span>'+esc(r.hook||r.tool)+' <span class="evre">'+esc(r.reason)+'</span><span class="when">'+esc(when(r.ts))+'</span></div>';}else{var ct=(it.caps||[]).map(function(c){return '<span class="cap">'+esc(c)+'</span>';}).join('');var dur=it.dur?'<span class="dg">'+it.dur+'ms</span>':'';var ik="act:"+it.id;html+='<div class="act '+it.verdict+(OPEN[ik]?" open":"")+'" data-k="'+esc(ik)+'"><span class="pill '+it.verdict+'">'+vlabel(it.verdict)+'</span><b>'+esc(it.tool)+'</b>'+ct+(it.signed?'<span class="badge sgn">signed</span>':'')+dur+'<span class="when">'+esc(when(it.ts))+'</span><div class="det">'+esc(JSON.stringify(it.raw||{},null,2))+'</div></div>';}});html+='</div></div>';});if(N>1500)html+='<p style="color:#8a837a;font-size:12px;margin-top:10px">Showing the first 1500 items. Search or pick a facet to narrow.</p>';document.getElementById("list").innerHTML=html;}
|
|
@@ -10759,8 +10992,8 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
|
|
|
10759
10992
|
document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
|
|
10760
10993
|
document.getElementById("chips").addEventListener("click",function(e){var c=e.target.closest(".chip");if(!c)return;var k=c.getAttribute("data-k"),v=c.getAttribute("data-v");ACT[k]=ACT[k]===v?undefined:v;render()});
|
|
10761
10994
|
document.getElementById("list").addEventListener("click",function(e){var ah=e.target.closest(".ahead");if(ah){var ag=ah.parentNode;ag.classList.toggle("open");var ak=ag.getAttribute("data-k");if(ak)OPEN[ak]=ag.classList.contains("open");return;}var el=e.target.closest(".act")||e.target.closest(".row");if(el){el.classList.toggle("open");var k=el.getAttribute("data-k");if(k)OPEN[k]=el.classList.contains("open");}});
|
|
10762
|
-
render();
|
|
10763
|
-
if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){var nr=d.recs||[];var changed=nr.length!==RECORDS.length;RECORDS=nr;META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;if(changed)render()}).catch(function(){})};poll();setInterval(poll,2000);}
|
|
10995
|
+
render();kickVerify();
|
|
10996
|
+
if(META.live){var poll=function(){fetch('/data',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){var nr=d.recs||[];var changed=nr.length!==RECORDS.length;RECORDS=nr;META.count=RECORDS.length;if(typeof d.signed==='boolean')META.signed=d.signed;if(changed){render();kickVerify()}}).catch(function(){})};poll();setInterval(poll,2000);}
|
|
10764
10997
|
</script></body></html>`;
|
|
10765
10998
|
async function handleClaim(argv) {
|
|
10766
10999
|
const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
|
|
@@ -10770,20 +11003,23 @@ async function handleClaim(argv) {
|
|
|
10770
11003
|
const di = argv.indexOf("--dir");
|
|
10771
11004
|
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
10772
11005
|
let predicate = null;
|
|
10773
|
-
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");
|
|
10774
11007
|
if (noIdx !== -1 && argv[noIdx + 1]) predicate = { kind: "no_capability", capability: argv[noIdx + 1] };
|
|
10775
11008
|
else if (onlyIdx !== -1 && argv[onlyIdx + 1]) predicate = { kind: "only_capabilities", capabilities: argv[onlyIdx + 1].split(",").map((s) => s.trim()).filter(Boolean) };
|
|
10776
11009
|
else if (nvIdx !== -1 && argv[nvIdx + 1]) predicate = { kind: "no_verdict", verdict: argv[nvIdx + 1] };
|
|
10777
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]) };
|
|
10778
11012
|
if (!predicate) {
|
|
10779
11013
|
process.stderr.write(`
|
|
10780
11014
|
${bold("protect-mcp claim")}
|
|
10781
11015
|
|
|
10782
11016
|
Attest a signed, position-blind claim over your record:
|
|
10783
|
-
--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")}
|
|
10784
11018
|
--only <c1,c2,...> all actions confined to these capabilities
|
|
10785
11019
|
--no-verdict <verdict> e.g. ${dim("--no-verdict blocked")}
|
|
10786
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)
|
|
10787
11023
|
--anchor also record the claim digest in the public append-only
|
|
10788
11024
|
log so a counterparty can trust it is complete (only the
|
|
10789
11025
|
hash is sent; your record stays local)
|
|
@@ -10886,8 +11122,18 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
10886
11122
|
`);
|
|
10887
11123
|
process.stdout.write(` ${dim("Anchor record written to " + sidecar + ". Only the digest was sent; your record stayed local.")}
|
|
10888
11124
|
`);
|
|
10889
|
-
|
|
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) : "") + ")")}
|
|
10890
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")}
|
|
11135
|
+
`);
|
|
11136
|
+
}
|
|
10891
11137
|
} else {
|
|
10892
11138
|
process.stdout.write(` ${yellow("Anchor skipped")} ${dim("(" + (res.error || "unavailable") + "). The claim above is complete and verifiable offline without it.")}
|
|
10893
11139
|
`);
|
|
@@ -10897,6 +11143,132 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
10897
11143
|
`);
|
|
10898
11144
|
process.exit(0);
|
|
10899
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
|
+
}
|
|
10900
11272
|
async function handleVerifyClaim(argv) {
|
|
10901
11273
|
const { readFileSync: readFileSync11, existsSync: existsSync9 } = await import("fs");
|
|
10902
11274
|
const { verifyClaim: verifyClaim2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
@@ -10946,17 +11318,79 @@ ${bold("protect-mcp verify-claim")}
|
|
|
10946
11318
|
`);
|
|
10947
11319
|
process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
|
|
10948
11320
|
`);
|
|
11321
|
+
const ai = argv.indexOf("--anchor-file");
|
|
11322
|
+
const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
|
|
11323
|
+
const requireAnchor = argv.includes("--check-anchor");
|
|
11324
|
+
let anchorOk = true;
|
|
11325
|
+
if (existsSync9(sidecarPath)) {
|
|
11326
|
+
const { checkClaimAnchor: checkClaimAnchor2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
11327
|
+
let sidecar = null;
|
|
11328
|
+
try {
|
|
11329
|
+
sidecar = JSON.parse(readFileSync11(sidecarPath, "utf-8"));
|
|
11330
|
+
} catch {
|
|
11331
|
+
}
|
|
11332
|
+
if (!sidecar) {
|
|
11333
|
+
anchorOk = false;
|
|
11334
|
+
process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
|
|
11335
|
+
`);
|
|
11336
|
+
} else {
|
|
11337
|
+
const a = await checkClaimAnchor2(pack, sidecar, { offline: argv.includes("--offline") });
|
|
11338
|
+
anchorOk = a.local_ok && a.log_ok !== false;
|
|
11339
|
+
if (a.local_ok) {
|
|
11340
|
+
process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
|
|
11341
|
+
`);
|
|
11342
|
+
process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
|
|
11343
|
+
`);
|
|
11344
|
+
} else {
|
|
11345
|
+
for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
|
|
11346
|
+
`);
|
|
11347
|
+
}
|
|
11348
|
+
if (a.log_ok === true) {
|
|
11349
|
+
process.stdout.write(` ${green("\u2713")} public log confirms it${typeof a.seq === "number" ? ": entry " + bold("#" + a.seq) : ""}${a.anchored_at ? dim(" \xB7 anchored " + a.anchored_at) : ""}
|
|
11350
|
+
`);
|
|
11351
|
+
} else if (a.log_ok === false) {
|
|
11352
|
+
process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
|
|
11353
|
+
`);
|
|
11354
|
+
} else if (a.local_ok) {
|
|
11355
|
+
process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
|
|
11356
|
+
`);
|
|
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
|
+
}
|
|
11373
|
+
}
|
|
11374
|
+
} else if (requireAnchor) {
|
|
11375
|
+
anchorOk = false;
|
|
11376
|
+
process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
|
|
11377
|
+
`);
|
|
11378
|
+
} else {
|
|
11379
|
+
process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
|
|
11380
|
+
`);
|
|
11381
|
+
}
|
|
11382
|
+
const finalValid = v.valid && anchorOk;
|
|
10949
11383
|
process.stdout.write(`
|
|
10950
|
-
${
|
|
11384
|
+
${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
|
|
10951
11385
|
`);
|
|
10952
11386
|
process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
|
|
10953
11387
|
`);
|
|
10954
11388
|
process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
|
|
10955
11389
|
`);
|
|
10956
|
-
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer;
|
|
11390
|
+
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
|
|
10957
11391
|
|
|
10958
11392
|
`);
|
|
10959
|
-
process.exit(
|
|
11393
|
+
process.exit(finalValid ? 0 : 1);
|
|
10960
11394
|
}
|
|
10961
11395
|
async function handleBundle(argv) {
|
|
10962
11396
|
const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
|
|
@@ -12463,6 +12897,10 @@ async function main() {
|
|
|
12463
12897
|
await handleVerifyClaim(args.slice(1));
|
|
12464
12898
|
return;
|
|
12465
12899
|
}
|
|
12900
|
+
if (args[0] === "anchor-record") {
|
|
12901
|
+
await handleAnchorRecord(args.slice(1));
|
|
12902
|
+
return;
|
|
12903
|
+
}
|
|
12466
12904
|
if (args[0] === "init-hooks") {
|
|
12467
12905
|
await handleInitHooks(args.slice(1));
|
|
12468
12906
|
process.exit(0);
|