protect-mcp 0.9.2 → 0.9.3
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 +22 -0
- package/README.md +40 -0
- package/dist/{chunk-CXW2EIRM.mjs → chunk-7MHK5RF4.mjs} +2 -2
- package/dist/{chunk-744JMCY4.mjs → chunk-DX7QOA3E.mjs} +2 -2
- package/dist/{chunk-KMNXHGGT.mjs → chunk-HPKHMVZX.mjs} +1 -1
- package/dist/{chunk-GHR65WVD.mjs → chunk-PB3TC7E3.mjs} +1 -1
- package/dist/{chunk-IDUH2O4Q.mjs → chunk-XLJUZ4WO.mjs} +7 -1
- package/dist/{claim-TUDH2WPB.mjs → claim-3EBRPP5U.mjs} +76 -1
- package/dist/cli.js +177 -11
- package/dist/cli.mjs +101 -16
- package/dist/hook-server.js +8 -2
- 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 +8 -2
- package/dist/index.mjs +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.3: the skeptic's tools sharpen
|
|
4
|
+
|
|
5
|
+
Two verification upgrades: the anchor check moves into the verifier, and the
|
|
6
|
+
record viewer proves signatures instead of just labelling them.
|
|
7
|
+
|
|
8
|
+
- `verify-claim` now verifies the anchor automatically: it finds the
|
|
9
|
+
`<claim>.anchor.json` sidecar, checks offline that the anchored envelope binds
|
|
10
|
+
this exact claim (digest, record root, and the same issuer key), and confirms
|
|
11
|
+
against the public log that the digest sits at the recorded entry. A failed
|
|
12
|
+
binding makes the attestation INVALID; an unreachable log does not (the local
|
|
13
|
+
checks stand alone). `--check-anchor` makes a missing anchor fatal,
|
|
14
|
+
`--anchor-file <p>` overrides the path, `--offline` skips the network hop.
|
|
15
|
+
- The `record` viewer now verifies Ed25519 signatures in the browser, locally:
|
|
16
|
+
each row shows `✓ verified`, `✗ invalid signature`, or `signed · unpinned
|
|
17
|
+
key`, and the stat strip counts how many verified. The CLI injects your
|
|
18
|
+
gateway's PUBLIC key so rows verify against your own key (the private key
|
|
19
|
+
never reaches the page).
|
|
20
|
+
- Receipts now embed the signer's public key inside the signed payload
|
|
21
|
+
(`payload.public_key`), so a receipt is self-contained: any verifier can check
|
|
22
|
+
it without a side channel, and the key cannot be swapped without breaking the
|
|
23
|
+
signature. Older receipts still verify against a pinned or pasted key.
|
|
24
|
+
|
|
3
25
|
## 0.9.2: anchor a claim to the public log
|
|
4
26
|
|
|
5
27
|
Closes the one honest gap in a bare claim: that the disclosed set is complete.
|
package/README.md
CHANGED
|
@@ -148,6 +148,43 @@ disclosure, not full zero-knowledge, but it makes the privacy claim concrete:
|
|
|
148
148
|
auditors can verify selected facts without receiving the full tool payload or
|
|
149
149
|
sensitive desk context.
|
|
150
150
|
|
|
151
|
+
### Prove a claim over the record (position-blind attestations)
|
|
152
|
+
|
|
153
|
+
You can prove a CLAIM over your record without revealing it. Mint a signed,
|
|
154
|
+
position-blind attestation over the whole record that discloses only per-decision
|
|
155
|
+
categories (a receipt digest, the verdict, capability tags), never your tool
|
|
156
|
+
inputs, outputs, or data:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# "No action reached the network across the record":
|
|
160
|
+
npx protect-mcp claim --no net.egress
|
|
161
|
+
|
|
162
|
+
# other predicates:
|
|
163
|
+
# --only fs.read,fs.write all actions were confined to these capabilities
|
|
164
|
+
# --no-verdict blocked no action was blocked
|
|
165
|
+
# --count blocked how many were blocked
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Anyone verifies it offline, seeing only the categories, never the content:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx protect-mcp verify-claim claim-<id>.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The verifier recomputes a Merkle root over the disclosed set and recomputes the
|
|
175
|
+
predicate independently, so the issuer cannot lie about the claim given the
|
|
176
|
+
disclosure. Add `--anchor` to record the claim's digest in the public,
|
|
177
|
+
append-only ScopeBlind transparency log, so a counterparty who does not trust you
|
|
178
|
+
can confirm the disclosed set is complete and was not quietly re-cut (only the
|
|
179
|
+
hash is sent; the record stays local):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npx protect-mcp claim --no net.egress --anchor
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This is an accountable, position-blind attestation, not full zero-knowledge: it
|
|
186
|
+
reveals the shape, not the content.
|
|
187
|
+
|
|
151
188
|
## Claude Code hook quickstart
|
|
152
189
|
|
|
153
190
|
```bash
|
|
@@ -335,6 +372,9 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
|
335
372
|
| `dashboard` | Start a local-only dashboard on `127.0.0.1` showing tool inventory, risk, policy coverage, exact-action approvals, receipt chains, and audit export. |
|
|
336
373
|
| `recommend` | Draft a reviewable JSON policy from observed local calls. Dry-run by default; use `--write` to create `protect-mcp.recommended.json`. |
|
|
337
374
|
| `registry` | Create an org identity, anchor receipt digests, and write a static verifier page. Hosted mode uploads digests only. |
|
|
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>`, `--only <c1,c2>`, `--no-verdict <verdict>`, `--count <verdict>`), disclosing only decision categories. Add `--anchor` to record the claim digest in the public transparency log. |
|
|
377
|
+
| `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. |
|
|
338
378
|
| `killer-demo` | Generate a complete shadow-mode to policy to approval to signed-receipt demo pack. |
|
|
339
379
|
| `verify-disclosure` | Verify a `scopeblind.selective_disclosure.v0` package and explain disclosed versus hidden fields. |
|
|
340
380
|
| `policy-packs` | List, inspect, and install starter Cedar policy packs. |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
meetsMinTier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PB3TC7E3.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XLJUZ4WO.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildEnrichment
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HPKHMVZX.mjs";
|
|
4
4
|
import {
|
|
5
5
|
ReceiptBuffer,
|
|
6
6
|
buildActionReadback,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
loadPolicy,
|
|
16
16
|
parseRateLimit,
|
|
17
17
|
signDecision
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XLJUZ4WO.mjs";
|
|
19
19
|
|
|
20
20
|
// src/hook-server.ts
|
|
21
21
|
import { createServer } from "http";
|
|
@@ -177,7 +177,13 @@ function signDecision(entry) {
|
|
|
177
177
|
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
178
178
|
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
179
179
|
// - uncertified = unsigned receipt (shadow mode, no signing configured)
|
|
180
|
-
issuer_certification: signerState ? "self-signed" : "uncertified"
|
|
180
|
+
issuer_certification: signerState ? "self-signed" : "uncertified",
|
|
181
|
+
// The signer's PUBLIC key, inside the signed payload, so a receipt is
|
|
182
|
+
// self-contained: any verifier (including the record viewer, in-browser)
|
|
183
|
+
// can check the signature without a side channel. Binding the key inside
|
|
184
|
+
// the signature means it cannot be swapped without breaking the signature;
|
|
185
|
+
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
186
|
+
public_key: signerState.publicKey
|
|
181
187
|
};
|
|
182
188
|
if (entry.tier) payload.tier = entry.tier;
|
|
183
189
|
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canonicalJson
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HPKHMVZX.mjs";
|
|
4
4
|
import {
|
|
5
5
|
sha256
|
|
6
6
|
} from "./chunk-AYNQIEN7.mjs";
|
|
@@ -156,6 +156,79 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
|
|
|
156
156
|
const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
|
|
157
157
|
return { ...signed, signature, digest };
|
|
158
158
|
}
|
|
159
|
+
function verifyAnchorEnvelope(pack, envelope) {
|
|
160
|
+
const reasons = [];
|
|
161
|
+
if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
|
|
162
|
+
return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
|
|
163
|
+
}
|
|
164
|
+
const expected = claimDigest(pack);
|
|
165
|
+
if (envelope.claim_digest !== expected) {
|
|
166
|
+
reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
|
|
167
|
+
}
|
|
168
|
+
if (envelope.record_root !== pack.record.root) {
|
|
169
|
+
reasons.push("anchored envelope commits to a different record root");
|
|
170
|
+
}
|
|
171
|
+
if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
|
|
172
|
+
reasons.push("anchor was signed by a different key than the claim issuer");
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const { signature, digest, ...signed } = envelope;
|
|
176
|
+
const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
|
|
177
|
+
if (bytesToHex(hash) !== String(digest).toLowerCase()) {
|
|
178
|
+
reasons.push("envelope digest does not match its contents");
|
|
179
|
+
} else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
|
|
180
|
+
reasons.push("envelope signature does not verify");
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
reasons.push("envelope signature does not verify");
|
|
184
|
+
}
|
|
185
|
+
return { ok: reasons.length === 0, reasons };
|
|
186
|
+
}
|
|
187
|
+
async function checkClaimAnchor(pack, sidecar, opts) {
|
|
188
|
+
const reasons = [];
|
|
189
|
+
const envelope = sidecar && sidecar.envelope;
|
|
190
|
+
if (!envelope) {
|
|
191
|
+
return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
|
|
192
|
+
}
|
|
193
|
+
const local = verifyAnchorEnvelope(pack, envelope);
|
|
194
|
+
reasons.push(...local.reasons);
|
|
195
|
+
const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
196
|
+
const out = {
|
|
197
|
+
local_ok: local.ok,
|
|
198
|
+
log_ok: null,
|
|
199
|
+
seq: sidecar.seq,
|
|
200
|
+
anchored_at: sidecar.anchored_at,
|
|
201
|
+
entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
|
|
202
|
+
reasons
|
|
203
|
+
};
|
|
204
|
+
if (opts?.offline) return out;
|
|
205
|
+
const doFetch = opts?.fetchImpl || globalThis.fetch;
|
|
206
|
+
if (!doFetch) return out;
|
|
207
|
+
try {
|
|
208
|
+
const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
|
|
209
|
+
const data = await resp.json().catch(() => null);
|
|
210
|
+
if (!resp.ok || !data) {
|
|
211
|
+
out.log_ok = null;
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
if (data.anchored !== true) {
|
|
215
|
+
out.log_ok = false;
|
|
216
|
+
out.reasons.push("the public log does not contain this anchor digest");
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
|
|
220
|
+
out.log_ok = false;
|
|
221
|
+
out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
out.log_ok = true;
|
|
225
|
+
if (typeof data.seq === "number") out.seq = data.seq;
|
|
226
|
+
return out;
|
|
227
|
+
} catch {
|
|
228
|
+
out.log_ok = null;
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
159
232
|
async function anchorClaim(pack, key, opts) {
|
|
160
233
|
const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
|
|
161
234
|
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
@@ -192,10 +265,12 @@ export {
|
|
|
192
265
|
anchorClaim,
|
|
193
266
|
buildAnchorEnvelope,
|
|
194
267
|
buildClaim,
|
|
268
|
+
checkClaimAnchor,
|
|
195
269
|
claimDigest,
|
|
196
270
|
evaluate,
|
|
197
271
|
leafHash,
|
|
198
272
|
merkleRoot,
|
|
199
273
|
receiptToLeaf,
|
|
274
|
+
verifyAnchorEnvelope,
|
|
200
275
|
verifyClaim
|
|
201
276
|
};
|
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;
|
|
@@ -5156,7 +5162,7 @@ var init_receipt_enrichment = __esm({
|
|
|
5156
5162
|
"use strict";
|
|
5157
5163
|
init_sha256();
|
|
5158
5164
|
init_utils();
|
|
5159
|
-
ENRICHMENT_VERSION =
|
|
5165
|
+
ENRICHMENT_VERSION = 2;
|
|
5160
5166
|
RULES = [
|
|
5161
5167
|
{ cap: "exec.shell", tool: /bash|shell|exec|terminal|run_command|command/ },
|
|
5162
5168
|
{ cap: "fs.read", tool: /(^|[_.])(read|cat|glob|grep|search|ls|view|list_files|open)/ },
|
|
@@ -5182,11 +5188,13 @@ __export(claim_exports, {
|
|
|
5182
5188
|
anchorClaim: () => anchorClaim,
|
|
5183
5189
|
buildAnchorEnvelope: () => buildAnchorEnvelope,
|
|
5184
5190
|
buildClaim: () => buildClaim,
|
|
5191
|
+
checkClaimAnchor: () => checkClaimAnchor,
|
|
5185
5192
|
claimDigest: () => claimDigest,
|
|
5186
5193
|
evaluate: () => evaluate,
|
|
5187
5194
|
leafHash: () => leafHash,
|
|
5188
5195
|
merkleRoot: () => merkleRoot,
|
|
5189
5196
|
receiptToLeaf: () => receiptToLeaf,
|
|
5197
|
+
verifyAnchorEnvelope: () => verifyAnchorEnvelope,
|
|
5190
5198
|
verifyClaim: () => verifyClaim
|
|
5191
5199
|
});
|
|
5192
5200
|
function sha256Hex3(input) {
|
|
@@ -5327,6 +5335,79 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
|
|
|
5327
5335
|
const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
|
|
5328
5336
|
return { ...signed, signature, digest };
|
|
5329
5337
|
}
|
|
5338
|
+
function verifyAnchorEnvelope(pack, envelope) {
|
|
5339
|
+
const reasons = [];
|
|
5340
|
+
if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
|
|
5341
|
+
return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
|
|
5342
|
+
}
|
|
5343
|
+
const expected = claimDigest(pack);
|
|
5344
|
+
if (envelope.claim_digest !== expected) {
|
|
5345
|
+
reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
|
|
5346
|
+
}
|
|
5347
|
+
if (envelope.record_root !== pack.record.root) {
|
|
5348
|
+
reasons.push("anchored envelope commits to a different record root");
|
|
5349
|
+
}
|
|
5350
|
+
if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
|
|
5351
|
+
reasons.push("anchor was signed by a different key than the claim issuer");
|
|
5352
|
+
}
|
|
5353
|
+
try {
|
|
5354
|
+
const { signature, digest, ...signed } = envelope;
|
|
5355
|
+
const hash = sha2562(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
|
|
5356
|
+
if (bytesToHex(hash) !== String(digest).toLowerCase()) {
|
|
5357
|
+
reasons.push("envelope digest does not match its contents");
|
|
5358
|
+
} else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
|
|
5359
|
+
reasons.push("envelope signature does not verify");
|
|
5360
|
+
}
|
|
5361
|
+
} catch {
|
|
5362
|
+
reasons.push("envelope signature does not verify");
|
|
5363
|
+
}
|
|
5364
|
+
return { ok: reasons.length === 0, reasons };
|
|
5365
|
+
}
|
|
5366
|
+
async function checkClaimAnchor(pack, sidecar, opts) {
|
|
5367
|
+
const reasons = [];
|
|
5368
|
+
const envelope = sidecar && sidecar.envelope;
|
|
5369
|
+
if (!envelope) {
|
|
5370
|
+
return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
|
|
5371
|
+
}
|
|
5372
|
+
const local = verifyAnchorEnvelope(pack, envelope);
|
|
5373
|
+
reasons.push(...local.reasons);
|
|
5374
|
+
const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
5375
|
+
const out = {
|
|
5376
|
+
local_ok: local.ok,
|
|
5377
|
+
log_ok: null,
|
|
5378
|
+
seq: sidecar.seq,
|
|
5379
|
+
anchored_at: sidecar.anchored_at,
|
|
5380
|
+
entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
|
|
5381
|
+
reasons
|
|
5382
|
+
};
|
|
5383
|
+
if (opts?.offline) return out;
|
|
5384
|
+
const doFetch = opts?.fetchImpl || globalThis.fetch;
|
|
5385
|
+
if (!doFetch) return out;
|
|
5386
|
+
try {
|
|
5387
|
+
const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
|
|
5388
|
+
const data = await resp.json().catch(() => null);
|
|
5389
|
+
if (!resp.ok || !data) {
|
|
5390
|
+
out.log_ok = null;
|
|
5391
|
+
return out;
|
|
5392
|
+
}
|
|
5393
|
+
if (data.anchored !== true) {
|
|
5394
|
+
out.log_ok = false;
|
|
5395
|
+
out.reasons.push("the public log does not contain this anchor digest");
|
|
5396
|
+
return out;
|
|
5397
|
+
}
|
|
5398
|
+
if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
|
|
5399
|
+
out.log_ok = false;
|
|
5400
|
+
out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
|
|
5401
|
+
return out;
|
|
5402
|
+
}
|
|
5403
|
+
out.log_ok = true;
|
|
5404
|
+
if (typeof data.seq === "number") out.seq = data.seq;
|
|
5405
|
+
return out;
|
|
5406
|
+
} catch {
|
|
5407
|
+
out.log_ok = null;
|
|
5408
|
+
return out;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5330
5411
|
async function anchorClaim(pack, key, opts) {
|
|
5331
5412
|
const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
|
|
5332
5413
|
const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
|
|
@@ -10563,6 +10644,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
10563
10644
|
return null;
|
|
10564
10645
|
}
|
|
10565
10646
|
}).filter((x) => x !== null).map(mapRecordEntry);
|
|
10647
|
+
let pinnedKey = "";
|
|
10648
|
+
let pinnedKid = "";
|
|
10649
|
+
try {
|
|
10650
|
+
const kd = JSON.parse(readFileSync11(join8(dir, "keys", "gateway.json"), "utf-8"));
|
|
10651
|
+
if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
|
|
10652
|
+
pinnedKey = kd.publicKey;
|
|
10653
|
+
pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
|
|
10654
|
+
}
|
|
10655
|
+
} catch {
|
|
10656
|
+
}
|
|
10566
10657
|
const openTarget = (target) => {
|
|
10567
10658
|
if (argv.includes("--no-open")) return;
|
|
10568
10659
|
const platform = process.platform;
|
|
@@ -10590,7 +10681,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
10590
10681
|
res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
|
|
10591
10682
|
return;
|
|
10592
10683
|
}
|
|
10593
|
-
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
|
|
10684
|
+
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
10594
10685
|
const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
|
|
10595
10686
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
10596
10687
|
res.end(page);
|
|
@@ -10615,7 +10706,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
|
|
|
10615
10706
|
return;
|
|
10616
10707
|
}
|
|
10617
10708
|
const recs = readRecs(chosen);
|
|
10618
|
-
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
|
|
10709
|
+
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
10619
10710
|
const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
|
|
10620
10711
|
const out = join8(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
|
|
10621
10712
|
writeFileSync4(out, html);
|
|
@@ -10681,6 +10772,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
10681
10772
|
.badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
|
|
10682
10773
|
.badge.sgn{background:var(--gb);color:var(--g)}
|
|
10683
10774
|
.badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
|
|
10775
|
+
.badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
|
|
10776
|
+
.badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
|
|
10777
|
+
.stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
|
|
10684
10778
|
.dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
10685
10779
|
.when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
10686
10780
|
.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 +10821,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
10727
10821
|
</div>
|
|
10728
10822
|
<script>
|
|
10729
10823
|
var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
|
|
10824
|
+
// In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
|
|
10825
|
+
// preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
|
|
10826
|
+
// signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
|
|
10827
|
+
// public half, injected by the CLI) = authenticity; key embedded in the receipt
|
|
10828
|
+
// payload (0.9.3+) = self-consistency. Everything runs locally.
|
|
10829
|
+
var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
|
|
10830
|
+
function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
|
|
10831
|
+
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}
|
|
10832
|
+
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})}
|
|
10833
|
+
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)}
|
|
10834
|
+
async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
|
|
10835
|
+
var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
|
|
10836
|
+
var msg=new TextEncoder().encode(canon(rest));
|
|
10837
|
+
var pin=String(META.pinned_key||"").toLowerCase();
|
|
10838
|
+
var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
|
|
10839
|
+
if(!/^[0-9a-f]{64}$/.test(emb))emb="";
|
|
10840
|
+
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"}
|
|
10841
|
+
if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
|
|
10842
|
+
return"nokey"}
|
|
10843
|
+
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}
|
|
10844
|
+
async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
|
|
10845
|
+
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;
|
|
10846
|
+
try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
|
|
10847
|
+
finally{VDONE=true;VBUSY=false;render()}}
|
|
10730
10848
|
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
10731
10849
|
function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
|
|
10732
10850
|
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 +10858,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
|
|
|
10740
10858
|
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
10859
|
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
10860
|
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
|
-
|
|
10861
|
+
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>'}
|
|
10862
|
+
p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
|
|
10863
|
+
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
10864
|
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
10865
|
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
10866
|
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 +10878,8 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
|
|
|
10759
10878
|
document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
|
|
10760
10879
|
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
10880
|
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);}
|
|
10881
|
+
render();kickVerify();
|
|
10882
|
+
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
10883
|
</script></body></html>`;
|
|
10765
10884
|
async function handleClaim(argv) {
|
|
10766
10885
|
const { readFileSync: readFileSync11, existsSync: existsSync9, writeFileSync: writeFileSync4 } = await import("fs");
|
|
@@ -10946,17 +11065,64 @@ ${bold("protect-mcp verify-claim")}
|
|
|
10946
11065
|
`);
|
|
10947
11066
|
process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
|
|
10948
11067
|
`);
|
|
11068
|
+
const ai = argv.indexOf("--anchor-file");
|
|
11069
|
+
const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
|
|
11070
|
+
const requireAnchor = argv.includes("--check-anchor");
|
|
11071
|
+
let anchorOk = true;
|
|
11072
|
+
if (existsSync9(sidecarPath)) {
|
|
11073
|
+
const { checkClaimAnchor: checkClaimAnchor2 } = await Promise.resolve().then(() => (init_claim(), claim_exports));
|
|
11074
|
+
let sidecar = null;
|
|
11075
|
+
try {
|
|
11076
|
+
sidecar = JSON.parse(readFileSync11(sidecarPath, "utf-8"));
|
|
11077
|
+
} catch {
|
|
11078
|
+
}
|
|
11079
|
+
if (!sidecar) {
|
|
11080
|
+
anchorOk = false;
|
|
11081
|
+
process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
|
|
11082
|
+
`);
|
|
11083
|
+
} else {
|
|
11084
|
+
const a = await checkClaimAnchor2(pack, sidecar, { offline: argv.includes("--offline") });
|
|
11085
|
+
anchorOk = a.local_ok && a.log_ok !== false;
|
|
11086
|
+
if (a.local_ok) {
|
|
11087
|
+
process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
|
|
11088
|
+
`);
|
|
11089
|
+
process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
|
|
11090
|
+
`);
|
|
11091
|
+
} else {
|
|
11092
|
+
for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
|
|
11093
|
+
`);
|
|
11094
|
+
}
|
|
11095
|
+
if (a.log_ok === true) {
|
|
11096
|
+
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) : ""}
|
|
11097
|
+
`);
|
|
11098
|
+
} else if (a.log_ok === false) {
|
|
11099
|
+
process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
|
|
11100
|
+
`);
|
|
11101
|
+
} else if (a.local_ok) {
|
|
11102
|
+
process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
|
|
11103
|
+
`);
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
11106
|
+
} else if (requireAnchor) {
|
|
11107
|
+
anchorOk = false;
|
|
11108
|
+
process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
|
|
11109
|
+
`);
|
|
11110
|
+
} else {
|
|
11111
|
+
process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
|
|
11112
|
+
`);
|
|
11113
|
+
}
|
|
11114
|
+
const finalValid = v.valid && anchorOk;
|
|
10949
11115
|
process.stdout.write(`
|
|
10950
|
-
${
|
|
11116
|
+
${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
|
|
10951
11117
|
`);
|
|
10952
11118
|
process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
|
|
10953
11119
|
`);
|
|
10954
11120
|
process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
|
|
10955
11121
|
`);
|
|
10956
|
-
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer;
|
|
11122
|
+
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
|
|
10957
11123
|
|
|
10958
11124
|
`);
|
|
10959
|
-
process.exit(
|
|
11125
|
+
process.exit(finalValid ? 0 : 1);
|
|
10960
11126
|
}
|
|
10961
11127
|
async function handleBundle(argv) {
|
|
10962
11128
|
const { readFileSync: readFileSync11, writeFileSync: writeFileSync4, existsSync: existsSync9 } = await import("fs");
|
package/dist/cli.mjs
CHANGED
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
readInstalledConnectorPilots,
|
|
12
12
|
simulate,
|
|
13
13
|
writeConnectorPilots
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-7MHK5RF4.mjs";
|
|
15
15
|
import {
|
|
16
16
|
ProtectGateway,
|
|
17
17
|
validateCredentials
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-PB3TC7E3.mjs";
|
|
19
19
|
import {
|
|
20
20
|
buildActionReadback,
|
|
21
21
|
evaluateCedar,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
policySetFromSource,
|
|
27
27
|
runEvaluatorSelfTest,
|
|
28
28
|
signDecision
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-XLJUZ4WO.mjs";
|
|
30
30
|
import "./chunk-PQJP2ZCI.mjs";
|
|
31
31
|
|
|
32
32
|
// src/cli.ts
|
|
@@ -2033,6 +2033,16 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
2033
2033
|
return null;
|
|
2034
2034
|
}
|
|
2035
2035
|
}).filter((x) => x !== null).map(mapRecordEntry);
|
|
2036
|
+
let pinnedKey = "";
|
|
2037
|
+
let pinnedKid = "";
|
|
2038
|
+
try {
|
|
2039
|
+
const kd = JSON.parse(readFileSync(join(dir, "keys", "gateway.json"), "utf-8"));
|
|
2040
|
+
if (kd && typeof kd.publicKey === "string" && /^[0-9a-f]{64}$/i.test(kd.publicKey)) {
|
|
2041
|
+
pinnedKey = kd.publicKey;
|
|
2042
|
+
pinnedKid = typeof kd.kid === "string" ? kd.kid : "";
|
|
2043
|
+
}
|
|
2044
|
+
} catch {
|
|
2045
|
+
}
|
|
2036
2046
|
const openTarget = (target) => {
|
|
2037
2047
|
if (argv.includes("--no-open")) return;
|
|
2038
2048
|
const platform = process.platform;
|
|
@@ -2060,7 +2070,7 @@ Start the gate with ${bold("npx protect-mcp serve")}, use your agent, then run t
|
|
|
2060
2070
|
res.end(JSON.stringify({ recs: recs2, signed: f === recPath }));
|
|
2061
2071
|
return;
|
|
2062
2072
|
}
|
|
2063
|
-
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true };
|
|
2073
|
+
const meta2 = { file: chosen, signed: pick() === recPath, count: 0, live: true, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
2064
2074
|
const page = RECORD_HTML.replace("__DATA__", () => "[]").replace("__META__", () => JSON.stringify(meta2));
|
|
2065
2075
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2066
2076
|
res.end(page);
|
|
@@ -2085,7 +2095,7 @@ ${bold("\u{1F6E1}\uFE0F Your record")} ${dim("\xB7")} live at ${url}
|
|
|
2085
2095
|
return;
|
|
2086
2096
|
}
|
|
2087
2097
|
const recs = readRecs(chosen);
|
|
2088
|
-
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false };
|
|
2098
|
+
const meta = { file: chosen, signed: chosen === recPath, count: recs.length, live: false, pinned_key: pinnedKey, pinned_kid: pinnedKid };
|
|
2089
2099
|
const html = RECORD_HTML.replace("__DATA__", () => JSON.stringify(recs)).replace("__META__", () => JSON.stringify(meta));
|
|
2090
2100
|
const out = join(osMod.tmpdir(), "protect-mcp-record-" + Date.now() + ".html");
|
|
2091
2101
|
writeFileSync(out, html);
|
|
@@ -2151,6 +2161,9 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
2151
2161
|
.badge{font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:100px}
|
|
2152
2162
|
.badge.sgn{background:var(--gb);color:var(--g)}
|
|
2153
2163
|
.badge.log{background:var(--paper);color:var(--faint);border:1px solid var(--line)}
|
|
2164
|
+
.badge.vbad{background:#fbecec;color:#b3382f;border:1px solid #edc6c2}
|
|
2165
|
+
.badge.vfor{background:#fbf3df;color:#8a6d1a;border:1px solid #e8d8ae}
|
|
2166
|
+
.stat .badk{color:#b3382f;font-weight:680}.stat .warnk{color:#8a6d1a}.stat .dim2{color:var(--faint);font-weight:400}
|
|
2154
2167
|
.dg{font-size:10.5px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
2155
2168
|
.when{margin-left:auto;font-size:12px;color:var(--faint);font-family:ui-monospace,Menlo,monospace}
|
|
2156
2169
|
.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}
|
|
@@ -2197,6 +2210,30 @@ input{width:100%;padding:10px 13px;border:1px solid var(--line);border-radius:9p
|
|
|
2197
2210
|
</div>
|
|
2198
2211
|
<script>
|
|
2199
2212
|
var RECORDS=__DATA__;var META=__META__;var Q="",ACT={},VIEW="list",OPEN={};var NL=String.fromCharCode(10);
|
|
2213
|
+
// In-browser signature verification. Mirrors @veritasacta/artifacts exactly:
|
|
2214
|
+
// preimage = JCS-style canonical JSON (sorted keys) of the receipt minus its
|
|
2215
|
+
// signature, verified with WebCrypto Ed25519. Pinned key (your keys/gateway.json
|
|
2216
|
+
// public half, injected by the CLI) = authenticity; key embedded in the receipt
|
|
2217
|
+
// payload (0.9.3+) = self-consistency. Everything runs locally.
|
|
2218
|
+
var VSTATE={},VDONE=false,VBUSY=false,VUNSUP=false;
|
|
2219
|
+
function vkey(r){return "row:"+(r.id||"")+"|"+(r.ts||"")}
|
|
2220
|
+
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}
|
|
2221
|
+
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})}
|
|
2222
|
+
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)}
|
|
2223
|
+
async function verifyRow(r){var raw=r.raw;if(!raw||typeof raw.signature!=="string")return"unsigned";
|
|
2224
|
+
var rest={},k;for(k in raw)if(k!=="signature")rest[k]=raw[k];
|
|
2225
|
+
var msg=new TextEncoder().encode(canon(rest));
|
|
2226
|
+
var pin=String(META.pinned_key||"").toLowerCase();
|
|
2227
|
+
var emb=String((raw.payload&&raw.payload.public_key)||raw.public_key||"").toLowerCase();
|
|
2228
|
+
if(!/^[0-9a-f]{64}$/.test(emb))emb="";
|
|
2229
|
+
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"}
|
|
2230
|
+
if(emb)return(await edv(raw.signature,msg,emb))?"ok":"bad";
|
|
2231
|
+
return"nokey"}
|
|
2232
|
+
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}
|
|
2233
|
+
async function kickVerify(){if(VBUSY||VUNSUP||!(window.crypto&&crypto.subtle))return;VBUSY=true;
|
|
2234
|
+
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;
|
|
2235
|
+
try{VSTATE[kk]=await verifyRow(r)}catch(e){if(e&&e.name==="NotSupportedError"){VUNSUP=true;break}VSTATE[kk]="bad"}}}
|
|
2236
|
+
finally{VDONE=true;VBUSY=false;render()}}
|
|
2200
2237
|
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
2201
2238
|
function vlabel(v){return v==="allowed"?"Allowed":v==="held"?"Held":"Blocked"}
|
|
2202
2239
|
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"})}
|
|
@@ -2210,8 +2247,9 @@ function exportJsonl(){var rows=filtered();if(!rows.length)return;var lines=rows
|
|
|
2210
2247
|
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")}
|
|
2211
2248
|
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)}}
|
|
2212
2249
|
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)}}
|
|
2213
|
-
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>');
|
|
2214
|
-
|
|
2250
|
+
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>'}
|
|
2251
|
+
p.push('<span class="stat sig">'+st+'</span>');document.getElementById("stats").innerHTML=p.join("")}
|
|
2252
|
+
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>';}
|
|
2215
2253
|
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";}
|
|
2216
2254
|
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];});}
|
|
2217
2255
|
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;}
|
|
@@ -2229,13 +2267,13 @@ if(VIEW==="tree"){renderTree(buildTree(rows));}else{renderList(rows);}}
|
|
|
2229
2267
|
document.getElementById("q").addEventListener("input",function(e){Q=e.target.value.toLowerCase().trim();render()});
|
|
2230
2268
|
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()});
|
|
2231
2269
|
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");}});
|
|
2232
|
-
render();
|
|
2233
|
-
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);}
|
|
2270
|
+
render();kickVerify();
|
|
2271
|
+
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);}
|
|
2234
2272
|
</script></body></html>`;
|
|
2235
2273
|
async function handleClaim(argv) {
|
|
2236
2274
|
const { readFileSync, existsSync, writeFileSync } = await import("fs");
|
|
2237
2275
|
const { join } = await import("path");
|
|
2238
|
-
const { buildClaim } = await import("./claim-
|
|
2276
|
+
const { buildClaim } = await import("./claim-3EBRPP5U.mjs");
|
|
2239
2277
|
let dir = process.cwd();
|
|
2240
2278
|
const di = argv.indexOf("--dir");
|
|
2241
2279
|
if (di !== -1 && argv[di + 1]) dir = argv[di + 1];
|
|
@@ -2336,7 +2374,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
2336
2374
|
process.stdout.write(` Hand it to anyone. They verify offline: ${bold("npx protect-mcp verify-claim " + out)}
|
|
2337
2375
|
`);
|
|
2338
2376
|
if (argv.indexOf("--anchor") !== -1) {
|
|
2339
|
-
const { anchorClaim } = await import("./claim-
|
|
2377
|
+
const { anchorClaim } = await import("./claim-3EBRPP5U.mjs");
|
|
2340
2378
|
const li = argv.indexOf("--log");
|
|
2341
2379
|
const logBase = li !== -1 && argv[li + 1] ? argv[li + 1] : void 0;
|
|
2342
2380
|
process.stdout.write(`
|
|
@@ -2369,7 +2407,7 @@ ${bold("\u{1F6E1}\uFE0F Signed claim")}
|
|
|
2369
2407
|
}
|
|
2370
2408
|
async function handleVerifyClaim(argv) {
|
|
2371
2409
|
const { readFileSync, existsSync } = await import("fs");
|
|
2372
|
-
const { verifyClaim } = await import("./claim-
|
|
2410
|
+
const { verifyClaim } = await import("./claim-3EBRPP5U.mjs");
|
|
2373
2411
|
const file = argv.find((a) => !a.startsWith("--"));
|
|
2374
2412
|
if (!file || !existsSync(file)) {
|
|
2375
2413
|
process.stderr.write(`
|
|
@@ -2416,17 +2454,64 @@ ${bold("protect-mcp verify-claim")}
|
|
|
2416
2454
|
`);
|
|
2417
2455
|
process.stdout.write(` Predicate: ${ok(v.predicate_ok)} ${v.predicate_ok ? "recomputed independently and matches" : "MISMATCH"}
|
|
2418
2456
|
`);
|
|
2457
|
+
const ai = argv.indexOf("--anchor-file");
|
|
2458
|
+
const sidecarPath = ai !== -1 && argv[ai + 1] ? argv[ai + 1] : file.replace(/\.json$/, "") + ".anchor.json";
|
|
2459
|
+
const requireAnchor = argv.includes("--check-anchor");
|
|
2460
|
+
let anchorOk = true;
|
|
2461
|
+
if (existsSync(sidecarPath)) {
|
|
2462
|
+
const { checkClaimAnchor } = await import("./claim-3EBRPP5U.mjs");
|
|
2463
|
+
let sidecar = null;
|
|
2464
|
+
try {
|
|
2465
|
+
sidecar = JSON.parse(readFileSync(sidecarPath, "utf-8"));
|
|
2466
|
+
} catch {
|
|
2467
|
+
}
|
|
2468
|
+
if (!sidecar) {
|
|
2469
|
+
anchorOk = false;
|
|
2470
|
+
process.stdout.write(` Anchor: ${red("\u2717")} ${sidecarPath} is not valid JSON
|
|
2471
|
+
`);
|
|
2472
|
+
} else {
|
|
2473
|
+
const a = await checkClaimAnchor(pack, sidecar, { offline: argv.includes("--offline") });
|
|
2474
|
+
anchorOk = a.local_ok && a.log_ok !== false;
|
|
2475
|
+
if (a.local_ok) {
|
|
2476
|
+
process.stdout.write(` Anchor: ${green("\u2713")} anchored envelope binds this exact claim and its record root
|
|
2477
|
+
`);
|
|
2478
|
+
process.stdout.write(` ${green("\u2713")} envelope signed by the claim issuer's key
|
|
2479
|
+
`);
|
|
2480
|
+
} else {
|
|
2481
|
+
for (const r of a.reasons.slice(0, 3)) process.stdout.write(` Anchor: ${red("\u2717")} ${r}
|
|
2482
|
+
`);
|
|
2483
|
+
}
|
|
2484
|
+
if (a.log_ok === true) {
|
|
2485
|
+
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) : ""}
|
|
2486
|
+
`);
|
|
2487
|
+
} else if (a.log_ok === false) {
|
|
2488
|
+
process.stdout.write(` ${red("\u2717")} ${a.reasons[a.reasons.length - 1]}
|
|
2489
|
+
`);
|
|
2490
|
+
} else if (a.local_ok) {
|
|
2491
|
+
process.stdout.write(` ${yellow("~")} log not checked ${dim(argv.includes("--offline") ? "(--offline)" : "(unreachable; local binding checks stand)")}
|
|
2492
|
+
`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
} else if (requireAnchor) {
|
|
2496
|
+
anchorOk = false;
|
|
2497
|
+
process.stdout.write(` Anchor: ${red("\u2717")} no anchor sidecar at ${sidecarPath} ${dim("(mint with: protect-mcp claim ... --anchor)")}
|
|
2498
|
+
`);
|
|
2499
|
+
} else {
|
|
2500
|
+
process.stdout.write(` Anchor: ${dim("none found (" + sidecarPath + "). Anchoring proves the claim was fixed at a time: claim ... --anchor")}
|
|
2501
|
+
`);
|
|
2502
|
+
}
|
|
2503
|
+
const finalValid = v.valid && anchorOk;
|
|
2419
2504
|
process.stdout.write(`
|
|
2420
|
-
${
|
|
2505
|
+
${finalValid ? green("VALID") : red("INVALID")} attestation${v.valid && !anchorOk ? red(" (anchor check failed)") : ""}.
|
|
2421
2506
|
`);
|
|
2422
2507
|
process.stdout.write(` ${dim("Proves the pack came from the issuer key and the claim is true over the disclosed decision")}
|
|
2423
2508
|
`);
|
|
2424
2509
|
process.stdout.write(` ${dim("categories (verdict + capabilities), which reveal no tool inputs, outputs, or data. Completeness")}
|
|
2425
2510
|
`);
|
|
2426
|
-
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer;
|
|
2511
|
+
process.stdout.write(` ${dim("of the disclosed set is attested by the issuer; the anchor fixes it in a public append-only log.")}
|
|
2427
2512
|
|
|
2428
2513
|
`);
|
|
2429
|
-
process.exit(
|
|
2514
|
+
process.exit(finalValid ? 0 : 1);
|
|
2430
2515
|
}
|
|
2431
2516
|
async function handleBundle(argv) {
|
|
2432
2517
|
const { readFileSync, writeFileSync, existsSync } = await import("fs");
|
|
@@ -4121,7 +4206,7 @@ async function main() {
|
|
|
4121
4206
|
if (useHttp) {
|
|
4122
4207
|
const portIdx = args.indexOf("--port");
|
|
4123
4208
|
const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
|
|
4124
|
-
const { startHttpTransport } = await import("./http-transport-
|
|
4209
|
+
const { startHttpTransport } = await import("./http-transport-HLSMVBI6.mjs");
|
|
4125
4210
|
startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
|
|
4126
4211
|
return;
|
|
4127
4212
|
}
|
package/dist/hook-server.js
CHANGED
|
@@ -307,7 +307,13 @@ function signDecision(entry) {
|
|
|
307
307
|
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
308
308
|
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
309
309
|
// - uncertified = unsigned receipt (shadow mode, no signing configured)
|
|
310
|
-
issuer_certification: signerState ? "self-signed" : "uncertified"
|
|
310
|
+
issuer_certification: signerState ? "self-signed" : "uncertified",
|
|
311
|
+
// The signer's PUBLIC key, inside the signed payload, so a receipt is
|
|
312
|
+
// self-contained: any verifier (including the record viewer, in-browser)
|
|
313
|
+
// can check the signature without a side channel. Binding the key inside
|
|
314
|
+
// the signature means it cannot be swapped without breaking the signature;
|
|
315
|
+
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
316
|
+
public_key: signerState.publicKey
|
|
311
317
|
};
|
|
312
318
|
if (entry.tier) payload.tier = entry.tier;
|
|
313
319
|
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
@@ -1039,7 +1045,7 @@ var sha256 = /* @__PURE__ */ createHasher(() => new SHA256());
|
|
|
1039
1045
|
var sha2562 = sha256;
|
|
1040
1046
|
|
|
1041
1047
|
// src/receipt-enrichment.ts
|
|
1042
|
-
var ENRICHMENT_VERSION =
|
|
1048
|
+
var ENRICHMENT_VERSION = 2;
|
|
1043
1049
|
function canonicalJson(value) {
|
|
1044
1050
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
1045
1051
|
const enc = (v) => {
|
package/dist/hook-server.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
startHookServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-DX7QOA3E.mjs";
|
|
4
|
+
import "./chunk-HPKHMVZX.mjs";
|
|
5
5
|
import "./chunk-AYNQIEN7.mjs";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-XLJUZ4WO.mjs";
|
|
7
7
|
import "./chunk-JIDDQUSQ.mjs";
|
|
8
8
|
import "./chunk-D733KAPG.mjs";
|
|
9
9
|
import "./chunk-PQJP2ZCI.mjs";
|
package/dist/index.d.mts
CHANGED
|
@@ -3,6 +3,16 @@ export { BUILTIN_PATTERNS, HookPattern, generateHookSettings, generateSampleCeda
|
|
|
3
3
|
export { createSandboxServer } from './demo-server.mjs';
|
|
4
4
|
import 'node:http';
|
|
5
5
|
|
|
6
|
+
interface PaymentInfo {
|
|
7
|
+
/** Normalized human amount in `asset` units, when derivable; else null. */
|
|
8
|
+
amount: number | null;
|
|
9
|
+
/** Asset symbol (e.g. 'USDC') or contract address, when derivable; else null. */
|
|
10
|
+
asset: string | null;
|
|
11
|
+
/** SHA-256 (hex) of the lowercased recipient: position-blind, when present. */
|
|
12
|
+
recipient_digest: string | null;
|
|
13
|
+
/** x402 scheme ('exact' | 'upto' | ...) when present. */
|
|
14
|
+
scheme?: string;
|
|
15
|
+
}
|
|
6
16
|
interface ReceiptEnrichment {
|
|
7
17
|
/** Rule/schema version, so derivations stay reproducible as rules evolve. */
|
|
8
18
|
v: number;
|
|
@@ -15,6 +25,8 @@ interface ReceiptEnrichment {
|
|
|
15
25
|
kind: 'path' | 'host' | 'command';
|
|
16
26
|
digest: string;
|
|
17
27
|
};
|
|
28
|
+
/** Minimum-disclosure facts about a value transfer (x402 / agent payment). */
|
|
29
|
+
payment?: PaymentInfo;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
interface ProtectPolicy {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ export { BUILTIN_PATTERNS, HookPattern, generateHookSettings, generateSampleCeda
|
|
|
3
3
|
export { createSandboxServer } from './demo-server.js';
|
|
4
4
|
import 'node:http';
|
|
5
5
|
|
|
6
|
+
interface PaymentInfo {
|
|
7
|
+
/** Normalized human amount in `asset` units, when derivable; else null. */
|
|
8
|
+
amount: number | null;
|
|
9
|
+
/** Asset symbol (e.g. 'USDC') or contract address, when derivable; else null. */
|
|
10
|
+
asset: string | null;
|
|
11
|
+
/** SHA-256 (hex) of the lowercased recipient: position-blind, when present. */
|
|
12
|
+
recipient_digest: string | null;
|
|
13
|
+
/** x402 scheme ('exact' | 'upto' | ...) when present. */
|
|
14
|
+
scheme?: string;
|
|
15
|
+
}
|
|
6
16
|
interface ReceiptEnrichment {
|
|
7
17
|
/** Rule/schema version, so derivations stay reproducible as rules evolve. */
|
|
8
18
|
v: number;
|
|
@@ -15,6 +25,8 @@ interface ReceiptEnrichment {
|
|
|
15
25
|
kind: 'path' | 'host' | 'command';
|
|
16
26
|
digest: string;
|
|
17
27
|
};
|
|
28
|
+
/** Minimum-disclosure facts about a value transfer (x402 / agent payment). */
|
|
29
|
+
payment?: PaymentInfo;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
interface ProtectPolicy {
|
package/dist/index.js
CHANGED
|
@@ -35540,7 +35540,13 @@ function signDecision(entry) {
|
|
|
35540
35540
|
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
35541
35541
|
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
35542
35542
|
// - uncertified = unsigned receipt (shadow mode, no signing configured)
|
|
35543
|
-
issuer_certification: signerState ? "self-signed" : "uncertified"
|
|
35543
|
+
issuer_certification: signerState ? "self-signed" : "uncertified",
|
|
35544
|
+
// The signer's PUBLIC key, inside the signed payload, so a receipt is
|
|
35545
|
+
// self-contained: any verifier (including the record viewer, in-browser)
|
|
35546
|
+
// can check the signature without a side channel. Binding the key inside
|
|
35547
|
+
// the signature means it cannot be swapped without breaking the signature;
|
|
35548
|
+
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
35549
|
+
public_key: signerState.publicKey
|
|
35544
35550
|
};
|
|
35545
35551
|
if (entry.tier) payload.tier = entry.tier;
|
|
35546
35552
|
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
@@ -40362,7 +40368,7 @@ function forwardReceipt(signedReceipt) {
|
|
|
40362
40368
|
}
|
|
40363
40369
|
|
|
40364
40370
|
// src/receipt-enrichment.ts
|
|
40365
|
-
var ENRICHMENT_VERSION =
|
|
40371
|
+
var ENRICHMENT_VERSION = 2;
|
|
40366
40372
|
function canonicalJson(value) {
|
|
40367
40373
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
40368
40374
|
const enc = (v) => {
|
package/dist/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
readInstalledConnectorPilots,
|
|
28
28
|
simulate,
|
|
29
29
|
writeConnectorPilots
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-7MHK5RF4.mjs";
|
|
31
31
|
import {
|
|
32
32
|
ProtectGateway,
|
|
33
33
|
buildDecisionContext,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
resolveCredential,
|
|
40
40
|
sendApprovalNotification,
|
|
41
41
|
validateCredentials
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-PB3TC7E3.mjs";
|
|
43
43
|
import {
|
|
44
44
|
createSandboxServer
|
|
45
45
|
} from "./chunk-SETXVE2K.mjs";
|
|
@@ -54,8 +54,8 @@ import {
|
|
|
54
54
|
forwardReceipt,
|
|
55
55
|
getScopeBlindBridge,
|
|
56
56
|
startHookServer
|
|
57
|
-
} from "./chunk-
|
|
58
|
-
import "./chunk-
|
|
57
|
+
} from "./chunk-DX7QOA3E.mjs";
|
|
58
|
+
import "./chunk-HPKHMVZX.mjs";
|
|
59
59
|
import {
|
|
60
60
|
sha256 as sha2562
|
|
61
61
|
} from "./chunk-AYNQIEN7.mjs";
|
|
@@ -73,7 +73,7 @@ import {
|
|
|
73
73
|
policySetFromSource,
|
|
74
74
|
runEvaluatorSelfTest,
|
|
75
75
|
signDecision
|
|
76
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-XLJUZ4WO.mjs";
|
|
77
77
|
import {
|
|
78
78
|
Field,
|
|
79
79
|
_abool2,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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",
|