Three fixes found by running the cloud-gateway path end to end before documenting it:
- Fail-open fixed: in
--httpmode, loaded Cedar policies were never attached to the gateway (the stdio path attached them after the HTTP branch returned), so a remote gate started with--enforceand a Cedar policy silently allowed everything. Cedar policies are now attached before the child starts, and a forbidden tool call over HTTP is denied. - Silent-unsigned fixed: a Cedar-mode gateway never read
signingfrom protect-mcp.json (only the JSON-policy branch did), so it emitted no receipts while looking fully configured. Cedar mode now picks up signing and credentials from protect-mcp.json when present and says so on stderr. - Gateway receipts now chain: the wrapped-server path (stdio and HTTP) threads previousReceiptHash per draft s5.7 and resumes the chain across restarts from the log tail, matching the hook server. Verified end to end: a cloud-style client over Streamable HTTP, one allow and one deny, chained draft-02 receipts, replayed offline with the published @veritasacta/verify with zero chain breaks.
Also in this release, the policy digest becomes a recomputable commitment (prompted by public review of opaque policy identifiers):
- One normative construction for every engine,
acta-policy-digest-v1: per-file SHA-256s into a sorted{construction, engine, files}manifest, JCS, SHA-256, emitted as"sha256:<64 hex>". Replaces two divergent code-defined preimages that concatenated sources (ambiguous file boundaries) and truncated to 16 hex characters with no prefix. Receipts now carry the full-strength digest; pre-0.10 digests should be treated as opaque labels. protect-mcp policy digestprints the digest, the per-file hashes, and the recompute rule (--expectfor CI,--jsonfor tooling).protect-mcp policy publishwrites theacta.policy-bundle.v1bundle (policy bytes plus preimage spec, addressed by the digest) ready to host at.well-known/acta-policies/<hex>.json; a verifier confirms which policy governed a receipt from the bundle bytes alone, with no communication with the issuer.- The construction is specified normatively in
draft-farley-acta-signed-receipts-03 (section 5.8), alongside
require_approvalas a decision value.
Receipts are now emitted in the envelope the IETF draft actually specifies
(draft-farley-acta-signed-receipts-02): a two-field
{ payload, signature: { alg: "EdDSA", kid, sig } } envelope, with the
access-decision fields from section 3.1 (type: "protectmcp:decision",
tool_name, decision, reason, policy_digest), issuer_id equal to
signature.kid (section 2.2), the signature computed as PureEdDSA directly
over the JCS bytes of the payload with no pre-hash (section 5.6), and new
keys defaulting to the section 2.1.1 recommended
sb:issuer:<base58-fingerprint> kid format (existing key files keep their
explicit kid). Previously the package emitted an internal envelope shape
that predated the published draft.
Receipt logs are now hash-chained per section 5.7: each receipt's
previousReceiptHash is the bare-hex SHA-256 of the JCS bytes of the
previous log line exactly as written, signature included. The hook server
resumes the chain across restarts from the tail of the existing log, and
signing-failure tombstones participate in the chain so an unsigned gap
cannot be silently dropped.
Verification is dual-shape everywhere (the verify_receipt MCP tool, the
serve --enforce self-test, the embedded record viewer, and the exported
verifyReceipt API): receipts written by protect-mcp 0.9.x and earlier
(flat v1 artifacts and structured v2 envelopes with a top-level signature
string) continue to verify, and a mixed pre/post-migration log replays
cleanly, including the chain link that spans the boundary. Verified against
3,352 real receipts from a production gate log: all verify. The published
@veritasacta/verify CLI (0.9.2) verifies the new envelopes as-is via its
passport path; no verifier upgrade is required.
New module acta-envelope (exported from the package root):
createReceiptEnvelope, verifyReceipt, receiptHash, receiptIdentity,
computeSbIssuerKid. signDecision accepts an optional previous-chain-hash
argument and returns the emitted receipt's chain hash. @noble/curves and
@noble/hashes moved from optional to regular dependencies.
Fixed: importing the package as a library no longer attaches the demo
server's stdin JSON-RPC listener (it now only starts when demo-server is
the process entry point). Previously any require("protect-mcp") kept the
host process's event loop alive and answered JSON-RPC on stdout.
Note for strict draft conformance: decision may be "require_approval" for
held-for-co-sign flows, an extension value beyond the section 3.1 set
(allow, deny, rate_limit), pending a revision of the draft.
protect-mcp mcp boots a stdio MCP server exposing the gate itself as four
tools, so an agent can call the gate directly instead of only through the
PreToolUse/PostToolUse hooks: evaluate_action (Cedar decision, fail-closed),
sign_decision (Ed25519 receipt, byte-compatible with the runtime gate's),
verify_receipt (offline check), and self_test (proves a known-forbidden
action is denied and a signed receipt round-trips). All four are read-only
and annotated with what they return. A bare-spawnable bin,
protect-mcp-mcp, is included alongside the mcp subcommand so any MCP
host or registry can point at either.
The gate is default-deny and fail-closed, but a deny used to be a dead end: an empty reason and no pointer to the file that decides. This release makes the policy legible and editable from the terminal, and lets a running gate pick up a policy change without a restart.
policycommand.policy listshows every tool named in the policy (permit / forbid / default-deny) cross-referenced with how often the gate actually allowed or denied it;policy showprints the active policy and its digest;policy allow <tool>/policy deny <tool>append a Cedar rule and print the digest change;policy pathprints the file. Idempotent and tool-name validated.- Denies that teach. A blocked call now says whether it was default-deny
(no permit matched) or an explicit forbid, names the policy directory, and
gives the exact fix:
npx protect-mcp policy allow <tool>. - Hot reload.
protect-mcp servewatches the .cedar files and reloads the policy on an on-disk change, sopolicy allow/deny(or a hand edit) takes effect without restarting the gate. Fail-closed: an unparseable edit keeps the previous policy rather than opening the gate, and the reload is logged.
The public demo film (legate.scopeblind.com/record) is now reproducible by anyone in 60 seconds, and the CLI answers the first two commands every new user types.
sample. Seeds a clearly-labeled sample record (fresh Ed25519 keypair, kidsample-demo): 8 decisions including one BLOCKED network call and two payments ($0.02 x402-style per-call, $12.50 invoice), plusdemo-tampered.jsonlwith one decision flipped after signing. The receipts are real signed artifacts (same envelope and canonical-JSON preimage as the gate's signer), sorecord,claim --payment-under,verify-claim, andanchor-recordwork on the folder immediately. Refuses to touch an existing record or signing key;--forceoverrides.--version/version/-V. Prints the installed version. Previously these fell through to wrap-mode parsing and errored demanding a--separator, which was the first thing many new users saw.helpword.npx protect-mcp helpprints usage (previously errored). Help and version flags are now only recognized BEFORE the--separator, soprotect-mcp -- node server.js --versionwraps that command as intended instead of printing our help.
The provenance layer reaches the agentic economy's payment rails (x402), the record gains continuous completeness, and anchoring can carry an identity.
- Payment receipts (x402 interop). The gate now tags agent payments with a
signed
paymentcapability, detected broadly across x402 wire shapes (paymentRequirements,X-PAYMENT, EIP-3009transferWithAuthorization) and payment-shaped tools. Receipts carry minimum-disclosure payment facts: amount (only when clearly readable in human units), asset, a HASHED recipient, and the x402 scheme.claim --no paymentproves no agent paid anything;claim --payment-under <cap>proves every payment stayed under a cap, and an amount the gate could not read counts as OVER (you cannot prove an amount you could not read, so the claim cannot lie). - Record checkpoints (
anchor-record). Anchors the record's CURRENT commitment (the same Merkle root a claim commits to, plus count and time range) into the public log. Run it on a heartbeat and the record grows an anchored history: a later claim whose root matches a checkpoint is provably over the complete set as of that checkpoint. Skips when the record is unchanged; writes a local.protect-mcp-anchors.jsonlhistory; only the root, count, and time range leave the machine. - Pinned identity in the anchor loop.
claim --anchor,anchor-record, andverify-claimnow resolve the anchoring key against the public ScopeBlind key directory: an enrolled key shows "anchored as (key pinned)", a revoked key fails verification, an anonymous key points at enrollment. The free anchor stays anonymous; the named identity is the paid upgrade.
Two verification upgrades: the anchor check moves into the verifier, and the record viewer proves signatures instead of just labelling them.
verify-claimnow verifies the anchor automatically: it finds the<claim>.anchor.jsonsidecar, checks offline that the anchored envelope binds this exact claim (digest, record root, and the same issuer key), and confirms against the public log that the digest sits at the recorded entry. A failed binding makes the attestation INVALID; an unreachable log does not (the local checks stand alone).--check-anchormakes a missing anchor fatal,--anchor-file <p>overrides the path,--offlineskips the network hop.- The
recordviewer now verifies Ed25519 signatures in the browser, locally: each row shows✓ verified,✗ invalid signature, orsigned · unpinned key, and the stat strip counts how many verified. The CLI injects your gateway's PUBLIC key so rows verify against your own key (the private key never reaches the page). - Receipts now embed the signer's public key inside the signed payload
(
payload.public_key), so a receipt is self-contained: any verifier can check it without a side channel, and the key cannot be swapped without breaking the signature. Older receipts still verify against a pinned or pasted key.
Closes the one honest gap in a bare claim: that the disclosed set is complete.
- Added
protect-mcp claim --anchor: records the claim's digest in the public, append-only ScopeBlind transparency log, so a counterparty who does not trust you can confirm the exact claim existed at a time and was not quietly re-cut. Only the hash is sent; the claim, its leaves, and every receipt stay on your machine. Writes a<claim>.anchor.jsonsidecar with the log entry and a verify URL. Anonymous and free; enrolling an org identity a counterparty can pin is the paid upgrade (scopeblind.com/enroll).
The record viewer closes the loop from a decision to a portable proof.
- The
recordviewer surfaces the exactclaimcommand when you filter by a capability, so a decision becomes a signed, position-blind attestation in one step, with copy-to-clipboard and an offlineverify-claimhint. - The browser drop-viewer (legate.scopeblind.com/record) reached parity with the CLI: signed capability tags, a capability facet, and a provenance tree.
Prove a predicate over the record without revealing it.
- Added
protect-mcp claim: mints a signed attestation of a predicate over the record (--no <cap>for "no action carried this capability",--only,--count/--no-verdict <verdict>), disclosing only per-decision categories (receipt digest, verdict, capability tags), never inputs, outputs, or data. - Added
protect-mcp verify-claim: checks it offline via Ed25519 over the pack, a recomputed Merkle root over the disclosed set, and the predicate recomputed independently. Honest trust model: an accountable position-blind attestation, not zero-knowledge; set-completeness is issuer-attested.
Each signed decision now carries minimum-disclosure, rule-derived metadata.
input_digest: SHA-256 of the canonicalized tool input, so you can prove what was requested without storing it.- Signed
capabilities: deterministic, rule-derived tags (fs.read, fs.write, net.egress, secret.adjacent, destructive, financial, exec.shell, ...) so the record is queryable by what an action touched. resource: the hashed target (path, host, or command). The gate sees tool calls, not the model's prompt or reasoning, and does not claim to.
- Added
protect-mcp record --live: a local, streaming view of the receipt file that turns decisions into a searchable, auto-labelled record as the agent runs (stat strip, per-row signed badge and digest, a List/Tree provenance view), all local, nothing uploaded. - One-click export:
.jsonlcarrying the real signatures (so a recipient verifies offline withnpx @veritasacta/verify) plus a Markdown report.
- Added
protect-mcp record, a local browser viewer over the receipts file (the answer to "the receipts are a hidden dotfile I cannot find"). - Fixed hard-coded version strings in the banner and health/serverInfo: they read the resolved package version now.
- WebAuthn co-sign hardening: verify the rpIdHash and enforce challenge expiry.
Everything the scopeblind.com trial and docs pages describe now ships in the published package: wrap, dashboard, trial, policy packs, connectors, the registry client, and selective disclosure v0.
- Added
protect-mcp trial, a guided ten-minute local trial: run the demo tool server, watch risky calls, install a policy pack, require an approval, and export a signed receipt you can verify offline. - Added
protect-mcp connectors, dry-run connector pilots for GitHub, email, filesystem/Git, Slack or Teams, and a mock-to-real PMS adapter (connectors list|show|init|doctor). These are scaffolds with policies and config, not a managed marketplace; GitHub, Slack/Teams, and real PMS mode still need customer tokens or adapters. - Added
protect-mcp wrapfor install-to-aha onboarding. It prints a protected MCP command, can patch Claude Desktop MCP server config in dry-run mode, and writes only when--writeis passed. - Added
protect-mcp dashboard, a local-only127.0.0.1dashboard for shadow-mode inventory, risk review, receipts, and next policy moves. - Added exact-action readbacks to decision logs and signed receipts: tool, action, destination, redacted payload preview, payload hash, disclosed fields, and redacted fields.
- Upgraded the local dashboard into an action-control surface: policy coverage,
one-click
Require approval/Block/Observedrafting, pending approval queue, reason capture, desktop approval forwarding, receipt-chain view, and audit-bundle export with explicit signed-receipt preflight. - Added
protect-mcp recommend, which drafts a reviewable JSON policy from observed local tool calls before users flip wrappers into--enforce. - Added
protect-mcp registry, a paid-boundary MVP for hosted org identity, receipt digest anchoring, org public-key directory, billing-account metadata, and a static verifier page. Hosted mode uploads digests only, not raw prompts, payloads, outputs, private keys, or raw receipts. - Added
protect-mcp killer-demo, which generates a three-minute shadow-mode → policy → exact approval → gateway execution → signed receipt → tamper failure → selective-disclosure demo pack. - Added Selective Disclosure v0 on committed receipts:
committed_fields_root, multi-field disclosure packages, verifier explanations for disclosed versus hidden fields, CLI verification viaprotect-mcp verify-disclosure, and audit bundle inclusion. This is salted commitments + Merkle proofs, not full ZK. - Added
protect-mcp policy-packs, a starter Cedar template library for filesystem-safe, Git-safe, email-safe, database-safe, cloud-spend-safe, secrets-safe, and finance-mandate-safe rollouts. - Expanded the built-in demo MCP server with GitHub PR, email, and mock PMS booking tools so the demo maps to agent actions a hedge fund actually cares about.
- Reworked the README quickstart around the practical path: initialize, wrap, inspect, draft policy, then enforce.
Two correctness fixes for policy authors who rely on the documented Cedar shape.
The evaluator maps tool input to Cedar context.input, and the bundled policy
examples are written against context.input.*, but the evaluate CLI path and
the HTTP hook server only flattened tool input into top-level context fields. So
a policy keyed on context.input.path silently saw nothing on those paths: a
forbid that should have denied never fired, and the call was allowed. Both
paths now pass the tool input through, so nested-shape policies match. The
existing flattened fields are kept for back-compat. (Thanks to @koriyoshi2041,
scopeblind-gateway#8, for the report and repro.)
Separately, the hook server's Cedar evaluation fell through to allow on an unexpected evaluator throw, which contradicted the "denies on any error" guarantee from 0.7.0 (the CLI path already failed closed). The hook path now denies on an unexpected eval error while a policy is configured.
Regression coverage added on all three paths; npm ci from a clean clone also
works again (the lockfile was out of sync).
The evaluate and sign verbs now accept --format <host>
(claude | codex | gemini | cursor | hermes | grok). With it, the verb reads the
host's hook payload from stdin (tool name and input) and emits the deny verdict
in that host's hook contract, so the same fail-closed Cedar gate works as a
PreToolUse/PostToolUse hook outside Claude Code.
The load-bearing detail: Hermes ignores hook exit codes and reads the verdict
from stdout, so --format hermes denies via {"decision":"block"} on stdout. A
raw exit-2 (which every other host honors) would have silently failed open there.
Cursor and Gemini receive their structured stdout deny verdict in addition to
exit 2. Without --format, the verbs behave exactly as before (the
--tool/--input flag mode is unchanged).
No code change from 0.7.0. Rewrote the README to lead with the fail-closed and
self-test guarantees, and added a SECURITY.md disclosure policy (supported
versions, the affected 0.5.x/0.6.x range, the published advisory, and the
coordinated-disclosure process). The package now ships SECURITY.md.
This release fixes the way the Cedar policy gate behaves when anything goes wrong, and a separate defect that meant Cedar policies were not being evaluated at all against the pinned engine. If you rely on protect-mcp to enforce a Cedar policy, upgrade.
- Fail closed, not open. Before 0.7.0, if the Cedar engine was unavailable,
the result was malformed, evaluation threw, or a policy errored, the evaluator
returned ALLOW. A security gate must do the opposite. Every error and
uncertainty path now DENIES. The allow-on-error behavior is reachable only by
explicitly passing
{ failClosed: false }(observe mode), and even then the decision is flaggedwould_deny: trueso a failure is never silent. - An errored policy can no longer permit-all. Cedar silently discards a
policy that errors at evaluation (for example the
context.<string> in [list]type error in the 0.5.x and 0.6.x advisory), which could leave a residual permit standing. The evaluator now treats any per-policy error as an evaluation error and denies under enforcement. - Cedar policies are actually evaluated now. The
isAuthorizedcall passed the policy text as a bare string, but@cedar-policy/cedar-wasm@4.xrequires a structuredPolicySet. As a result every Cedar evaluation errored against the pinned engine and (with the old fail-open default) allowed everything. The call shape and the response parser are corrected, so aforbidrule actually denies and apermitactually allows.
protect-mcp evaluateandprotect-mcp sign: one-shot per-call verbs for PreToolUse and PostToolUse hooks.evaluateloads a Cedar policy, evaluates one tool call fail-closed, and exits 2 on deny (so the tool is blocked) and 0 on allow; a missing or unloadable policy denies unless--fail-on-missing-policy falseis set. This makes hook configs that invoke these verbs work as written.runEvaluatorSelfTest()plus a startup gate:serve --enforceruns the self-test before arming and refuses to start if the engine cannot prove it denies a known-forbidden vector.protect-mcp doctorreports the same, so the gate verifies its own restraint before it is trusted.- A CI tripwire test that fails the build if the discarded
in-on-String pattern is ever reintroduced into a shipped policy.
The fail-open behavior and the unevaluated-Cedar defect are present in the 0.5.x and 0.6.x lines. Upgrade to 0.7.0.