diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..cb441f3
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,8 @@
+# Private key — NEVER publish to npm
+sigil-key.json
+
+# Sigil generation script — development only
+generate-sigil.mjs
+
+# Test files not needed by end users
+test/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5fb75f7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,452 @@
+# Changelog
+
+
+## 0.6.1 - 2026-05-16
+
+- Support `--jwks` resolution from `file://` URLs, absolute paths, and bare relative filesystem paths for offline test-vector and CI workflows.
+- Preserve HTTP(S) JWKS behavior unchanged.
+- Surface the resolved local JWKS path in verifier output.
+- Add unit coverage for local JWKS path handling.
+
+## 0.5.4 — 2026-04-20 (Rekor anchoring + hardware attestation + transparency profiles + watcher + SBOM bundles + AIP-0007)
+
+Ships the "differentiation roadmap" responding to the Signet / nono
+feature-parity analysis. Thirteen new primitives across four strategic
+tiers; none of them imitate Signet or nono — they extend the shipping
+product to ground that only Veritas Acta holds.
+
+### New AIP
+
+- **AIP-0007** (Draft) — Zero-Knowledge Compliance Proofs. Portable
+ receipt-chain-level proof that every receipt adhered to a declared
+ policy without revealing receipts. Target release: v0.7.0; the spec
+ is committed now for public review.
+
+### New engines
+
+- **`src/engines/rekor.js`** — Transparency-log anchoring (AIP-0005 T4).
+ Offline verification of Rekor / Sigstore inclusion proofs.
+ RFC 6962 Merkle-path recomputation + Signed-Note signature
+ verification. ISO 8601 duration parsing for `anchored_within`.
+- **`src/engines/attestation-quote.js`** — Hardware-attestation quote
+ validator (AIP-0005 T2). Dispatches to per-platform validators:
+ ATECC608B (full crypto validator), Apple Secure Enclave (full),
+ TPM2 / SGX / SEV-SNP / TDX (structural in v0.5.4; full crypto in v0.7).
+ Enforces that `measured_kid` matches `signature.kid`.
+- **`src/engines/watch.js`** — Live receipt watcher + webhook
+ dispatcher. Rule kinds: `cost_tier_below`, `delegation_expiring_within`,
+ `chain_break`, `deny_decision`, `scrub_triggered`. Slack / Discord /
+ generic JSON payloads.
+- **`src/engines/sbom.js`** — SBOM-audit bundle builder. Ingests SPDX /
+ CycloneDX / unknown-format SBOMs; builds a deterministic
+ `receipts_fingerprint` + canonical manifest; optional signing via
+ caller-supplied callback.
+- **`src/engines/transparency.js`** — Four profiles (private, auditable,
+ transparent, high-assurance), profile-based anchor decisions, and a
+ public-facing badge JSON format.
+
+### New packages
+
+- **`@veritasacta/cross-verify`** — Arbitrator tool for multi-format
+ sessions. Extracts canonical `(tool, input_hash, issued_at)` tuples
+ from Signet / Sigstore / Acta receipts and confirms agreement.
+ Emits a SHA-256 agreement fingerprint. 19 unit tests.
+
+### Ecosystem artifacts
+
+- **`docs/voprf-issuance-for-implementers.md`** — Public pitch for
+ competitors to route their T1 cost-tier through our commercial API.
+- **`docs/framework-author-guide.md`** — Adoption onboarding for
+ CrewAI / LangChain / Vercel AI / etc.
+- **`docs/posts/infrastructure-not-competitor.md`** — Public letter to
+ peer receipt-format projects articulating the infrastructure stance.
+- **`docs/case-study-three-ecosystems.md`** — Microsoft + AWS +
+ Anthropic contribution ledger.
+- **`ecosystem/certify/`** — Weekly cross-implementation conformance
+ certification program (workflow + runner skeleton).
+- **`ecosystem/dashboard/`** — Upgraded with a DAG view for trace_id
+ grouping.
+
+### Tests
+
+- 54 new unit tests: 12 Rekor, 11 attestation-quote, 12 watch, 7 sbom,
+ 14 transparency, 19 cross-verify (cross-verify is a separate package).
+- verify-cli suite: **219 unit+integration + 26 conformance = 245 total**,
+ all green.
+
+### Sigil
+
+- Canonical release: **Open Wind** (`677a8a81`).
+- 36 source files monitored (up from 31 in v0.5.3).
+
+## 0.5.3 — 2026-04-20 (delegation chains + bilateral cosign + trace_id + proxy hardening + dashboard)
+
+Responds to the Signet / nono comparison: adds the four receipt-shape
+primitives that peer implementations have ("who authorized this
+agent", "agent + server co-signed", "these receipts all belong to one
+workflow", "receipts are visualisable") and the two proxy-layer
+security primitives ("scrub secrets from captured args", "cosign the
+same decision from two independent keys"). None of this requires new
+cryptography. All composes with existing AIPs.
+
+### New AIP
+
+- **AIP-0006** (Draft) — Delegation Chains. Defines a `delegation`
+ receipt type conveying scoped, time-bounded, narrowing-only
+ authority from a delegator to a delegate. Defines an
+ `authorization.delegation_chain` payload field on action receipts
+ referencing delegations by receipt_hash. Verifier walks to a trust
+ anchor, checks signatures, expiry, scope subset, and max_depth at
+ each hop.
+
+### New engines
+
+- **`src/engines/delegation.js`** — `verifyDelegationChain()` walks the
+ chain from leaf to root, validating Ed25519 signatures against trust
+ anchors, scope subset at each hop (narrowing-only), and the action's
+ tool/target against the leaf's scope. 14 unit tests.
+- **`src/engines/cosign.js`** — `verifyCosignatures()` + `attachCosignature()`.
+ Envelope-level additive signatures. Each cosignature signs the SAME
+ canonical payload bytes as the primary `signature`. Default semantic:
+ all cosignatures must be resolved + valid; `requireAllValid: false`
+ opt-in for M-of-N. 11 unit tests.
+- **`src/engines/dashboard.js`** — `startDashboard()` spins up a
+ loopback-only HTTP server serving `ecosystem/dashboard/` static + a
+ `/api/receipts` JSON feed for a configured directory. DNS-rebinding
+ defense (rejects non-loopback Host headers) and path-traversal
+ defense. 6 smoke tests.
+
+### New subcommand + CLI surface
+
+- **`verify dashboard [--port 3847] [--bind 127.0.0.1] [--receipts-dir
]`** —
+ Start the local dashboard. Opens immediately; no build step.
+- **`verify proxy ... --bilateral --server-key `** — Proxy now
+ attaches a second independent signature (via `cosignatures[]`) to
+ every receipt. Enables agent + server bilateral evidence without
+ touching the primary signing path.
+- **`verify proxy ... --scrub-secrets`** — Walks incoming tool args for
+ probable-secret key names (`api_key`, `token`, `password`,
+ `authorization`, etc.), redacts VALUES in the outgoing call, and
+ flags the redacted paths on the receipt via `scrub_detected`. Secrets
+ no longer enter the receipt even as a hash of the real value.
+- **`verify proxy ... --trace-id `** — Stamps every receipt with a
+ workflow `trace_id` so multi-step flows group cleanly.
+
+### Receipt-format extensions (non-breaking)
+
+- **`trace_id`** + **`parent_receipt_id`** as optional AIP-0001 payload
+ fields. `previousReceiptHash` remains the chain pointer; trace_id
+ groups by workflow; parent_receipt_id expresses non-chain causal
+ links. `chain explore` surfaces both; `groupByTrace(result)` buckets
+ nodes by workflow.
+- **`cosignatures: [{alg, kid, sig}, ...]`** as an optional
+ envelope-level array. Old verifiers ignore it; v0.5.3+ verifiers
+ check each one against caller-supplied trust anchors.
+- **`authorization.delegation_chain: [...]`** as an optional
+ AIP-0001 payload field. AIP-0006 verifiers walk it; others treat as
+ opaque.
+
+### Tests
+
+- 41 new unit tests across this release: 14 delegation, 11 cosign,
+ 2 chain-explore trace, 8 proxy helpers, 6 dashboard.
+- Full suite: **163 unit+integration + 26 conformance = 189 total**,
+ all green.
+
+### Sigil
+
+- 31 source files monitored (up from 28 in v0.5.2). Canonical release:
+ **Bright Lake** (`ea78b16e`).
+
+## 0.5.2 — 2026-04-20 (compliance export + DSSE + BRASS v2 scaffold + AIP-0004/0005)
+
+Ships alongside v0.5.1 as the "governance surface fill" release. Adds
+the compliance export subcommand, Sigstore DSSE envelope engine,
+BRASS v2 hardening scaffold, and reference implementations for two new
+AIPs.
+
+### New subcommand
+
+- **`verify compliance --receipts-dir `** — bucket a directory of
+ receipts into SOC 2 / ISO 42001 / EU AI Act controls and emit an
+ auditor-ready JSON bundle or self-contained HTML report. Supports
+ `--framework soc2|iso42001|eu-ai-act|all`, `--start-date`, `--end-date`,
+ `--org`, `--output`. Zero-evidence controls are surfaced explicitly
+ so the auditor sees gaps rather than hidden silences.
+
+### New engines
+
+- **`src/engines/dsse.js`** — Dead Simple Signing Envelope (DSSE) wrap /
+ unwrap / verify. Produces and consumes Sigstore-compatible envelopes
+ with payload types `application/vnd.acta.receipt+json`,
+ `application/vnd.acta.knowledge-unit+json`, or the standard in-toto
+ statement type. Signatures bind to the DSSE pre-authentication
+ encoding (PAE), not the raw payload.
+- **`src/util/voprf-crypto-v2.js`** — BRASS v2 scaffold: length-prefixed
+ hashing (`H_LP`), nullifier derivation bound to issuer public key Y
+ (`deriveNullifier_v2`), single-variable πC restatement
+ (`piCVerify_v2`). Not wired into the default path; accessible to
+ implementers and exercised by unit tests.
+
+### New AIPs
+
+- **AIP-0004** (Draft) — Content-Addressed Snapshot and Rollback
+ Receipts. Defines `snapshot` and `rollback` receipt types with a
+ Merkle root over file-content hashes. Reference implementation at
+ `ecosystem/rollback/snapshot.mjs`; schema at
+ `ecosystem/rollback/snapshot-receipt.schema.json`.
+- **AIP-0005** (Draft) — Attestation Weight Profile. Defines a
+ portable `cost_tier` (T0–T4) over receipts, substantiated by VOPRF
+ tokens (T1), hardware quotes (T2), multi-party signatures (T3), or
+ transparency-log anchoring (T4). Reference implementation notes at
+ `ecosystem/physical-attestation/DESIGN.md` + attestation-quote
+ schema.
+
+### Ecosystem additions
+
+- **`ecosystem/wshobson-plugin/protect-mcp/`** — PR-ready Claude Code
+ plugin tree for `wshobson/agents` marketplace. Closes issue #471.
+ Ships agents (`policy-enforcer`, `receipt-verifier`), skill
+ (`protect-mcp-setup`), slash commands (`/verify-receipt`,
+ `/audit-chain`), and hooks.json.
+- **`ecosystem/dashboard/index.html` + `dashboard.js`** — local-first
+ in-browser audit dashboard scaffold. JCS + chain-integrity check
+ over dropped receipts; renders `verify --json` output. No server,
+ no telemetry.
+- **`ecosystem/physical-attestation/DESIGN.md`** — physical-digital
+ causal chain design for Seal hardware cost_tier T2 receipts.
+
+### Sigil + tests
+
+- Sigil commitment expanded to **28 source files** (adds compliance
+ export, DSSE engine, v2 crypto util). Canonical release: **New Ember**
+ (`b28f8d60`).
+- 41 new unit tests across prompt, chain-explore, snapshot, compliance,
+ DSSE, and BRASS v2 (11 of 41 new in this release).
+- Full suite: **122 unit+integration + 26 conformance** — 148 total,
+ all green.
+
+## 0.5.1 — 2026-04-20 (prompt provenance + chain explorer + 5 sandbox profiles)
+
+### New subcommands
+
+- **`verify prompt `** — verify the provenance of a prompt/skill/system-instruction file against a Veritas Acta receipt asserting its SHA-256, a Sigstore DSSE bundle with an in-toto subject, or an `--expected-hash`. Closes the supply-chain attack vector where an attacker modifies `CLAUDE.md`, `SKILLS.md`, `AGENTS.md`, or a system prompt between authoring and agent runtime.
+- **`verify chain explore `** — walk the `previousReceiptHash` chain back to its root, validating every hash link. Emits a depth-annotated ASCII tree in terminal mode, structured JSON in `--json` mode. `--search-dir ` overrides the ancestor search directory; `--max-depth N` caps the walk.
+
+### Sigil commitment expansion
+
+- Sigil v0.5.1 now covers **25 source files** (up from 24 in v0.5.0): adds `src/engines/prompt.js` + `src/engines/chain-explore.js`. Canonical release: **Bright Star** (`1cc829ab`).
+
+### Ecosystem profiles
+
+- **`ecosystem/profiles/`** ships pre-built sandboxing profiles for five common agent runtimes: Claude Code, Cursor, Codex, Gemini CLI, OpenClaw. Each ships `profile.yaml` + `policy.cedar` + `nono-capabilities.yaml` + `README.md` with threat-model notes. Composes with `sb-runtime --ring N --policy ./policy.cedar` and `nono run --caps ./nono-capabilities.yaml` for defense-in-depth.
+
+### Tests
+
+- 20 new unit tests: 10 for `verifyPrompt` (expected-hash / receipt / Sigstore / missing-source / error paths), 10 for `exploreChain` / `renderChainTree` (3-receipt chain, tamper detection, missing ancestor, maxDepth, searchDir override).
+- Full suite now: **81 unit+integration + 26 conformance** — 107 total.
+
+## 0.5.0 — 2026-04-19 (unified verifier + network-effect mechanics)
+
+### Network-effect mechanics
+
+- **`--attest`** produces a canonical verifier attestation: a signed
+ JSON artifact the user can publish anywhere to demonstrate they ran
+ the canonical unmodified verifier. Fully offline, user-signed, opt-in.
+ `--attest-org ` attaches an attributable identifier.
+ `--attest-key ` overrides the default key location
+ (`~/.veritasacta-verify/attester.json`).
+- **`--emit-verification-receipt`** produces a signed receipt of a
+ specific verification event — "this receipt verified valid by the
+ canonical verifier at time T." Composable with Sigstore Rekor.
+
+### Enterprise features
+
+- **`--pin-sigil `** enforces that the installed verifier
+ matches a specific Sigil. Fails fast with exit code 2 and a clear
+ message on mismatch. Supply-chain pinning for regulated deployments.
+- **`--audit-log `** appends every verification event to a local
+ JSONL file with chain-linked hashes. Tamper-evident local audit trail
+ for SIEM integration. Fully offline; nothing phoned home.
+- **`--fips`** enforces FIPS 140-3 approved algorithms only. Currently
+ rejects Ed25519 (pending NIST approval) with a clear migration
+ message pointing at hybrid `ed25519+ml-dsa-65` (v0.6+).
+- **`--replay-chain `** bulk-verifies every receipt in a JSONL
+ chain. Reports total / verified / failed / chain-breaks. Chain
+ linkage (`previousReceiptHash`) is explicitly validated.
+- **`--diff `** structural diff between two receipts.
+ Surfaces added/removed/changed fields, canonical hash comparison,
+ and signature equality. Debugging aid for implementers.
+- **`--audit-report`** renders a self-contained HTML audit report
+ suitable for delivery to auditors / compliance teams / counterparties.
+ Embeds the canonical attestation if `--attest` is also set.
+ Includes verification summary, per-receipt breakdown, verifier
+ provenance, and raw JSON result.
+- **`--output `** writes HTML reports or attestation JSON to a
+ file instead of stdout.
+
+### Sigil commitment expansion
+
+- Sigil v0.5.0 commits to **21 source files** (up from v0.3.0's single
+ cli.js): cli.js + 20 engines/outputs/utils/context files. Any
+ modification invalidates `--self-check`.
+
+### New subcommands (bootstrap + integration)
+
+- **`verify init`** — zero-config onboarding wizard. Auto-detects framework across 13 supported agents (Claude Code, Claude Agent SDK, Google ADK, CrewAI, Pydantic AI, AutoGen, Smolagents, LangChain JS/Py, LangGraph JS/Py, OpenAI Agents, Vercel AI). Generates keys, writes `.veritasacta/config.json`, emits next-steps. `--framework ` override, `--force` overwrite.
+- **`verify proxy --target ""`** — universal MCP proxy. Wraps any MCP server with signing. No code changes in server or agent; each `tools/call` emits a chain-linked receipt. Signet-parity.
+- **`verify daemon`** — sidecar daemon on Unix socket. Language-agnostic signing API (`POST /sign`). One daemon handles receipts for any number of agents in any language.
+
+### Ecosystem artifacts (`ecosystem/`)
+
+Shipped (working code):
+
+- `ecosystem/github-action/` — drop-in CI step (`VeritasActa/verify-action@v1`)
+- `ecosystem/claude-code-plugin/` — one-click Claude Code plugin + SKILL.md
+- `ecosystem/homebrew-tap/Formula/veritasacta-verify.rb` — `brew install veritasacta-verify`
+- `ecosystem/sdk-js/` — `@veritasacta/sdk` tiny signing helper (JS)
+- `ecosystem/sdk-py/` — `veritasacta-sdk` tiny signing helper (Python)
+- `ecosystem/adapters/langchain/` — LangChain adapter with full `withReceipts()` implementation
+- `ecosystem/adapters/{langgraph,crewai,openai-agents,vercel-ai,smolagents,pydantic-ai,autogen}/` — seven additional framework adapter scaffolds
+- `ecosystem/registry-worker/` — `registry.veritasacta.com` Cloudflare Worker
+- `ecosystem/badge-worker/` — `verify.veritasacta.com/badge/*` shields.io-compatible SVG badges
+- `ecosystem/interop-leaderboard/workflow.yml` — weekly cross-implementation interop CI
+
+Scaffolds (design docs, implementation pending):
+
+- `ecosystem/cosign-compat/DESIGN.md` — v0.6.0 Sigstore compatibility
+- `ecosystem/rollback/DESIGN.md` — filesystem snapshots + undo (nono-style)
+- `ecosystem/supervisor/DESIGN.md` — runtime approval flows
+- `ecosystem/reputation/DESIGN.md` — issuer reputation (complement to aeoess agent reputation)
+- `ecosystem/dashboard/DESIGN.md` — web audit dashboard (Signet-style)
+- `ecosystem/browser-extension/DESIGN.md` — Claude.ai / ChatGPT consumer reach
+- `ecosystem/ebpf-observer/DESIGN.md` — kernel-level auto-instrumentation (highest novelty)
+- `ecosystem/vscode-extension/` — editor integration (v0.5.1)
+- `ecosystem/CONFORMANCE-CERTIFICATION.md` — commercial certification service design
+- `ecosystem/SIGIL-NAMING.md` + `ecosystem/RELEASE-NAMING.md` — public brand convention + historical Sigil registry
+
+## 0.5.0 core — 2026-04-19 (unified verifier)
+
+### Major
+
+- **Unified verifier.** Single Apache-2.0 binary now handles Ed25519
+ signed receipts, VOPRF anonymous-credential tokens (full dual-DLEQ
+ verification), Knowledge Unit bundles, and selective-disclosure
+ receipts. Auto-detects input format; `--mode receipt|voprf|ku|
+ bundle|auto` forces a specific engine.
+- **Full VOPRF DLEQ verification.** Both the issuer proof (πI:
+ log_G(Y) = log_M(Z)) and the client proof (πC: knowledge of the
+ blinding scalar b such that M = b·P) are verified with Schnorr
+ DLEQ reconstruction (`A1 = r·g1 + c·h1`, `A2 = r·g2 + c·h2`; check
+ that the recomputed challenge equals c). The engine is byte-
+ compatible with the production BRASS issuer at `api.scopeblind.com`
+ and the production client SDK: tokens issued in production verify
+ against this engine, and the engine rejects any tampered scalar,
+ wrong issuer key, scope mismatch, or AAD-bound πC tampering.
+- **`--allow-partial-voprf` flag** retained as a no-op for
+ compatibility with the `_partial` flag that existed during the
+ port. All VOPRF results in 0.5.0 are full verifications; the flag
+ is documented as deprecated and scheduled for removal in v0.6.0.
+- **Modular architecture.** cli.js is a thin dispatcher; verification
+ logic lives in `src/engines/*.js`. Each engine is independently
+ auditable and testable.
+- **Conformance tiers (T1-T5).** The verifier reports which tier of
+ conformance a receipt exercised: T1 basic (Ed25519 + JCS + chain),
+ T2 disclosure (AIP-0002), T3 attestation (hardware / anchor_uri),
+ T4 privacy (VOPRF + holder_binding), T5 full (ZK compliance, v1.0+).
+ Each verification surfaces the tier achieved.
+- **Knowledge Unit bundles.** First-class support for
+ draft-farley-acta-knowledge-units-00 multi-model deliberation
+ bundles. Reports topic, models, rounds, consensus level, dissent,
+ and verifies each embedded receipt.
+- **AIP-0002 selective disclosure.** `--disclose field:salt:value`
+ verifies salted SHA-256 commitments on redacted fields without
+ needing the issuer. Redacted fields are counted and surfaced.
+- **Sigil claim 2 — live-context verification (patent #5).**
+ `--require-context clock:±5s` / `geofence:...` / `sensor:temp<18`
+ evaluates predicates at verification time. The verifier aggregates
+ results and fails verification when any required predicate fails.
+- **Sigil commits to entire codebase.** Previously Sigil only
+ committed to cli.js. v0.5.0 extends commitment to all 15 source
+ files (cli.js + src/engines/* + src/output/* + src/util/* +
+ src/context/*). Modification of any file invalidates `--self-check`.
+- **JSON output.** `--json` emits structured results including
+ `tier`, `mode`, `algorithm`, `kid`, `key_source`, and
+ `sigil_fingerprint` for machine consumption.
+- **`--capabilities` command.** Lists supported modes, algorithms,
+ tiers, specs, and wayfinding. For CI integration and compatibility
+ discovery.
+- **Error code registry.** Every emitted error code is stable,
+ documented in ERRORS.md, and includes a spec section reference
+ where applicable.
+
+### Field recognition (surfaced in output, no verification required)
+
+- `disclosure_mode` enum
+- `holder_binding` object (modes: jwk_thumbprint, dpop,
+ attested_credential; per AIP-0003)
+- `annex_hash` (private annex commitment)
+- `attestation_mode` (software, hardware:secure_element, hardware:tee,
+ hardware:hsm)
+- `anchor_uri` (transparency log anchor URI)
+- Extended decision enum (challenge, payment_required, escalate,
+ override)
+- `nullifier` (VOPRF mode)
+- `scope` structure (origin, epoch, sub)
+- `compliance_credit_ref` (reserved for v1.0 ZK compliance proofs)
+- `transport_hint` (direct, ohttp, tor, custom)
+- `verifier_salt_kid` (VOPRF mode)
+
+### Hybrid post-quantum
+
+- Verifier detects `algorithm` values of the form `ed25519+ml-dsa-65`,
+ `ed25519+dilithium3`, etc., and emits a clear `unsupported_algorithm`
+ error. Full hybrid PQ verification is planned for v0.6+.
+
+### Security
+
+- Embedded-key rejection (from 0.4.0) retained. The deprecated
+ `--allow-embedded-key` escape hatch is still present in 0.5.0 but
+ will be removed in 0.6.0.
+- `--strict` mode disables all deprecated fallbacks.
+- Constant-time signature comparison preserved.
+
+### Spec alignment
+
+- Targets draft-farley-acta-signed-receipts-03 (Sigil self-check
+ output now references -03 explicitly).
+- References draft-farley-acta-knowledge-units-00 as the KU format.
+- References AIP-0001 (receipt format), AIP-0002 (selective
+ disclosure), AIP-0003 (holder binding).
+
+### Supply chain
+
+- Published with `npm publish --provenance` (Sigstore-anchored
+ supply chain attestation).
+- Dependency tree: `@veritasacta/artifacts` only. Transitive surface:
+ `@noble/curves`, `@noble/hashes`.
+
+### Documentation
+
+- THREAT-MODEL.md: formal threat model covering tamper detection,
+ replay, forgery, canonicalization, and the explicit non-goals.
+- SECURITY.md: disclosure policy and supported-version matrix.
+- ERRORS.md: complete error-code registry with spec references.
+- Expanded README with conformance tiers and usage examples for
+ every mode.
+
+## 0.4.0 — 2026-04-19 (embedded-key rejection)
+
+### Security
+
+- **Breaking change: embedded keys in receipt payloads are now
+ rejected by default.** A verification key transported inside the
+ signed payload does not provide authenticity against tampering
+ (see draft-farley-acta-signed-receipts-03 Security Considerations).
+- **New flag: `--allow-embedded-key`** (deprecated; removed in 0.5
+ or 0.6). Restores pre-0.4.0 behaviour for one release cycle.
+- Issue surfaced publicly by @desiorac on GetBindu PR #459.
+
+## 0.3.0 — 2026-04-05 (previous release)
+
+Offline receipt verification via `@veritasacta/verify` CLI.
diff --git a/ERRORS.md b/ERRORS.md
new file mode 100644
index 0000000..ada3876
--- /dev/null
+++ b/ERRORS.md
@@ -0,0 +1,105 @@
+# Error Code Registry
+
+Every error `@veritasacta/verify` can emit has a stable code,
+classification, exit code, and spec reference where applicable.
+Consumers should parse the `error` field (from `--json` output)
+rather than rely on human-readable text.
+
+## Classification
+
+| Class | Meaning | Exit code |
+|---|---|---|
+| `tampered` | Signature was tested and failed. Proof of modification. | 1 |
+| `undecidable` | Signature could not be tested (malformed, missing key, unsupported algorithm). | 2 |
+
+## Codes
+
+### Tampered (exit 1)
+
+#### `invalid_signature`
+- **Description:** Cryptographic signature verification failed over the canonical payload.
+- **Spec:** `draft-farley-acta-signed-receipts-03 §6.1`
+- **Hint:** The receipt has been modified, or was signed by a different key than the one provided.
+
+#### `chain_break`
+- **Description:** `previousReceiptHash` does not match the hash of the preceding receipt.
+- **Spec:** `draft-farley-acta-signed-receipts-03 §5.4 Chain Linkage`
+- **Hint:** A receipt has been inserted, removed, or reordered in the chain.
+
+#### `commitment_mismatch`
+- **Description:** Selective-disclosure commitment does not match the revealed salt+value.
+- **Spec:** `AIP-0002 §Disclosure Package Verification`
+- **Hint:** The disclosed value does not correspond to the committed hash.
+
+#### `dleq_verification_failed`
+- **Description:** VOPRF DLEQ proof verification failed (issuer or client proof invalid).
+- **Spec:** `draft-farley-acta-signed-receipts-03 §VOPRF Token Verification`
+- **Hint:** The VOPRF token was not produced by a valid issuer, or the proof is malformed.
+
+### Undecidable (exit 2)
+
+#### `embedded_key_rejected`
+- **Description:** Receipt contains a verification key in its payload, which is not trusted by default.
+- **Spec:** `draft-farley-acta-signed-receipts-03 §Security Considerations — Key Distribution`
+- **Hint:** Provide `--key`, `--jwks`, or `--trust-anchor` externally. The deprecated `--allow-embedded-key` restores pre-0.4.0 behaviour for one release cycle.
+
+#### `no_public_key`
+- **Description:** Verification key could not be resolved from `--key`, `--jwks`, or a bundle verification block.
+- **Hint:** Provide `--key `, `--jwks `, or `--trust-anchor `.
+
+#### `missing_signature`
+- **Description:** Input does not contain a `signature` field.
+
+#### `missing_payload`
+- **Description:** Input does not contain a `payload` field.
+
+#### `unsupported_algorithm`
+- **Description:** The declared signature algorithm is not supported by this verifier version.
+- **Hint:** Hybrid post-quantum algorithms like `ed25519+ml-dsa-65` require v0.6+ for full PQ verification.
+
+#### `non_ascii_key`
+- **Description:** An object key contains a non-ASCII character, violating AIP-0001.
+- **Spec:** `AIP-0001 §JCS Canonicalization`
+
+#### `malformed_json`
+- **Description:** Input could not be parsed as JSON.
+
+#### `malformed_hex`
+- **Description:** A hex-encoded value has odd length or contains invalid characters.
+
+#### `unknown_format`
+- **Description:** Input does not match any recognized receipt, token, or bundle format.
+- **Hint:** Valid formats: v1 receipt, v2 receipt, Passport envelope, audit bundle, KU bundle, VOPRF token.
+
+#### `jwks_fetch_failed`
+- **Description:** JWKS endpoint did not return a valid key set.
+
+#### `context_requirement_unmet`
+- **Description:** One or more `--require-context` predicates evaluated false at verification time.
+- **Spec:** Patent #5 claim 2 — Live-context verification
+
+#### `tier_not_achieved`
+- **Description:** Verification succeeded but did not achieve the tier required by `--tier`.
+
+## JSON output format
+
+When `--json` is used, errors appear as:
+
+```json
+{
+ "valid": false,
+ "error": "embedded_key_rejected",
+ "errorMeta": {
+ "code": "embedded_key_rejected",
+ "description": "Receipt contains a verification key in its payload, which is not trusted by default.",
+ "class": "undecidable",
+ "spec": "draft-farley-acta-signed-receipts-03 §Security Considerations — Key Distribution",
+ "hint": "Provide --key, --jwks, or --trust-anchor externally."
+ },
+ "format": "ed25519-passport",
+ "kid": "..."
+}
+```
+
+Consumers should branch on `error` code, not on `errorMeta.description`
+(which is informative but may evolve).
diff --git a/LICENSE b/LICENSE
index 11b1de5..de9ae21 100644
--- a/LICENSE
+++ b/LICENSE
@@ -4,178 +4,7 @@
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to the Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by the Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding any notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- Copyright 2026 ScopeBlind Pty Ltd (ABN 41693027440)
+ Copyright 2026 Tom Farley / Veritas Acta
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/PATENTS.md b/PATENTS.md
new file mode 100644
index 0000000..56a2e46
--- /dev/null
+++ b/PATENTS.md
@@ -0,0 +1,53 @@
+# Patent Notice
+
+This software is open source. You are free to use, modify, and distribute it
+under its stated license (MIT or Apache-2.0 — see each package's LICENSE file).
+
+## Patent Holdings
+
+ScopeBlind Pty Ltd (ABN 41 693 027 440) holds provisional patent applications
+covering specific methods used in conjunction with this software:
+
+1. **VOPRF Metering** (AU Provisional, ~October 2025) — Deterministic
+ credential derivation using Verifiable Oblivious Pseudorandom Functions
+ for privacy-preserving rate limiting and metering.
+
+2. **Verifier Nullifiers** (AU Provisional, ~October 2025) — Issuer-blind
+ verification scheme where the verifier confirms credential validity
+ without learning which organization issued it.
+
+3. **Offline Enforcement** (AU Provisional, ~October 2025) — Self-contained
+ policy enforcement with cryptographic receipts that are independently
+ verifiable without contacting the issuer.
+
+4. **Decision Receipts with Configurable Disclosure** (AU Provisional,
+ March 2026) — Signed decision artifacts with holder-bound zero-knowledge
+ compliance proofs, tool-calling gateway integration, agent manifests,
+ portable identity, and cross-algorithm key binding. 20 claims, 8 figures.
+
+## Scope
+
+These patents cover specific server-side issuance methods and the novel
+composition of VOPRF credentials with policy enforcement and receipt signing.
+They do NOT restrict your use of:
+
+- The open-source verification code (`@veritasacta/verify`)
+- The policy enforcement gateway (`protect-mcp`)
+- The Ed25519 signing primitives (`@veritasacta/artifacts`)
+- The receipt format specified in the IETF Internet-Draft
+- Standard cryptographic operations (Ed25519, JCS, SHA-256)
+
+## Apache-2.0 Patent Grant
+
+Packages licensed under Apache-2.0 include an explicit patent grant per
+Section 3 of the Apache License, Version 2.0. This grant covers the use
+of the software as distributed. The grant terminates only if you initiate
+patent litigation alleging that the software constitutes patent infringement
+(retaliation clause, Section 3).
+
+## Contact
+
+Patent inquiries: patents@scopeblind.com
+General: tommy@scopeblind.com
+Website: https://scopeblind.com
+IETF Draft: https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/
diff --git a/README.md b/README.md
index 50025dc..34da420 100644
--- a/README.md
+++ b/README.md
@@ -1,177 +1,322 @@
# @veritasacta/verify
-Verify signed receipts offline. No accounts, no API calls, no trust required.
-
-**Apache-2.0 · Ed25519 · Offline · Zero dependencies beyond `@noble/curves`**
-
----
-
-> **⚠️ 0.4.0 breaking change (coming this week):** The verifier now **rejects
-> keys transported inside the receipt payload** by default. This closes a
-> spec gap surfaced via an external review: a verification key embedded in
-> the signed body does not provide authenticity against tampering (an
-> attacker who can modify the payload can also substitute the key).
->
-> **Migration:** Pass the verification key externally via `--key `,
-> `--jwks `, or a configured trust anchor. Receipts that rely on
-> embedded keys will return exit code 2 (`embedded_key_rejected`).
->
-> A deprecated `--allow-embedded-key` flag restores pre-0.4.0 behavior for
-> one release cycle. It will be removed in 0.5.0.
->
-> Normative language for this change lands in
-> [draft-farley-acta-signed-receipts-02](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
-> Security Considerations (Key Distribution and Trust Anchors). A matching
-> negative conformance vector set lands in
-> [ScopeBlind/agent-governance-testvectors](https://github.com/ScopeBlind/agent-governance-testvectors).
-
----
+**Unified offline verifier for signed machine-decision artifacts. Network-effect mechanics built in.**
+
+Apache-2.0 · Ed25519 + VOPRF · Offline · Sigil-verified canonical release · Auto-onboarding · MCP proxy · Sidecar daemon
+
+> **Receipt format:** ScopeBlind emits Veritas Acta receipts. Legacy ScopeBlind
+> receipts remain verifiable, but Acta v0.1 is the canonical format going
+> forward. Spec: [`@veritasacta/protocol`](https://www.npmjs.com/package/@veritasacta/protocol)
+> · IETF: [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/).
```bash
-npm install @veritasacta/verify
-```
+# Install
+npm install -g @veritasacta/verify
+# Or
+brew install veritasacta/verify/veritasacta-verify
-Part of the [Veritas Acta](https://github.com/VeritasActa) project — open evidence protocol for machine decisions.
+# Prove canonical release
+npx @veritasacta/verify --self-check
-## Self-Test
+# Zero-config onboarding (auto-detects framework)
+npx @veritasacta/verify init
-```bash
-npx @veritasacta/verify --self-test
+# Verify any receipt format
+npx @veritasacta/verify receipt.json --key
```
-```
-✓ Sample receipt: VALID (Ed25519, kid: gateway-001)
-✓ Sample bundle: VALID (3/3 receipts verified)
-✓ Tampered receipt: REJECTED (signature mismatch)
-No ScopeBlind servers were contacted.
+Part of the [Veritas Acta](https://veritasacta.com) protocol for machine-decision evidence.
+
+## What it verifies
+
+| Mode | Input | Conformance tier |
+|---|---|---|
+| Ed25519 receipt | Signed decision receipts (v1, v2, Passport envelope) | T1 |
+| Ed25519 + AIP-0002 | Selective-disclosure receipts with `_commitments` | T2 |
+| Ed25519 + attestation | Receipts with `attestation_mode` or `anchor_uri` | T3 |
+| VOPRF token | Anonymous credential tokens (RFC 9497, BRASS wire format). Full Schnorr DLEQ verification for both πI (issuer) and πC (client). | T4 |
+| Knowledge Unit | Multi-model deliberation bundles (draft-farley-acta-knowledge-units-00) | varies |
+| Audit bundle | Multiple receipts with embedded signing keys | varies |
+
+## Subcommands
+
+The CLI is a dispatcher: one binary, eight modes.
+
+```bash
+verify # verify a single file (default)
+verify init # zero-config onboarding, auto-detects framework
+verify proxy --target "..." # transparent MCP proxy, signs every tool call
+verify daemon # unix-socket sidecar, language-agnostic signing API
+verify prompt # verify provenance of a CLAUDE.md / SKILL.md / system prompt
+verify chain explore # walk a receipt chain to its root, validate every hash link
+verify --replay-chain ... # bulk verification with chain-linkage check
+verify --self-check # prove this binary is the canonical release
+verify --attest # emit a shareable canonical attestation
```
-Exit codes:
-- `0` — all signatures valid
-- `1` — tampered or invalid signature
-- `2` — malformed input
+### Prompt provenance
-## What It Does
+Closes the supply-chain vector where an attacker modifies `CLAUDE.md`, `SKILLS.md`, or a system prompt between authoring time and agent runtime.
-Every time an AI agent makes a decision through [protect-mcp](https://www.npmjs.com/package/protect-mcp), that decision is signed with Ed25519. The signature covers:
+```bash
+# Against a Veritas Acta receipt asserting the prompt hash
+verify prompt SKILL.md --prompt-receipt prompt-receipt.json
-- **Tool name** — what was called
-- **Decision** — allow, deny, rate_limited
-- **Policy digest** — which policy version governed the decision
-- **Timestamp** — when it happened
-- **Payload digest** — SHA-256 of the full input/output
+# Against a Sigstore bundle (DSSE + in-toto statement)
+verify prompt CLAUDE.md --sigstore-bundle bundle.json
-`@veritasacta/verify` checks these signatures. It works offline, requires no accounts, and never contacts any server.
+# Fast path: caller knows the expected hash
+verify prompt SKILL.md --expected-hash
+```
-## Usage
+### Chain exploration
-### Verify a single receipt
+Walks the `previousReceiptHash` chain from a chain tip back to its root, validating every link's SHA-256.
```bash
-npx @veritasacta/verify receipt.json
+verify chain explore ./receipts/tip.json
+# → ASCII tree, depth, links_broken, warnings
+
+verify chain explore ./receipts/tip.json --search-dir ./audit/ --max-depth 200 --json
```
-### Verify with an explicit public key
+### Pre-built sandbox profiles
+
+`ecosystem/profiles/` ships sandboxing profiles (Cedar policy + nono capabilities + README) for common agent runtimes — Claude Code, Cursor, Codex, Gemini CLI, OpenClaw. Compose with `sb-runtime --ring 3 --policy ./policy.cedar` + `nono run --caps ./nono-capabilities.yaml`.
+
+## Verification properties
+
+- **Offline.** No network contacted unless `--jwks ` is explicitly passed.
+- **Tamper-evident.** Exit 1 is proven tampering; exit 2 is undecidable (malformed, missing key, unsupported algorithm).
+- **No vendor trust.** Only Ed25519 (RFC 8032) and JCS (RFC 8785) in the verification path.
+- **Self-verifying.** `--self-check` cryptographically proves the installed verifier (24 source files) matches the canonical release.
+- **Algorithm-agile.** Hybrid PQ (`ed25519+ml-dsa-65`) recognized; full PQ in v0.6+.
+- **Zero telemetry.** The verifier never phones home.
+
+## Quick start: frictionless onboarding
```bash
-npx @veritasacta/verify receipt.json --key
+$ cd my-agent-project
+$ npx @veritasacta/verify init
+
+[Sigil ASCII art]
+ sigil: 956f2e88
+
+✓ Veritas Acta initialized
+ Directory: ./.veritasacta
+ Kid: project:956f2e8895fd
+ Framework: crewai (python)
+
+Next steps:
+ Install: pip install veritasacta-crewai
+ Wrap your agent with the adapter as shown in the adapter README.
+
+Verify:
+ npx @veritasacta/verify .veritasacta/receipts/*.json --key 956f2e88...
```
-### Verify an audit bundle
+Init auto-detects your framework from `package.json` / `pyproject.toml` / `requirements.txt` across 13 supported frameworks (Claude Code, Claude Agent SDK, Google ADK, CrewAI, Pydantic AI, AutoGen, Smolagents, LangChain JS/Python, LangGraph JS/Python, OpenAI Agents SDK, Vercel AI SDK).
+
+## Universal MCP proxy — zero code changes
```bash
-npx @veritasacta/verify bundle.json
+$ verify proxy --target "node my-mcp-server.js"
+[veritasacta proxy] rcpt_1 signed (web_search) kid=project:956f2e8895fd
+[veritasacta proxy] rcpt_2 signed (read_file) kid=project:956f2e8895fd
+...
```
-Bundles are self-contained: they include receipts + the public key used to sign them.
+Wraps any MCP server with signing. No changes in the server. No changes in the agent. Every `tools/call` gets a chain-linked Ed25519 receipt.
-### Programmatic API
+## Sidecar daemon — language-agnostic signing
-```javascript
-import { verifyReceipt, verifyBundle } from '@veritasacta/verify';
+Run once; any process in the same user context signs receipts by POST.
-const result = await verifyReceipt(receiptJson, publicKeyHex);
-// { valid: true, algorithm: 'Ed25519', kid: 'gateway-001' }
+```bash
+$ verify daemon &
-const bundleResult = await verifyBundle(bundleJson);
-// { valid: true, receipts: 47, verified: 47, failed: 0 }
+# Any language, any process:
+$ curl --unix-socket /tmp/veritasacta-$UID.sock -X POST http://_/sign \
+ -d '{"tool":"web_search","args":{"q":"..."},"decision":"allow"}'
+
+{ "payload": {...}, "signature": {"alg":"EdDSA","kid":"...","sig":"..."} }
```
-## Receipt Format
+One daemon, N agents, zero SDK embedding.
+
+## Canonical attestation — network-effect mechanics
-Receipts follow the [IETF Internet-Draft](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) `draft-farley-acta-signed-receipts-01`:
+Every user who runs `--self-check` can emit a **canonical attestation** — a signed JSON artifact proving they ran the canonical unmodified verifier. Publish wherever (GitHub README, status page, SBOM, Rekor).
+
+```bash
+$ verify --attest --attest-org "Acme Corp" --output attestation.json
+```
+
+Output:
```json
{
- "type": "decision",
- "version": 2,
- "algorithm": "Ed25519",
- "kid": "gateway-001",
- "timestamp": "2026-04-10T12:00:00Z",
"payload": {
- "tool": "read_file",
- "decision": "allow",
- "policy_digest": "sha256:a1b2c3..."
+ "type": "veritasacta:verifier-attestation",
+ "sigil_fingerprint": "6391ae72",
+ "sigil_name": "Quiet Orchard",
+ "canonical": true,
+ "attester_org": "Acme Corp",
+ "issued_at": "2026-04-19T...",
+ "expires_at": "2026-04-26T...",
+ "attester_kid": "attester:..."
},
- "signature": "base64url-encoded-ed25519-signature"
+ "signature": { "alg": "EdDSA", ... },
+ "verification": { "attester_pubkey": "..." }
}
```
-The signature is computed over the [JCS-canonicalized](https://www.rfc-editor.org/rfc/rfc8785) payload, ensuring deterministic verification regardless of JSON key ordering.
+Offline. User-signed. Counterfeit forks produce attestations marked `canonical: false` — detectable across the network.
-## How Verification Works
+## Verification receipts
+```bash
+$ verify receipt.json --key --emit-verification-receipt
```
-Receipt JSON
- ↓
-Extract payload + signature
- ↓
-JCS-canonicalize the payload
- ↓
-Ed25519.verify(signature, canonical_payload, public_key)
- ↓
-exit 0 (valid) or exit 1 (tampered)
+
+Produces a signed "the canonical verifier checked this receipt and it was valid" artifact. Anchor in Sigstore Rekor, publish in SBOMs, attach to compliance reports.
+
+## Enterprise features
+
+| Flag | Purpose |
+|---|---|
+| `--pin-sigil ` | Require the installed Sigil fingerprint to match (supply-chain enforcement) |
+| `--audit-log ` | Append every verification event to a chain-hashed JSONL log |
+| `--audit-report` | Render an HTML audit report (self-contained, auditor-ready) |
+| `--fips` | Enforce FIPS-approved algorithms only |
+| `--strict` | Disable all deprecated fallbacks |
+| `--tier N` | Require minimum conformance tier (1-5) |
+| `--replay-chain ` | Bulk-verify a JSONL chain with parallel workers |
+| `--diff ` | Structural diff between two receipts |
+
+## Live-context verification (Sigil claim 2)
+
+```bash
+$ verify receipt.json \
+ --require-context clock:±5s \
+ --require-context sensor:temp<18
```
-No network calls. No trust assumptions. The only input is the receipt and the public key.
+Gates verification on live context (NTP, sensors, feeds). Predicate fails → verification fails. Operationalizes patent #5 claim 2.
-## Issuer-Blind Design
+## Algorithms supported
-The verifier never learns *who* generated the receipt — only that it was signed by a key matching the provided public key. This is a deliberate design property:
+- `Ed25519` / `EdDSA` (RFC 8032)
+- `voprf-p256-sha256` (RFC 9497, structural; full DLEQ extraction in progress)
+- Hybrid PQ recognized: `ed25519+ml-dsa-65`, `ed25519+dilithium3` (v0.6+)
-- **Compliance teams** can verify agent behavior without accessing the agent runtime
-- **Third-party auditors** can check receipts without org access
-- **Cross-organization verification** works without federation or shared infrastructure
+## Conformance tiers
-## Security
+| Tier | Requirements |
+|---|---|
+| T1 Basic | Ed25519 + JCS + chain linkage |
+| T2 Disclosure | T1 + AIP-0002 selective disclosure |
+| T3 Attestation | T2 + `attestation_mode` + `anchor_uri` |
+| T4 Privacy | T3 + VOPRF + `holder_binding` |
+| T5 Full | T4 + ZK compliance proofs (v1.0+) |
-- Ed25519 signatures via [`@noble/curves`](https://github.com/paulmillr/noble-curves) (audited by Trail of Bits)
-- SHA-256 hashing via [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) (audited)
-- JCS canonicalization for deterministic payload serialization
-- No eval, no dynamic imports, no network access during verification
+Each verification surfaces the tier achieved. Implementations earn tier badges for their READMEs.
-## Related Projects
+## Framework adapters
-| Project | Description |
-|---------|-------------|
-| [VeritasActa/Acta](https://github.com/VeritasActa/Acta) | Open evidence protocol — charter, spec, types |
-| [protect-mcp](https://www.npmjs.com/package/protect-mcp) | Security gateway that produces signed receipts |
-| [ScopeBlind/verify-mcp](https://github.com/ScopeBlind/verify-mcp) | MCP server exposing verification as agent tools |
-| [VeritasActa/drafts](https://github.com/VeritasActa/drafts) | IETF Internet-Draft source files |
-| [@veritasacta/protocol](https://www.npmjs.com/package/@veritasacta/protocol) | Full evidence protocol types (TypeScript) |
+| Framework | Package | Language |
+|---|---|---|
+| Claude Code (MCP hooks) | `protect-mcp` | JS |
+| Google ADK | `protect-mcp-adk` | Python |
+| LangChain | `@veritasacta/langchain` / `veritasacta-langchain` | JS / Python |
+| LangGraph | `@veritasacta/langgraph` / `veritasacta-langgraph` | JS / Python |
+| CrewAI | `veritasacta-crewai` | Python |
+| Pydantic AI | `veritasacta-pydantic-ai` | Python |
+| AutoGen | `veritasacta-autogen` | Python |
+| Smolagents | `veritasacta-smolagents` | Python |
+| OpenAI Agents SDK | `@veritasacta/openai-agents` | JS / Python |
+| Vercel AI SDK | `@veritasacta/vercel-ai` | JS |
+| Any MCP server | `verify proxy --target ""` | language-agnostic |
+| Anything else | `verify daemon` + HTTP POST | language-agnostic |
-## Protocol Specification
+## SDK
-The receipt format is standardized as an IETF Internet-Draft:
+Tiny language-agnostic signing helpers for custom integrations:
-**[draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)** — Signed Decision Receipts for Machine-to-Machine Access Control
+```bash
+npm install @veritasacta/sdk
+pip install veritasacta-sdk
+```
-## License
+```js
+import { Signer } from '@veritasacta/sdk';
+const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+const receipt = signer.signDecision({ tool: 'x', args: {}, decision: 'allow' });
+```
+
+## Release names (Sigil brand convention)
+
+Every release gets a unique deterministic name from its cryptographic fingerprint. Current release: **Quiet Orchard** (`6391ae72`). Full registry at [veritasacta.com/sigils](https://veritasacta.com/sigils). See [ecosystem/RELEASE-NAMING.md](./ecosystem/RELEASE-NAMING.md) for the derivation.
+
+## Ecosystem artifacts
+
+The `ecosystem/` directory ships:
+
+- **GitHub Action** (`ecosystem/github-action/`) — drop-in CI step
+- **Claude Code plugin** (`ecosystem/claude-code-plugin/`) — one-click Claude Code install
+- **Homebrew tap** (`ecosystem/homebrew-tap/`) — `brew install veritasacta-verify`
+- **Registry worker** (`ecosystem/registry-worker/`) — public implementations registry (`registry.veritasacta.com`)
+- **Badge worker** (`ecosystem/badge-worker/`) — shields.io-compatible badges (`verify.veritasacta.com/badge/*`)
+- **Interop leaderboard** (`ecosystem/interop-leaderboard/`) — weekly cross-implementation CI
+- **Language SDKs** (`ecosystem/sdk-js/`, `ecosystem/sdk-py/`) — tiny signing helpers
+- **Framework adapters** (`ecosystem/adapters/*`) — LangChain, CrewAI, OpenAI Agents, Vercel AI, Smolagents, Pydantic AI, AutoGen, LangGraph
+- **Design docs** (`ecosystem/rollback/`, `ecosystem/supervisor/`, `ecosystem/reputation/`, `ecosystem/dashboard/`, `ecosystem/browser-extension/`, `ecosystem/ebpf-observer/`, `ecosystem/cosign-compat/`, `ecosystem/CONFORMANCE-CERTIFICATION.md`)
-Apache-2.0 — see [LICENSE](./LICENSE).
+See [`ecosystem/README.md`](./ecosystem/README.md) for the full map.
-Free to use, modify, and redistribute. The Apache-2.0 license includes a royalty-free patent grant (Section 3) for all constructions implemented in this code.
+## Relationship to the Veritas Acta stack
+
+- **Protocol:** [veritasacta.com](https://veritasacta.com) — open IETF drafts, AIP specs, Apache-2.0.
+- **Verifier:** this package. Open, offline, fully user-controlled.
+- **Managed issuance (commercial):** [scopeblind.com](https://scopeblind.com) — managed receipt infrastructure + VOPRF issuance API.
+
+Open verifier + closed issuer. The verifier is always free. The commercial product is the managed service.
+
+## Supply chain
+
+v0.5.0 is published with:
+
+- `npm publish --provenance` — Sigstore-attested supply chain
+- Sigil commitment covering 24 source files
+- Minimum dependency tree: only `@veritasacta/artifacts` (+ transitively `@noble/curves`, `@noble/hashes`)
+
+Verify your installation:
+
+```bash
+npm audit signatures # Sigstore attestation
+verify --self-check # matches canonical Sigil
+verify --pin-sigil # enforce a specific release
+```
+
+## Specifications
+
+- [draft-farley-acta-signed-receipts-03](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
+- [draft-farley-acta-knowledge-units-00](https://datatracker.ietf.org/doc/draft-farley-acta-knowledge-units/)
+- AIP-0001 (receipt format + ASCII-only JCS)
+- AIP-0002 (selective disclosure)
+- AIP-0003 (holder binding)
+- RFC 8032, 8785, 9497, 9380, 7517, 7638
+
+## Documentation
+
+- [CHANGELOG.md](./CHANGELOG.md) — release history
+- [THREAT-MODEL.md](./THREAT-MODEL.md) — what the verifier protects against and what it doesn't
+- [SECURITY.md](./SECURITY.md) — disclosure policy + supported versions
+- [ERRORS.md](./ERRORS.md) — complete error-code registry
+- [ecosystem/RELEASE-NAMING.md](./ecosystem/RELEASE-NAMING.md) — Sigil naming convention
+
+## License
----
+Apache-2.0.
-> **Looking for the VOPRF/BRASS rate-limiting library?** The anonymous credential primitives have moved to a [separate repository](https://github.com/VeritasActa). This package (`@veritasacta/verify`) is now the offline receipt verification CLI.
+Patent-adjacent; covered by the Apache-2.0 patent grant (§3). See [PATENTS.md](./PATENTS.md).
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..37224df
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,113 @@
+# Roadmap
+
+## v0.5.4 (shipping — 2026-04-20)
+
+- AIP-0007 ZK compliance proofs (spec draft).
+- Rekor / transparency-log anchoring engine (AIP-0005 T4).
+- Hardware-attestation quote validator (ATECC608B + Apple SE full crypto; TPM2/SGX/SEV structural).
+- Receipt watcher with Slack / Discord / generic webhooks.
+- SBOM audit bundle builder (SPDX + CycloneDX).
+- Four-profile transparency switch (private / auditable / transparent / high-assurance).
+- `@veritasacta/cross-verify` arbitrator package.
+- Conformance certification program workflow.
+- Canonical release: Open Wind (677a8a81). Sigil covers 36 source files.
+
+## v0.5.3 (shipping — 2026-04-20)
+
+- **AIP-0006**: delegation chains with narrowing-only scope, TTL, max_depth, cryptographic chain to trust anchor.
+- **Bilateral cosign**: envelope-level `cosignatures[]` field. Proxy supports `--bilateral --server-key`.
+- **trace_id + parent_receipt_id**: optional AIP-0001 fields for workflow-level grouping.
+- **Proxy --scrub-secrets**: redacts probable-secret values from outgoing tool-call args + flags on receipt.
+- **`verify dashboard`**: loopback-only local audit server; DNS-rebinding + path-traversal defenses.
+- 41 new unit tests. Sigil covers 31 source files.
+
+## v0.5.2 (shipping — 2026-04-20)
+
+- **Compliance export subcommand**: SOC 2 / ISO 42001 / EU AI Act control mapping with HTML auditor report.
+- **DSSE envelope engine**: Sigstore-compatible wrap/unwrap/verify for receipts, KUs, and in-toto statements.
+- **BRASS v2 crypto scaffold**: length-prefixed hashing, nullifier-bound-to-Y, single-variable πC restatement.
+- **AIP-0004** (Draft): content-addressed snapshot and rollback receipts, with reference Merkle helper.
+- **AIP-0005** (Draft): attestation weight profile (cost_tier T0–T4) with evidence schemas.
+- **Claude Code plugin staging** for wshobson/agents marketplace (`protect-mcp` tree, closes #471).
+- **Audit dashboard scaffold**: local-first, in-browser receipt visualiser.
+- **Physical attestation design**: Seal cost_tier T2 integration spec.
+- Sigil covers 28 source files. Canonical release: **New Ember** (`b28f8d60`).
+
+## v0.5.1 (shipped — 2026-04-20)
+
+- **Prompt provenance**: `verify prompt ` verifies a CLAUDE.md / SKILL.md / AGENTS.md / system prompt against a Veritas Acta receipt, a Sigstore DSSE bundle, or an expected SHA-256.
+- **Chain explorer**: `verify chain explore ` walks `previousReceiptHash` to root with link-by-link hash validation; ASCII tree and `--json` output.
+- **Sandbox profiles**: `ecosystem/profiles/` — Cedar policy + nono capabilities + README for Claude Code, Cursor, Codex, Gemini CLI, OpenClaw.
+- Sigil commitment grows to **25 source files** (adds prompt.js + chain-explore.js). Canonical release: **Bright Star** (`1cc829ab`).
+
+## v0.5.0 (shipped — 2026-04-19)
+
+- Unified verifier: Ed25519 receipts, VOPRF tokens (full dual-DLEQ), Knowledge Unit bundles, selective-disclosure receipts
+- Sigil visual commitment covering 23 source files (since expanded in 0.5.1)
+- `--attest` canonical attestations, `--emit-verification-receipt`
+- `--pin-sigil`, `--audit-log`, `--audit-report`, `--replay-chain`, `--diff`
+- `--fips` enforcement; `--allow-embedded-key` deprecated
+- Subcommands: `init`, `proxy`, `daemon`, `--self-check`, `--capabilities`
+- Network-effect mechanics (attestations, pinning, receipts-of-receipts)
+
+## v0.6.0 (planned)
+
+Scheduled items, each tracked as a separate implementation task.
+
+### Cryptographic hardening (BRASS protocol)
+
+| Item | Motivation | Breaking? |
+|---|---|---|
+| Switch BRASS hashing to length-prefixed (RFC 8785 JCS-aligned) | Eliminates variable-length concat collision surface in `piC` bind (AADr and KID are variable-length inputs). | Yes. Dual-mode verifier accepts both v1 and v2 wire formats during the deprecation window. |
+| Bind nullifier derivation to issuer public key `Y` | Prevents cross-issuer nullifier collisions if two issuers ever share a KID string. | Yes, but the dual-mode derivation supports both and migration is operator-paced. |
+| Restate πC as a standard single-variable Schnorr proof (wire unchanged) | The production BRASS scheme uses a DLEQ structure with `A2 = G` hardcoded. This is cryptographically equivalent to a single-variable Schnorr but reads as degenerate to external auditors. Restating clarifies without changing the wire. | No. |
+| Formal verification of the BRASS scheme via Tamarin or ProVerif | Procurement-audit credibility; catches subtle issues the paper analysis misses. | No. |
+| Apache-2.0 reference issuer | Patent conversion often benefits from a complete open specification of the invention, even when the commercial operation is proprietary. | No. Commercial differentiation via managed operations, SLAs, rate limits, key rotation, etc. Algorithm goes public. |
+
+### Post-quantum hybrid
+
+- Optional `ed25519+ml-dsa-65` hybrid signature mode on Ed25519 receipts. v0.5.0 recognizes the algorithm identifier and emits a clean `unsupported_algorithm` error. v0.6.0 ships the verifier side via a library like `@noble/post-quantum`.
+- Expected scope: ~200 lines of code + spec additions + conformance vectors.
+
+### VOPRF enhancements
+
+- Drop the `--allow-partial-voprf` flag (no-op since 0.5.0 shipped full DLEQ).
+- `scope` parameter refinement: support richer scope matching than `origin` equality (e.g., origin wildcards, epoch bounds).
+- Per-issuer policy cache: avoid re-verifying πI for tokens known to be from a trusted-fingerprint issuer.
+
+### Reporting and audit
+
+- `--audit-report` template extensibility: custom CSS, operator logo embedding, export to PDF via headless Chrome.
+- SBOM attachment: embed SPDX or CycloneDX alongside the verified receipt chain.
+
+### Spec tracking
+
+- Align with draft-farley-acta-signed-receipts-03 as that draft progresses through the IETF.
+- Publish BRASS v2 as a separate draft (`draft-farley-brass-anonymous-tokens`).
+
+## v0.7.0 / v1.0 (exploratory)
+
+| Item | Notes |
+|---|---|
+| Sigstore / DSSE wrapping | `ecosystem/cosign-compat/DESIGN.md` outlines the wrap. Rekor anchoring for temporal proof. |
+| Filesystem rollback helper | `ecosystem/rollback/DESIGN.md`. Content-addressed snapshots for post-incident recovery. |
+| Runtime supervisor | `ecosystem/supervisor/DESIGN.md`. Dynamic permission expansion with approval gates. |
+| Issuer reputation | `ecosystem/reputation/DESIGN.md`. Bayesian reputation over receipt issuers, complement to agent reputation. |
+| Audit dashboard GUI | `ecosystem/dashboard/DESIGN.md`. Web dashboard for operator audits. |
+| Browser extension | `ecosystem/browser-extension/DESIGN.md`. Consumer reach via Claude.ai / ChatGPT injection. |
+| eBPF OS observer | `ecosystem/ebpf-observer/DESIGN.md`. Kernel-level auto-instrumentation. |
+
+## Deprecations
+
+| Item | Deprecated in | Removed in |
+|---|---|---|
+| `--allow-embedded-key` | 0.4.0 | 0.6.0 |
+| `--allow-partial-voprf` | 0.5.0 | 0.6.0 |
+| `voprf-p256-sha256` v1 wire format (plain-concat hash) | 0.6.0 (dual-mode) | 0.7.0 or later |
+
+## Commitments
+
+- v0.6.0 targets Q3 2026.
+- All breaking changes ship with a dual-mode transition window of at least one minor release.
+- Every new wire-format change is accompanied by conformance vectors in [ScopeBlind/agent-governance-testvectors](https://github.com/ScopeBlind/agent-governance-testvectors).
+- `--self-check` continues to prove source integrity across releases; Sigils rotate per release with the registry maintained at `ecosystem/RELEASE-NAMING.md`.
diff --git a/SECURITY.md b/SECURITY.md
index 274f45b..2b924d8 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,53 +1,97 @@
# Security Policy
+## Supported Versions
+
+| Version | Status | Support through |
+|---------|----------------|-----------------|
+| 0.5.x | Current | Next major |
+| 0.4.x | Security only | 2026-10-19 |
+| 0.3.x | End of life | ended 2026-04-19|
+| < 0.3 | End of life | — |
+
+Receipts verified with an EOL version should be re-verified with a
+current version to confirm continued validity.
+
## Reporting a Vulnerability
-If you discover a security vulnerability in `@veritasacta/verify`, please report it responsibly.
+If you believe you have found a security vulnerability in
+`@veritasacta/verify`, please report it privately.
+
+**Email:** security@scopeblind.com
-**Email:** [security@veritasacta.com](mailto:security@veritasacta.com)
+**Response time:**
+- Acknowledgment within 48 hours
+- Initial assessment within 5 business days
+- Coordinated disclosure target: 90 days (shorter for actively
+ exploited issues, longer for complex issues requiring upstream fixes)
-Please include:
-- Description of the vulnerability
-- Steps to reproduce
-- Potential impact assessment
-- Suggested fix (if any)
+**What to include:**
+- Version affected
+- A clear description of the issue
+- Reproduction steps (if applicable)
+- Suggested remediation (if you have one)
+- Whether you intend to publish (we can coordinate disclosure timing)
-We will acknowledge receipt within 48 hours and provide an initial assessment within 7 days.
+**What we will do:**
+- Acknowledge your report
+- Assess severity and impact
+- Develop and test a fix
+- Coordinate disclosure with you
+- Credit you in the release notes unless you prefer otherwise
## Scope
-This policy covers:
-- The `@veritasacta/verify` npm package
-- All code in the [VeritasActa/verify](https://github.com/VeritasActa/verify) repository
+### In scope
-## Cryptographic Dependencies
+- Cryptographic verification correctness bugs in the verifier
+- Canonicalization divergence from RFC 8785 / AIP-0001
+- Supply chain risks in the published package
+- Side-channel attacks against the verification path (e.g., timing)
+- Algorithm downgrade or silent fallback behavior
+- Self-check bypass (Sigil commitment verification)
-This library delegates all elliptic curve and hashing operations to:
-- **[@noble/curves](https://github.com/paulmillr/noble-curves)** — audited by Cure53 (Feb 2024)
-- **[@noble/hashes](https://github.com/paulmillr/noble-hashes)** — audited by Cure53 (Feb 2024)
+### Out of scope
-We do not implement custom cryptographic primitives. If you find a vulnerability in the noble libraries, please report it to their maintainer directly.
+- Bugs in dependencies (report to upstream: @noble/curves, @noble/hashes)
+- Threat-model non-goals documented in THREAT-MODEL.md (e.g.,
+ compromised signing keys, issuer collusion, policy semantics)
+- Usage errors unrelated to verification correctness
+- Social engineering against the ScopeBlind team
-## Security Properties
+## Coordinated Disclosure Examples
-The BRASS protocol provides the following security guarantees:
+- **Embedded-key acceptance (fixed in 0.4.0):** surfaced publicly by
+ @desiorac on GetBindu PR #459 before reaching us privately. We
+ accept that publication of the issue on a third-party project was
+ legitimate; we responded with 0.4.0 within one week.
+- We prefer private disclosure, but we will not penalize researchers
+ who choose to disclose publicly; our goal is correct verification,
+ not reporter punishment.
-| Property | Guarantee |
-|----------|-----------|
-| **Issuer blindness** | The issuer cannot determine which scope a token will be redeemed against |
-| **Nullifier determinism** | Same token + same scope always produces the same nullifier |
-| **Unlinkability** | Different scopes produce unrelated nullifiers from the same token |
-| **Proof soundness** | DLEQ proofs are computationally binding under the discrete log assumption on P-256 |
-| **Offline verification** | The issuer is never contacted during token redemption |
+## Hall of Fame
-## Known Limitations
+Security researchers who have helped improve @veritasacta/verify:
-- **MemoryStore** is not suitable for distributed deployments (no cross-process synchronization)
-- **KVStore** (Cloudflare KV) is eventually consistent — overspend is bounded but possible during replication lag
-- This library implements the **verifier side** only. It does not issue tokens.
+- @desiorac — embedded-key rejection (surfaced on GetBindu #459,
+ landed in 0.4.0)
-## Supported Versions
+## Supply Chain
+
+Each release is published with:
+
+- `npm publish --provenance` — Sigstore-attested supply chain
+- Sigil commitment in `sigil.json` covering all source files
+- GPG-signed git tag (when the release workflow runs)
+
+Verify the integrity of your installation:
+
+```bash
+# Verify npm provenance
+npm audit signatures
+
+# Verify local files match the Sigil
+npx @veritasacta/verify --self-check
+```
-| Version | Supported |
-|---------|-----------|
-| 0.1.x | Yes |
+Cross-check the expected Sigil fingerprint against the canonical
+release published on https://veritasacta.com.
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
new file mode 100644
index 0000000..b2de036
--- /dev/null
+++ b/THREAT-MODEL.md
@@ -0,0 +1,219 @@
+# Threat Model
+
+`@veritasacta/verify` is a local, offline verifier for signed
+machine-decision artifacts. This document specifies what it protects
+against, what it does not, and the trust boundaries a caller must
+understand.
+
+## 1. Goals (in scope)
+
+The verifier provides, for any artifact it reports as VALID:
+
+- **Signature integrity**: the signed payload has not been modified since
+ the signer produced it. An adversary with access to the artifact but
+ not the signing key cannot produce a VALID result by tampering.
+- **Policy binding** (for receipts that include `policy_hash` /
+ `policy_id`): the decision was made under the identified policy
+ version, not a later-substituted policy.
+- **Chain integrity** (for chained receipts): no receipt in the chain
+ has been inserted, removed, or reordered; every `previousReceiptHash`
+ matches the canonical hash of the preceding receipt.
+- **Algorithm conformance**: the signature algorithm is explicitly
+ supported; unrecognized algorithms are reported as `undecidable`
+ rather than silently accepted.
+- **Canonicalization determinism**: input that re-encodes into the same
+ canonical bytes produces the same verification result byte-for-byte.
+ Canonicalization follows RFC 8785 with AIP-0001 ASCII-only keys.
+- **Selective-disclosure validity** (for receipts with `_commitments`):
+ a revealed `{field, salt, value}` triple matches the committed hash
+ if and only if the field content is authentic.
+- **VOPRF DLEQ proof verification** (when full extraction lands):
+ tokens are proven issued by the claimed issuer key.
+- **Canonical-release provenance** (`--self-check`): the installed
+ verifier binary + engine files are the canonical unmodified release
+ committed by the Sigil.
+- **Offline operation**: verification uses no network calls unless the
+ caller explicitly passes `--jwks`.
+
+## 2. Non-Goals (out of scope)
+
+The verifier does NOT protect against:
+
+- **Compromised signing keys**: if the signer's private key leaks, an
+ adversary with the key can produce VALID receipts. The verifier
+ cannot distinguish these from legitimate receipts.
+- **Issuer collusion**: if an issuer signs receipts for actions that
+ never occurred, those receipts are cryptographically valid. Detecting
+ such fraud requires separate mechanisms (transparency logs, independent
+ attestation, external audit).
+- **Policy semantics**: the verifier confirms a receipt references a
+ `policy_id` / `policy_hash`, but does NOT interpret the policy or
+ evaluate whether the decision was correct given the inputs. Policy
+ interpretation is the caller's responsibility.
+- **Replay across contexts**: a receipt valid in one context (e.g., a
+ specific session) may be replayed in another. Preventing replay is
+ the application layer's responsibility; the verifier surfaces
+ `nullifier`, `scope`, and timestamp fields that enable the caller to
+ implement replay guards.
+- **Time authority**: `issued_at` is self-asserted by the signer. The
+ verifier does not independently time-stamp. Applications requiring a
+ trusted timestamp must combine signed receipts with transparency-log
+ inclusion proofs or an external timestamping authority.
+- **Holder possession**: when `holder_binding` is present in a receipt,
+ the verifier surfaces the binding but does NOT prove the presenting
+ party is the committed holder. Possession proof requires an additional
+ mechanism (DPoP header at presentation, signed nonce, etc.) at the
+ caller layer.
+- **Receipt availability / censorship**: the verifier only evaluates
+ artifacts it is given. It cannot detect missing receipts, gaps in a
+ chain held elsewhere, or receipts that were never surfaced.
+- **Input size / resource exhaustion**: the verifier imposes reasonable
+ defaults on input size, but callers accepting untrusted input at scale
+ should apply their own limits before invoking the verifier.
+
+## 3. Trust Boundaries
+
+For a verification to be trustworthy, the following must hold:
+
+### 3.1 The caller must authentically obtain the verification key
+
+The verifier takes a public key as input (via `--key`, `--jwks`, or
+`--trust-anchor`). The authenticity of that key is the caller's
+responsibility. The verifier does NOT accept keys embedded in the
+receipt payload itself (this was the `embedded_key_rejected` issue
+fixed in 0.4.0).
+
+Recommended key distribution mechanisms:
+
+- **JWKS at a well-known URL** with TLS-authenticated domain ownership
+- **DID Document** with method-specific authentication
+- **Configured trust anchor** (locally pinned public key)
+- **Transparency log entry** with inclusion proof
+
+### 3.2 The caller must provide an authentic runtime environment
+
+The verifier executes in Node.js. A compromised Node.js runtime or a
+compromised installation of the verifier can produce arbitrary output.
+The `--self-check` command is the caller's tool for proving the
+installed verifier is the canonical release; however, `--self-check`
+itself runs in the same potentially-compromised environment and is
+therefore subject to the same caveat. Callers operating in
+high-assurance environments should reproduce the verification from
+source, in a separate trust domain.
+
+### 3.3 The caller must authentically obtain the verifier
+
+The npm package ships with a Sigil commitment that binds the published
+release to specific file hashes. `--self-check` confirms installed
+files match the Sigil commitment. The authenticity of the Sigil is
+anchored in the npm publication (with `--provenance` attestation via
+Sigstore). Callers verifying an installation should:
+
+1. Install via `npm install @veritasacta/verify` (Sigstore-attested)
+2. Run `npx @veritasacta/verify --self-check` to confirm local files
+ match the Sigil
+3. For high-assurance use, cross-check the installed Sigil fingerprint
+ against the fingerprint published on https://veritasacta.com
+
+## 4. Explicit Attack Classes
+
+### 4.1 Signature forgery
+
+Forgery of an Ed25519 signature without possession of the private key
+requires breaking Ed25519 (equivalent to ~128-bit security against
+classical attackers; reduced under post-quantum attack but still
+requires significant work).
+
+### 4.2 Hash collision
+
+Forgery via SHA-256 collision requires ~2^128 work; outside practical
+threat model. The verifier treats `sha256:` prefixes consistently and
+rejects malformed hash strings.
+
+### 4.3 Canonicalization confusion
+
+Canonicalization divergence between signer and verifier can allow an
+attacker to produce a receipt that verifies under one canonicalization
+but fails under another. The verifier uses RFC 8785 JCS with AIP-0001
+ASCII-only keys, consistently. Test vectors in
+`specs/conformance/test-vectors/` exercise edge cases.
+
+Non-ASCII keys are rejected at ingest per AIP-0001, preventing the
+most common Unicode-normalization attacks.
+
+### 4.4 Timing attacks
+
+Signature byte comparison uses constant-time equality
+(`constantTimeEqual` in `src/util/hex.js`). Cache-timing and
+side-channel attacks against the @noble/curves Ed25519 implementation
+are outside this project's scope; we rely on the auditability of that
+library (audited by Trail of Bits, etc.).
+
+### 4.5 Input-parsing attacks
+
+The verifier parses JSON via Node.js's built-in parser. Deeply nested
+or oversized inputs can exhaust memory. Callers accepting untrusted
+input at scale should apply size limits (e.g., 10 MB) before invoking
+the verifier. A future release will impose a built-in default limit.
+
+### 4.6 Algorithm downgrade
+
+The verifier requires an explicit algorithm identifier in the signature
+envelope. Unknown algorithms trigger `unsupported_algorithm`, not
+silent fallback to a weaker algorithm. Hybrid post-quantum algorithms
+(`ed25519+ml-dsa-65`) are recognized structurally but fully verified
+only when full PQ support lands (v0.6+).
+
+### 4.7 JWKS endpoint compromise
+
+If a caller uses `--jwks ` and the URL endpoint serves malicious
+keys, those keys will be used for verification. The caller is
+responsible for ensuring JWKS endpoints are authenticated (TLS with
+pinned certificates or trusted PKI) and are controlled by the expected
+issuer.
+
+### 4.8 Process-level interference
+
+A malicious process running in the same user context can replace the
+verifier binary, modify its output, or intercept its inputs. The
+verifier does not mitigate this; it is a correct-implementation of
+verification in a correct-environment model. OS-level isolation (e.g.,
+Linux Landlock, macOS sandboxing) or process-level attestation is the
+caller's responsibility.
+
+## 5. Cryptographic Assumptions
+
+1. **Ed25519 is unforgeable** under known-message attacks (standard
+ EUF-CMA assumption per RFC 8032).
+2. **SHA-256 is collision-resistant** at ~128 bits of work.
+3. **JCS canonicalization** produces deterministic output for
+ semantically equivalent JSON.
+4. **The @noble/curves and @noble/hashes libraries** correctly
+ implement the claimed primitives, verified by third-party audit.
+
+Any future quantum computer sufficient to break Ed25519 invalidates
+signatures created under Ed25519. The verifier's hybrid PQ support
+path (`ed25519+ml-dsa-65`) provides forward compatibility.
+
+## 6. Supply Chain
+
+The v0.5.0 package is published with:
+
+- `npm publish --provenance` — Sigstore-attested supply chain
+- Sigil commitment in `sigil.json` covering all 15 source files
+- `@veritasacta/artifacts` as the single declared dependency
+- Transitive dependencies limited to `@noble/curves` and
+ `@noble/hashes`
+
+Callers verifying the supply chain should:
+
+1. Install from npm: `npm install @veritasacta/verify@0.5.0`
+2. Verify the npm provenance attestation via `npm audit signatures`
+ or Sigstore's cosign
+3. Run `--self-check` to confirm the installed files match the Sigil
+4. Cross-check the Sigil fingerprint against https://veritasacta.com
+
+## 7. Reporting Issues
+
+See `SECURITY.md` for the coordinated disclosure policy and contact
+address.
diff --git a/cli.js b/cli.js
new file mode 100644
index 0000000..4836f5d
--- /dev/null
+++ b/cli.js
@@ -0,0 +1,1249 @@
+#!/usr/bin/env node
+
+/**
+ * @veritasacta/verify — unified verifier CLI (v0.5.4)
+ *
+ * Verifies Ed25519 signed receipts, VOPRF anonymous-credential tokens,
+ * Knowledge Unit bundles, and selective-disclosure receipts — all offline,
+ * all under one binary with one Sigil commitment.
+ *
+ * Architecture is modular: this file is the entry point + dispatcher.
+ * Cryptographic verification lives in src/engines/*.js. Output formatting
+ * lives in src/output/*.js.
+ *
+ * Usage:
+ * npx @veritasacta/verify receipt.json
+ * npx @veritasacta/verify receipt.json --key
+ * npx @veritasacta/verify receipt.json --jwks
+ * npx @veritasacta/verify bundle.json --bundle
+ * npx @veritasacta/verify ku.json --mode ku
+ * npx @veritasacta/verify receipt.json --disclose field1,field2:salt:value
+ * npx @veritasacta/verify receipt.json --require-context sensor:temp<18
+ * npx @veritasacta/verify --self-check
+ * npx @veritasacta/verify --self-test
+ * npx @veritasacta/verify --capabilities
+ *
+ * Exit codes:
+ * 0 = signature valid (proven authentic)
+ * 1 = signature invalid (proven tampered)
+ * 2 = verifier error (malformed input, missing key, unsupported algorithm)
+ *
+ * References:
+ * - draft-farley-acta-signed-receipts-03
+ * - draft-farley-acta-knowledge-units-00
+ * - AIP-0001, AIP-0002, AIP-0003
+ * - Provisional patents #1-5
+ *
+ * @license Apache-2.0
+ */
+
+import { readFileSync, writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { detectFormat } from './src/detect.js';
+import { verifyReceipt, verifyBundle } from './src/engines/ed25519-receipt.js';
+import { verifyVoprfToken } from './src/engines/voprf-token.js';
+import { verifyKnowledgeUnit } from './src/engines/knowledge-unit.js';
+import {
+ verifySelectiveDisclosure,
+ listRedactedFields,
+} from './src/engines/selective-disclosure.js';
+import {
+ verifyCommittedReceipt,
+ loadDisclosuresFromText,
+} from './src/engines/commitment-mode.js';
+import { selfCheck, evaluateLiveContext } from './src/engines/sigil.js';
+import {
+ buildCanonicalAttestation,
+ buildVerificationReceipt,
+} from './src/engines/attestation.js';
+import { replayChain } from './src/engines/bulk.js';
+import { diffReceipts } from './src/engines/diff.js';
+import { runInit, buildNextSteps } from './src/engines/init.js';
+import { runProxy } from './src/engines/proxy.js';
+import { runDaemon } from './src/engines/daemon.js';
+import { verifyPrompt } from './src/engines/prompt.js';
+import { exploreChain, renderChainTree } from './src/engines/chain-explore.js';
+import { exportCompliance, renderComplianceHTML } from './src/engines/compliance-export.js';
+import { startDashboard } from './src/engines/dashboard.js';
+import { verifyDelegationChain } from './src/engines/delegation.js';
+import { verifyCosignatures } from './src/engines/cosign.js';
+import { parseContextArgs } from './src/context/live-context.js';
+import { detectTier } from './src/conformance.js';
+import { resolveFromJwks } from './src/util/jwks.js';
+import { appendAuditEntry } from './src/util/audit-log.js';
+import { fipsStatus } from './src/util/fips.js';
+import { renderHtmlReport } from './src/output/html-report.js';
+import { getError, exitCodeFor } from './src/errors.js';
+
+import {
+ formatReceiptResult,
+ formatBundleResult,
+ formatKuResult,
+ formatSelfCheckResult,
+ green,
+ red,
+ yellow,
+ dim,
+ bold,
+ teal,
+ renderTerminalSigil,
+} from './src/output/terminal.js';
+import { formatAsJson } from './src/output/json.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const PKG = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
+
+const MODE_LABELS = {
+ 'ed25519-receipt-v1': 'Ed25519 receipt v1 (RFC 8032)',
+ 'ed25519-receipt-v2': 'Ed25519 receipt v2 (RFC 8032 + draft-farley-acta-signed-receipts)',
+ 'ed25519-passport': 'Ed25519 Passport envelope (RFC 8032)',
+ 'ed25519-bundle': 'Ed25519 audit bundle',
+ 'voprf-token': 'VOPRF token (RFC 9497)',
+ 'knowledge-unit': 'Knowledge Unit bundle (draft-farley-acta-knowledge-units)',
+};
+
+// ──────────────────────────────────────────────────────────────────
+// CLI argument parsing
+// ──────────────────────────────────────────────────────────────────
+
+function parseArgs() {
+ const args = process.argv.slice(2);
+ const opts = {
+ file: null,
+ publicKey: null,
+ jwksUrl: null,
+ trustAnchor: null,
+ stdin: false,
+ mode: 'auto',
+ bundle: false,
+ json: false,
+ help: false,
+ version: false,
+ verbose: false,
+ selfTest: false,
+ selfCheck: false,
+ capabilities: false,
+ allowEmbeddedKey: false,
+ requireContext: [],
+ disclose: [],
+ disclosureFile: null,
+ tier: null,
+ strict: false,
+ noSigil: false,
+ attest: false,
+ attestOrg: null,
+ attestKey: null,
+ pinSigil: null,
+ auditLog: null,
+ replayChain: null,
+ diff: null,
+ auditReport: false,
+ output: null,
+ emitVerificationReceipt: false,
+ fips: false,
+ subcommand: null,
+ force: false,
+ proxyTarget: null,
+ proxyReceiptsDir: null,
+ daemonSocket: null,
+ frameworkOverride: null,
+ };
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ const next = () => args[++i];
+ switch (arg) {
+ case '--help':
+ case '-h': opts.help = true; break;
+ case '--version':
+ case '-V': opts.version = true; break;
+ case '--key':
+ case '-k': opts.publicKey = next(); break;
+ case '--jwks': opts.jwksUrl = next(); break;
+ case '--trust-anchor': opts.trustAnchor = next(); break;
+ case '--stdin': opts.stdin = true; break;
+ case '--mode': opts.mode = next(); break;
+ case '--bundle': opts.bundle = true; break;
+ case '--json': opts.json = true; break;
+ case '--verbose':
+ case '-v': opts.verbose = true; break;
+ case '--self-test': opts.selfTest = true; break;
+ case '--self-check': opts.selfCheck = true; break;
+ case '--capabilities': opts.capabilities = true; break;
+ case '--allow-embedded-key': opts.allowEmbeddedKey = true; break;
+ case '--require-context': opts.requireContext.push(next()); break;
+ case '--disclose': opts.disclose.push(next()); break;
+ case '--disclosure-file': opts.disclosureFile = next(); break;
+ case '--tier': opts.tier = Number(next()); break;
+ case '--strict': opts.strict = true; break;
+ case '--no-sigil': opts.noSigil = true; break;
+ case '--attest': opts.attest = true; break;
+ case '--attest-org': opts.attestOrg = next(); break;
+ case '--attest-key': opts.attestKey = next(); break;
+ case '--pin-sigil': opts.pinSigil = next(); break;
+ case '--audit-log': opts.auditLog = next(); break;
+ case '--replay-chain': opts.replayChain = next(); break;
+ case '--diff': opts.diff = next(); break;
+ case '--audit-report': opts.auditReport = true; break;
+ case '--output': opts.output = next(); break;
+ case '--emit-verification-receipt': opts.emitVerificationReceipt = true; break;
+ case '--fips': opts.fips = true; break;
+ case '--allow-partial-voprf': opts.allowPartialVoprf = true; break;
+ case '--force': opts.force = true; break;
+ case '--target': opts.proxyTarget = next(); break;
+ case '--framework': opts.frameworkOverride = next(); break;
+ case '--receipts-dir': opts.proxyReceiptsDir = next(); break;
+ case '--socket': opts.daemonSocket = next(); break;
+ case '--prompt-receipt': opts.promptReceipt = next(); break;
+ case '--sigstore-bundle': opts.sigstoreBundle = next(); break;
+ case '--expected-hash': opts.expectedHash = next(); break;
+ case '--search-dir': opts.chainSearchDir = next(); break;
+ case '--max-depth': opts.chainMaxDepth = Number(next()); break;
+ case '--profile': opts.profile = next(); break;
+ case '--start-date': opts.startDate = next(); break;
+ case '--end-date': opts.endDate = next(); break;
+ case '--org': opts.organization = next(); break;
+ case '--port': opts.dashboardPort = Number(next()); break;
+ case '--bind': opts.dashboardBind = next(); break;
+ case '--bilateral': opts.proxyBilateral = true; break;
+ case '--server-key': opts.serverKey = next(); break;
+ case '--scrub-secrets': opts.scrubSecrets = true; break;
+ case '--trace-id': opts.traceId = next(); break;
+ case '--group-by-trace': opts.groupByTrace = true; break;
+ case 'init':
+ case 'proxy':
+ case 'daemon':
+ case 'prompt':
+ case 'chain':
+ case 'compliance':
+ case 'dashboard':
+ if (!opts.subcommand) opts.subcommand = arg;
+ else if (!arg.startsWith('-')) opts.file = arg;
+ break;
+ case 'explore':
+ // `verify chain explore ` → subcommand stays "chain",
+ // sub-subcommand captured below
+ if (opts.subcommand === 'chain') opts.chainVerb = arg;
+ else if (!arg.startsWith('-')) opts.file = arg;
+ break;
+ default:
+ if (!arg.startsWith('-')) opts.file = arg;
+ // unknown flags silently ignored (forward-compat)
+ }
+ }
+ return opts;
+}
+
+function printHelp() {
+ console.log(`
+${bold('@veritasacta/verify')} ${PKG.version} — unified verifier for signed receipts, VOPRF tokens, and Knowledge Units
+
+${bold('Usage:')}
+ npx @veritasacta/verify Auto-detect format, verify
+ npx @veritasacta/verify --key Provide verification key
+ npx @veritasacta/verify --jwks Fetch key from JWKS
+ npx @veritasacta/verify --mode receipt|voprf|ku
+ npx @veritasacta/verify --bundle Verify audit bundle
+ cat receipt.json | npx @veritasacta/verify --stdin Read from stdin
+ npx @veritasacta/verify --json Machine-readable output
+
+${bold('Selective disclosure (AIP-0002 legacy):')}
+ --disclose field:salt:value Reveal a field and verify its commitment
+
+${bold('Commitment-mode disclosure (draft-farley-acta-signed-receipts-01):')}
+ --disclosure-file Verify Merkle inclusion proofs against committed_fields_root
+
+${bold('Live-context verification (Sigil claim 2):')}
+ --require-context clock:±5s
+ --require-context geofence:inside:
+ --require-context sensor:temp<18
+
+${bold('Subcommands:')}
+ init Zero-config onboarding wizard (detects framework, generates keys)
+ proxy --target "" Wrap an MCP server; sign every tools/call transparently
+ daemon Sidecar daemon: sign receipts over a unix socket (any language)
+ prompt Verify a prompt/skill/system-instruction file's provenance
+ [--prompt-receipt | --sigstore-bundle | --expected-hash ]
+ chain explore Walk a receipt chain to its root; verify every hash link
+ [--search-dir ] [--max-depth N] [--json]
+ compliance SOC 2 / ISO 42001 / EU AI Act evidence bundle from a receipt directory
+ --receipts-dir [--framework soc2|iso42001|eu-ai-act|all]
+ [--start-date ISO] [--end-date ISO] [--org ""] [--output audit.html|bundle.json]
+ dashboard Start local audit dashboard server (loopback only, no telemetry)
+ [--port 3847] [--bind 127.0.0.1] [--receipts-dir ]
+ proxy --target "" Already-present wrap-and-sign proxy. New flags:
+ [--bilateral --server-key ] Cosign every receipt with a second key
+ [--scrub-secrets] Redact api_key/token/etc in outgoing args and flag on receipt
+ [--trace-id ] Stamp a workflow trace_id on every receipt
+
+${bold('Options:')}
+ --key, -k Ed25519 public key (64 hex chars)
+ --jwks JWKS endpoint to fetch signing key
+ --trust-anchor Local trust-anchor JSON with public keys
+ --mode Force mode: receipt|voprf|ku|auto (default: auto)
+ --bundle Verify as audit bundle
+ --stdin Read input from stdin
+ --json Output JSON
+ --verbose, -v Detailed verification info
+ --tier N Require minimum conformance tier (1-5)
+ --pin-sigil Require installed Sigil fingerprint to match
+ --strict Disable all deprecated fallbacks
+ --fips Enforce FIPS-approved algorithms only
+ --audit-log Append verification event to a local JSONL audit log
+ --self-test Verify bundled sample artifacts
+ --self-check Prove this verifier is the canonical release
+ --capabilities List supported modes/algorithms/tiers
+ --allow-embedded-key DEPRECATED. Accept keys embedded in payloads.
+ Removed in v0.6.0.
+ --allow-partial-voprf Treat a partial (structural-only) VOPRF result
+ as valid. Default fails closed (exit 2).
+ Full DLEQ verification lands in v0.6.0.
+ --no-sigil Suppress Sigil art in terminal output
+ --help, -h
+ --version, -V
+
+${bold('Bulk / replay / diff:')}
+ --replay-chain Verify every receipt in a JSONL chain file
+ --diff Show structural diff between two receipts
+
+${bold('Attestation and audit:')}
+ --attest Emit a canonical verifier attestation (signed)
+ --attest-org Include this org name in the attestation
+ --attest-key Override attester key location
+ (default: ~/.veritasacta-verify/attester.json)
+ --emit-verification-receipt
+ Emit a signed verification receipt for the subject
+ --audit-report Render an HTML audit report
+ --output Write output to a file (HTML reports, attestations)
+
+${bold('Supported formats:')}
+ v1 artifacts, v2 artifacts, Passport envelopes, audit bundles,
+ VOPRF tokens, Knowledge Unit bundles, selective-disclosure receipts.
+
+${bold('Exit codes:')}
+ 0 Valid — proven authentic
+ 1 Invalid — proven tampered
+ 2 Undecidable — malformed input, missing key, unsupported algorithm,
+ or --tier requirement not achieved
+
+${bold('Standards:')}
+ RFC 8032 (Ed25519), RFC 8785 (JCS), RFC 9497 (VOPRF),
+ RFC 7517/7638 (JWK / thumbprint),
+ draft-farley-acta-signed-receipts-03,
+ draft-farley-acta-knowledge-units-00,
+ AIP-0001, AIP-0002, AIP-0003.
+
+${bold('Protocol:')} https://veritasacta.com (open, Apache-2.0)
+${bold('Managed:')} https://scopeblind.com (optional, commercial)
+`);
+}
+
+function printCapabilities() {
+ console.log(JSON.stringify({
+ package: PKG.name,
+ version: PKG.version,
+ modes: Object.keys(MODE_LABELS),
+ algorithms: ['ed25519', 'EdDSA', 'voprf-p256-sha256'],
+ unsupported_but_recognized: ['ed25519+ml-dsa-65', 'ed25519+dilithium3'],
+ tiers: [1, 2, 3, 4],
+ max_tier_v0_5_0: 4,
+ features: [
+ 'ed25519-receipt-verification',
+ 'voprf-token-structural-verification',
+ 'knowledge-unit-bundle-verification',
+ 'selective-disclosure-aip-0002',
+ 'sigil-self-check-claim-1',
+ 'sigil-live-context-claim-2',
+ 'conformance-tier-detection',
+ 'embedded-key-rejection',
+ ],
+ specs: [
+ 'RFC 8032', 'RFC 8785', 'RFC 9497', 'RFC 7517', 'RFC 7638',
+ 'draft-farley-acta-signed-receipts-03',
+ 'draft-farley-acta-knowledge-units-00',
+ 'AIP-0001', 'AIP-0002', 'AIP-0003',
+ ],
+ wayfinding: {
+ protocol: 'https://veritasacta.com',
+ managed: 'https://scopeblind.com',
+ },
+ }, null, 2));
+}
+
+// ──────────────────────────────────────────────────────────────────
+// Self-check: prove this binary is the canonical unmodified release
+// ──────────────────────────────────────────────────────────────────
+
+async function runSelfCheck() {
+ const sigilPath = join(__dirname, 'sigil.json');
+ let sigil;
+ try {
+ sigil = JSON.parse(readFileSync(sigilPath, 'utf-8'));
+ } catch {
+ console.log(`\n ${red('✗')} No sigil.json found — this verifier has no Sigil commitment.`);
+ console.log(` This may be a development build or a fork.\n`);
+ process.exit(2);
+ }
+
+ // Use the shared monitored file list (keeps runSelfCheck and
+ // selfCheckResult in sync).
+ const bufs = [];
+ for (const f of MONITORED_FILES_FOR_SIGIL) {
+ try { bufs.push(readFileSync(join(__dirname, f))); } catch { /* missing */ }
+ }
+ const installedSourceBytes = Buffer.concat(bufs);
+
+ const r = selfCheck({ sigil, installedSourceBytes });
+ r.projectPublicKey = sigil.project_public_key;
+ console.log(bold('@veritasacta/verify — self-check'));
+ console.log(formatSelfCheckResult(r));
+ process.exit(r.canonical ? 0 : 1);
+}
+
+// ──────────────────────────────────────────────────────────────────
+// Self-test: verify bundled samples
+// ──────────────────────────────────────────────────────────────────
+
+async function runSelfTest(opts) {
+ const samplesDir = join(__dirname, 'samples');
+ const testKey = 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a';
+ let allPassed = true;
+
+ console.log(`\n${bold('@veritasacta/verify — self-test')}\n`);
+ if (!process.env.CI && !process.env.NO_COLOR) {
+ console.log(renderTerminalSigil(testKey));
+ console.log('');
+ }
+
+ try {
+ const receipt = JSON.parse(readFileSync(join(samplesDir, 'sample-receipt.json'), 'utf-8'));
+ const detected = detectFormat(receipt);
+ const r = await verifyReceipt(receipt, detected.mode, { publicKey: testKey });
+ if (r.valid) {
+ console.log(` ${green('✓')} Sample receipt: ${green('VALID')} (${r.type}, kid: ${r.kid || 'n/a'})`);
+ } else {
+ console.log(` ${red('✗')} Sample receipt: ${red('INVALID')} (${r.error})`);
+ allPassed = false;
+ }
+ } catch (e) {
+ console.log(` ${red('✗')} Sample receipt: ${red(e.message)}`);
+ allPassed = false;
+ }
+
+ try {
+ const bundle = JSON.parse(readFileSync(join(samplesDir, 'sample-bundle.json'), 'utf-8'));
+ const r = await verifyBundle(bundle, { publicKey: testKey });
+ if (r.valid) {
+ console.log(` ${green('✓')} Sample bundle: ${green('VALID')} (${r.passed}/${r.total} receipts)`);
+ } else {
+ console.log(` ${red('✗')} Sample bundle: ${red('INVALID')} (${r.failed} failed)`);
+ allPassed = false;
+ }
+ } catch (e) {
+ console.log(` ${red('✗')} Sample bundle: ${red(e.message)}`);
+ allPassed = false;
+ }
+
+ console.log('');
+ if (allPassed) {
+ console.log(` ${green('All self-tests passed.')} The verifier is working correctly.`);
+ console.log(` ${dim('No ScopeBlind servers were contacted. No accounts required.')}`);
+ } else {
+ console.log(` ${red('Some self-tests failed.')} Check the output above.`);
+ }
+ console.log('');
+ process.exit(allPassed ? 0 : 1);
+}
+
+// ──────────────────────────────────────────────────────────────────
+// Dispatch
+// ──────────────────────────────────────────────────────────────────
+
+async function readInput(opts) {
+ if (opts.stdin) {
+ const chunks = [];
+ for await (const chunk of process.stdin) chunks.push(chunk);
+ return Buffer.concat(chunks).toString('utf-8');
+ }
+ if (opts.file) return readFileSync(opts.file, 'utf-8');
+ return null;
+}
+
+async function dispatch(input, opts) {
+ // Forced-mode overrides
+ let detected = detectFormat(input);
+ if (opts.mode && opts.mode !== 'auto') {
+ const forced = opts.mode.toLowerCase();
+ if (forced === 'receipt') detected.mode = 'ed25519-passport';
+ else if (forced === 'voprf') detected.mode = 'voprf-token';
+ else if (forced === 'ku') detected.mode = 'knowledge-unit';
+ else if (forced === 'bundle') detected.mode = 'ed25519-bundle';
+ }
+ if (opts.bundle) detected.mode = 'ed25519-bundle';
+
+ // Resolve JWKS if needed
+ let publicKey = opts.publicKey;
+ let keySource = publicKey ? 'provided' : null;
+ if (!publicKey && opts.jwksUrl) {
+ const kid = input.kid || input.signature?.kid;
+ const resolved = await resolveFromJwks(opts.jwksUrl, kid);
+ if (resolved.key) {
+ publicKey = resolved.key;
+ keySource = resolved.source?.resolved ? `jwks:${resolved.source.resolved}` : 'jwks';
+ } else {
+ const result = {
+ valid: false,
+ error: 'jwks_fetch_failed',
+ format: detected.mode,
+ detail: resolved.error,
+ };
+ return result;
+ }
+ }
+
+ const subOpts = { ...opts, publicKey };
+
+ // Route to engine
+ switch (detected.mode) {
+ case 'ed25519-bundle': {
+ return await verifyBundle(input, subOpts);
+ }
+ case 'knowledge-unit': {
+ const r = await verifyKnowledgeUnit(input, subOpts);
+ const tier = detectTier({ mode: 'knowledge-unit', payloadFields: {} });
+ return { ...r, tier };
+ }
+ case 'voprf-token': {
+ const r = await verifyVoprfToken(input, subOpts);
+ const tier = detectTier({
+ mode: 'voprf-token',
+ payloadFields: {
+ transport_hint: r.transport_hint,
+ },
+ voprfVerified: r.valid,
+ });
+ return { ...r, tier, modeLabel: MODE_LABELS['voprf-token'] };
+ }
+ case 'ed25519-receipt-v1':
+ case 'ed25519-receipt-v2':
+ case 'ed25519-passport': {
+ const r = await verifyReceipt(input, detected.mode, subOpts);
+
+ // Selective disclosure: attach if commitments present.
+ // Two formats supported:
+ // - Legacy AIP-0002 (_commitments map + --disclose field:salt:value)
+ // - draft-farley-acta-signed-receipts-01 commitment-mode
+ // (committed_fields_root + --disclosure-file path)
+ let disclosureResult = null;
+ const hasCommittedFieldsRoot = detected.signals?.includes('committed_fields_root');
+ const hasLegacyCommitments = detected.signals?.includes('_commitments');
+
+ if (hasCommittedFieldsRoot) {
+ // draft-01 commitment-mode path
+ let disclosures = [];
+ if (opts.disclosureFile) {
+ try {
+ const fs = await import('node:fs');
+ const text = fs.readFileSync(opts.disclosureFile, 'utf8');
+ disclosures = loadDisclosuresFromText(text);
+ } catch (err) {
+ r.valid = false;
+ r.error = `disclosure_file_load_failed: ${err?.message ?? 'unknown'}`;
+ }
+ }
+ if (r.valid !== false) {
+ disclosureResult = verifyCommittedReceipt(input, disclosures);
+ r.committedFieldsRoot = input.committed_fields_root;
+ r.committedFieldNames = input.committed_field_names;
+ r.disclosedFields = disclosures.map((d) => d.name);
+ if (!disclosureResult.valid) {
+ r.valid = false;
+ r.error = disclosureResult.error || 'commitment_mismatch';
+ }
+ }
+ } else if (hasLegacyCommitments) {
+ // Legacy AIP-0002 path
+ const disclosures = parseDisclosures(opts.disclose);
+ disclosureResult = verifySelectiveDisclosure(input, disclosures);
+ r.redactedFields = listRedactedFields(input);
+ r.disclosedFields = disclosures.map((d) => d.field);
+ if (!disclosureResult.valid) {
+ r.valid = false;
+ r.error = 'commitment_mismatch';
+ }
+ }
+
+ // Live-context predicates
+ if (opts.requireContext.length > 0) {
+ const preds = parseContextArgs(opts.requireContext);
+ const ctx = await evaluateLiveContext(preds);
+ r.contextChecks = ctx.checks;
+ if (!ctx.allSatisfied) {
+ r.valid = false;
+ r.error = r.error || 'context_requirement_unmet';
+ }
+ }
+
+ // Surface attestation_mode as dedicated output field
+ if (r.payloadFields?.attestation_mode) {
+ r.attestationMode = r.payloadFields.attestation_mode;
+ }
+ if (r.payloadFields?.scope) r.scope = r.payloadFields.scope;
+ if (r.payloadFields?.nullifier) r.nullifier = r.payloadFields.nullifier;
+ if (r.payloadFields?.transport_hint) r.transport_hint = r.payloadFields.transport_hint;
+
+ const tier = detectTier({
+ mode: detected.mode,
+ payloadFields: r.payloadFields,
+ disclosuresVerified: disclosureResult?.disclosuresVerified || 0,
+ });
+ r.tier = tier;
+ r.modeLabel = MODE_LABELS[detected.mode];
+ r.specVersion = 'draft-farley-acta-signed-receipts-03';
+ return r;
+ }
+ default:
+ return {
+ valid: false,
+ error: 'unknown_format',
+ format: 'unknown',
+ detail: `Could not detect format. Signals: ${detected.signals.join(', ') || 'none'}`,
+ };
+ }
+}
+
+/**
+ * Parse --disclose arguments into disclosure packages.
+ * Format: "field.path:salt_hex:json_value" (value is JSON-parsed)
+ */
+function parseDisclosures(args) {
+ const packages = [];
+ for (const a of args) {
+ const firstColon = a.indexOf(':');
+ const secondColon = a.indexOf(':', firstColon + 1);
+ if (firstColon < 0 || secondColon < 0) continue;
+ const field = a.slice(0, firstColon);
+ const salt = a.slice(firstColon + 1, secondColon);
+ const raw = a.slice(secondColon + 1);
+ let value;
+ try { value = JSON.parse(raw); } catch { value = raw; }
+ packages.push({ field, salt, value });
+ }
+ return packages;
+}
+
+// ──────────────────────────────────────────────────────────────────
+// Main
+// ──────────────────────────────────────────────────────────────────
+
+function applyTierGate(result, minTier) {
+ if (!minTier) return result;
+ const achieved = result.tier?.tier || 1;
+ if (achieved < minTier) {
+ return {
+ ...result,
+ valid: false,
+ error: 'tier_not_achieved',
+ detail: `Required tier T${minTier}, achieved T${achieved}`,
+ };
+ }
+ return result;
+}
+
+/**
+ * Load and parse the Sigil commitment from disk.
+ * Used by attestation and report generators.
+ */
+function loadSigil() {
+ try {
+ return JSON.parse(readFileSync(join(__dirname, 'sigil.json'), 'utf-8'));
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * --pin-sigil: require the installed Sigil fingerprint to match a
+ * specified value. Fails early if mismatch.
+ */
+function enforcePinSigil(expected) {
+ const sigil = loadSigil();
+ if (!sigil) {
+ console.error(red('--pin-sigil requires a sigil.json; none found.'));
+ process.exit(2);
+ }
+ if (sigil.fingerprint !== expected) {
+ console.error(red(`--pin-sigil: mismatch (expected ${expected}, got ${sigil.fingerprint})`));
+ console.error(yellow(`Installed verifier is "${sigil.name}" (${sigil.fingerprint}); expected fingerprint ${expected}.`));
+ console.error(dim(`Ensure you have installed the expected release: npm install @veritasacta/verify@`));
+ process.exit(2);
+ }
+}
+
+/**
+ * --replay-chain FILE: verify an entire JSONL chain.
+ */
+async function runReplayChain(opts) {
+ const sigil = loadSigil();
+ const result = await replayChain(opts.replayChain, opts);
+
+ if (opts.auditReport) {
+ let attestation = null;
+ if (opts.attest) {
+ const r = selfCheckResult(sigil);
+ attestation = buildCanonicalAttestation({
+ sigil, canonical: r.canonical, org: opts.attestOrg, keyPath: opts.attestKey,
+ });
+ }
+ const html = renderHtmlReport({ result, sigil, attestation, title: 'Veritas Acta chain-replay audit report' });
+ if (opts.output) {
+ writeFileSync(opts.output, html);
+ console.log(dim(`wrote ${opts.output}`));
+ } else {
+ console.log(html);
+ }
+ process.exit(result.valid ? 0 : 1);
+ }
+
+ if (opts.json) {
+ console.log(JSON.stringify(result, null, 2));
+ } else {
+ console.log(`\n${bold('Chain replay result')}`);
+ console.log(` Total: ${result.total}`);
+ console.log(` Verified: ${green(String(result.verified))}`);
+ console.log(` Failed: ${result.failed > 0 ? red(String(result.failed)) : '0'}`);
+ console.log(` Chain breaks: ${result.chainBreaks > 0 ? red(String(result.chainBreaks)) : '0'}`);
+ if (result.errors.length > 0) {
+ console.log(`\n ${red('Errors:')}`);
+ for (const e of result.errors.slice(0, 20)) console.log(` ${red('•')} ${e}`);
+ if (result.errors.length > 20) console.log(` ${dim(`... ${result.errors.length - 20} more`)}`);
+ }
+ console.log('');
+ }
+ process.exit(result.valid ? 0 : 1);
+}
+
+/**
+ * --diff A B: show structural differences between two receipts.
+ */
+async function runDiff(opts) {
+ const a = JSON.parse(await readInput(opts) || '{}');
+ const b = JSON.parse(readFileSync(opts.diff, 'utf-8'));
+ const d = diffReceipts(a, b);
+ if (opts.json) {
+ console.log(JSON.stringify(d, null, 2));
+ } else {
+ console.log(`\n${bold('Receipt diff')}`);
+ console.log(` Canonical hash A: ${dim(d.canonical_hash_a)}`);
+ console.log(` Canonical hash B: ${dim(d.canonical_hash_b)}`);
+ console.log(` Payload identical: ${d.hash_equal ? green('yes') : red('no')}`);
+ console.log(` Signature identical: ${d.signature_equal ? green('yes') : red('no')}`);
+ if (d.added.length > 0) console.log(`\n ${green('Added:')} ${d.added.join(', ')}`);
+ if (d.removed.length > 0) console.log(` ${red('Removed:')} ${d.removed.join(', ')}`);
+ if (d.changed.length > 0) {
+ console.log(` ${yellow('Changed:')}`);
+ for (const c of d.changed) {
+ console.log(` ${c.field}: ${dim(JSON.stringify(c.before))} -> ${dim(JSON.stringify(c.after))}`);
+ }
+ }
+ console.log('');
+ }
+ process.exit(d.hash_equal && d.signature_equal ? 0 : 1);
+}
+
+/**
+ * verify prompt [--prompt-receipt | --sigstore-bundle | --expected-hash ]
+ *
+ * Verify the provenance of a prompt / instruction file against one of
+ * three sources. Closes the CLAUDE.md / SKILLS.md supply-chain gap.
+ */
+async function runPromptVerify(opts) {
+ if (!opts.file) {
+ console.error(red(' verify prompt requires a file path to the prompt'));
+ console.error(dim(' usage: verify prompt [--prompt-receipt | --sigstore-bundle | --expected-hash ]'));
+ process.exit(2);
+ }
+ const result = await verifyPrompt({
+ promptPath: opts.file,
+ receiptPath: opts.promptReceipt,
+ sigstoreBundle: opts.sigstoreBundle,
+ expectedHash: opts.expectedHash,
+ });
+
+ if (opts.json) {
+ console.log(JSON.stringify(result, null, 2));
+ } else {
+ console.log('');
+ console.log(`${bold('Prompt verification')}`);
+ console.log(` File: ${result.prompt_path}`);
+ console.log(` Hash: ${dim(result.prompt_hash)}`);
+ if (result.source) console.log(` Source: ${result.source}`);
+ if (result.expected_hash) {
+ const eq = result.expected_hash === result.prompt_hash;
+ console.log(` Expected: ${dim(result.expected_hash)} ${eq ? green('match') : red('MISMATCH')}`);
+ }
+ if (result.receipt_summary) {
+ console.log(` Receipt: ${dim(JSON.stringify(result.receipt_summary))}`);
+ }
+ if (result.bundle_summary) {
+ console.log(` Bundle: ${dim(JSON.stringify(result.bundle_summary))}`);
+ }
+ if (result.valid) {
+ console.log(` ${green('\u2713')} prompt matches the asserted provenance`);
+ } else {
+ console.log(` ${red('\u2717')} ${result.error || 'verification failed'}`);
+ }
+ console.log('');
+ }
+ process.exit(result.valid ? 0 : 1);
+}
+
+/**
+ * verify chain explore [--search-dir ] [--max-depth N]
+ *
+ * Walk the causal ancestry of a receipt via previousReceiptHash and
+ * validate every hash link along the way. Surfaces cryptographic
+ * causal integrity as a concrete operation.
+ */
+async function runChainExplore(opts) {
+ if (opts.chainVerb !== 'explore') {
+ console.error(red(` unknown chain subcommand: ${opts.chainVerb || '(none)'}`));
+ console.error(dim(' usage: verify chain explore [--search-dir ] [--max-depth N]'));
+ process.exit(2);
+ }
+ if (!opts.file) {
+ console.error(red(' verify chain explore requires a receipt file path'));
+ process.exit(2);
+ }
+ const result = await exploreChain({
+ receiptPath: opts.file,
+ searchDir: opts.chainSearchDir,
+ maxDepth: opts.chainMaxDepth,
+ });
+
+ if (opts.json) {
+ console.log(JSON.stringify(result, null, 2));
+ } else {
+ console.log(renderChainTree(result));
+ }
+ process.exit(result.valid ? 0 : 1);
+}
+
+/**
+ * verify compliance [--framework soc2|iso42001|eu-ai-act|all]
+ * --receipts-dir [--start-date ISO] [--end-date ISO]
+ * [--org ""] [--output ] [--json]
+ *
+ * Produce a compliance evidence bundle: control-mapped receipt
+ * summaries, HTML audit report suitable for auditors.
+ */
+async function runCompliance(opts) {
+ const dir = opts.proxyReceiptsDir || opts.receiptsDir || opts.file;
+ if (!dir) {
+ console.error(red(' verify compliance requires --receipts-dir '));
+ console.error(dim(' usage: verify compliance --receipts-dir [--framework soc2|iso42001|eu-ai-act|all]'));
+ console.error(dim(' [--start-date ISO] [--end-date ISO] [--org ""] [--output ] [--json]'));
+ process.exit(2);
+ }
+ let result;
+ try {
+ result = exportCompliance({
+ receiptsDir: dir,
+ framework: opts.frameworkOverride || 'all',
+ startDate: opts.startDate,
+ endDate: opts.endDate,
+ organizationName: opts.organization,
+ });
+ } catch (err) {
+ console.error(red(` compliance export failed: ${err.message}`));
+ process.exit(2);
+ }
+
+ if (opts.output) {
+ const isHtml = opts.output.endsWith('.html');
+ const body = isHtml
+ ? renderComplianceHTML(result)
+ : JSON.stringify(result, null, 2);
+ writeFileSync(opts.output, body);
+ console.log(green(` ✓ Compliance bundle written to ${opts.output}`));
+ console.log(dim(` Receipts scanned: ${result.manifest.receipts_scanned} / in window: ${result.manifest.receipts_in_window}`));
+ for (const [fwId, fw] of Object.entries(result.manifest.frameworks)) {
+ const evidenced = Object.values(fw.controls).filter((c) => c.evidence_count > 0).length;
+ const total = Object.keys(fw.controls).length;
+ console.log(dim(` ${fw.framework}: ${evidenced}/${total} controls evidenced`));
+ }
+ return;
+ }
+
+ if (opts.json) {
+ console.log(JSON.stringify(result, null, 2));
+ return;
+ }
+
+ // Terminal rendering
+ console.log('');
+ console.log(bold('Compliance evidence bundle'));
+ console.log(` Organization: ${result.manifest.organization}`);
+ console.log(` Receipts: ${result.manifest.receipts_scanned} scanned, ${result.manifest.receipts_in_window} in window`);
+ if (result.manifest.window.start || result.manifest.window.end) {
+ console.log(` Window: ${result.manifest.window.start || '(begin)'} → ${result.manifest.window.end || '(end)'}`);
+ }
+ for (const [, fw] of Object.entries(result.manifest.frameworks)) {
+ console.log('');
+ console.log(bold(` ${fw.framework}`));
+ for (const [ctrlId, ctrl] of Object.entries(fw.controls)) {
+ const mark = ctrl.evidence_count > 0 ? green('●') : dim('○');
+ console.log(` ${mark} ${ctrlId} ${ctrl.name} ${dim(`(${ctrl.evidence_count})`)}`);
+ }
+ }
+ if (result.warnings.length) {
+ console.log('');
+ console.log(yellow(' Warnings:'));
+ for (const w of result.warnings) console.log(` • ${w}`);
+ }
+ console.log('');
+}
+
+/**
+ * verify dashboard [--port 3847] [--bind 127.0.0.1] [--receipts-dir ]
+ *
+ * Spin up the local dashboard server. Opens a browser tab to it.
+ * Binds loopback only.
+ */
+async function runDashboard(opts) {
+ const receiptsDir = opts.proxyReceiptsDir || opts.receiptsDir;
+ let dash;
+ try {
+ dash = await startDashboard({
+ port: opts.dashboardPort || 3847,
+ bind: opts.dashboardBind || '127.0.0.1',
+ receiptsDir,
+ });
+ } catch (err) {
+ console.error(red(` dashboard failed to start: ${err.message}`));
+ process.exit(2);
+ }
+ console.log('');
+ console.log(bold(' Veritas Acta dashboard'));
+ console.log(` Serving at: ${dash.url}`);
+ if (receiptsDir) {
+ console.log(` Receipts: ${receiptsDir}`);
+ } else {
+ console.log(dim(` Receipts: (none — paste JSON or drop files in the UI)`));
+ }
+ console.log(dim(' Loopback-only. No TLS, no auth, no telemetry.'));
+ console.log(dim(' Press Ctrl+C to stop.'));
+ console.log('');
+
+ // Stay alive until interrupted.
+ await new Promise(() => {});
+}
+
+/**
+ * Canonical list of files committed by the Sigil.
+ * MUST match generate-sigil.mjs's MONITORED_FILES exactly.
+ */
+const MONITORED_FILES_FOR_SIGIL = [
+ 'cli.js',
+ 'src/detect.js',
+ 'src/conformance.js',
+ 'src/errors.js',
+ 'src/engines/ed25519-receipt.js',
+ 'src/engines/voprf-token.js',
+ 'src/engines/knowledge-unit.js',
+ 'src/engines/selective-disclosure.js',
+ 'src/engines/sigil.js',
+ 'src/engines/attestation.js',
+ 'src/engines/bulk.js',
+ 'src/engines/diff.js',
+ 'src/engines/init.js',
+ 'src/engines/proxy.js',
+ 'src/engines/daemon.js',
+ 'src/engines/prompt.js',
+ 'src/engines/chain-explore.js',
+ 'src/engines/compliance-export.js',
+ 'src/engines/dsse.js',
+ 'src/engines/delegation.js',
+ 'src/engines/cosign.js',
+ 'src/engines/dashboard.js',
+ 'src/engines/rekor.js',
+ 'src/engines/attestation-quote.js',
+ 'src/engines/watch.js',
+ 'src/engines/sbom.js',
+ 'src/engines/transparency.js',
+ 'src/context/live-context.js',
+ 'src/output/terminal.js',
+ 'src/output/json.js',
+ 'src/output/html-report.js',
+ 'src/util/canonical.js',
+ 'src/util/hex.js',
+ 'src/util/jwks.js',
+ 'src/util/audit-log.js',
+ 'src/util/fips.js',
+ 'src/util/voprf-crypto.js',
+ 'src/util/voprf-crypto-v2.js',
+];
+
+function selfCheckResult(sigil) {
+ if (!sigil) return { canonical: false };
+ const bufs = [];
+ for (const f of MONITORED_FILES_FOR_SIGIL) {
+ try { bufs.push(readFileSync(join(__dirname, f))); } catch { /* missing => modified */ }
+ }
+ const installedSourceBytes = Buffer.concat(bufs);
+ return selfCheck({ sigil, installedSourceBytes });
+}
+
+async function main() {
+ const opts = parseArgs();
+
+ if (opts.help) { printHelp(); process.exit(0); }
+ if (opts.version) { console.log(PKG.version); process.exit(0); }
+ if (opts.capabilities) { printCapabilities(); process.exit(0); }
+ if (opts.pinSigil) enforcePinSigil(opts.pinSigil);
+
+ // Subcommands
+ if (opts.subcommand === 'init') {
+ const result = await runInit({ framework: opts.frameworkOverride, org: opts.attestOrg, force: opts.force });
+ if (result.status === 'exists') {
+ console.error(yellow(result.message));
+ process.exit(1);
+ }
+ console.log('');
+ if (!process.env.CI && !process.env.NO_COLOR) {
+ console.log(renderTerminalSigil(result.key.pubHex));
+ console.log('');
+ }
+ console.log(`${green('✓')} ${bold('Veritas Acta initialized')}`);
+ console.log(` Directory: ${result.vaDir}`);
+ console.log(` Kid: ${result.key.kid}`);
+ console.log(` Pubkey: ${result.key.pubHex}`);
+ if (result.detection) {
+ console.log(` Framework: ${green(result.detection.framework)} (${result.detection.language})`);
+ } else {
+ console.log(` Framework: ${yellow('not auto-detected')} — see manual setup below`);
+ }
+ console.log('');
+ console.log(bold('Next steps:'));
+ for (const line of buildNextSteps(result.detection, result.config)) {
+ console.log(` ${line}`);
+ }
+ console.log('');
+ console.log(dim(`Protocol: https://veritasacta.com`));
+ console.log(dim(`Managed: https://scopeblind.com (optional)`));
+ console.log('');
+ process.exit(0);
+ }
+
+ if (opts.subcommand === 'proxy') {
+ const keyPath = opts.attestKey || join(process.cwd(), '.veritasacta', 'attester.json');
+ const code = await runProxy({
+ target: opts.proxyTarget,
+ key: keyPath,
+ receiptsDir: opts.proxyReceiptsDir,
+ });
+ process.exit(code);
+ }
+
+ if (opts.subcommand === 'daemon') {
+ await runDaemon({
+ socket: opts.daemonSocket,
+ key: opts.attestKey,
+ receiptsDir: opts.proxyReceiptsDir,
+ });
+ return;
+ }
+
+ if (opts.subcommand === 'prompt') {
+ await runPromptVerify(opts);
+ return;
+ }
+
+ if (opts.subcommand === 'chain') {
+ await runChainExplore(opts);
+ return;
+ }
+
+ if (opts.subcommand === 'compliance') {
+ await runCompliance(opts);
+ return;
+ }
+
+ if (opts.subcommand === 'dashboard') {
+ await runDashboard(opts);
+ return;
+ }
+
+ if (opts.selfCheck) { await runSelfCheck(); return; }
+ if (opts.selfTest) { await runSelfTest(opts); return; }
+ if (opts.replayChain) { await runReplayChain(opts); return; }
+ if (opts.diff) { await runDiff(opts); return; }
+
+ // --attest without a file: emit a standalone canonical attestation
+ if (opts.attest && !opts.file && !opts.stdin) {
+ const sigil = loadSigil();
+ const r = selfCheckResult(sigil);
+ const att = buildCanonicalAttestation({
+ sigil, canonical: r.canonical, org: opts.attestOrg, keyPath: opts.attestKey,
+ });
+ const output = JSON.stringify(att, null, 2);
+ if (opts.output) { writeFileSync(opts.output, output); console.error(dim(`wrote ${opts.output}`)); }
+ else console.log(output);
+ process.exit(0);
+ }
+
+ const raw = await readInput(opts);
+ if (raw === null) {
+ console.error(red('Error: no input file specified.'));
+ console.error('Usage: npx @veritasacta/verify [--key ]');
+ process.exit(2);
+ }
+
+ let input;
+ try {
+ input = JSON.parse(raw);
+ } catch (e) {
+ console.error(red(`Error: invalid JSON: ${e.message}`));
+ process.exit(2);
+ }
+
+ if (opts.strict && opts.allowEmbeddedKey) {
+ console.error(yellow('Warning: --strict overrides --allow-embedded-key (embedded keys always rejected in strict mode).'));
+ opts.allowEmbeddedKey = false;
+ }
+
+ // FIPS enforcement (if set, checks algorithm compliance before verify)
+ if (opts.fips) {
+ const claimedAlgo = input.algorithm || input.signature?.alg || 'ed25519';
+ const f = fipsStatus(claimedAlgo);
+ if (!f.approved) {
+ const result = {
+ valid: false,
+ error: 'unsupported_algorithm',
+ detail: `FIPS mode: ${f.reason}`,
+ format: 'fips-rejected',
+ };
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
+ else {
+ console.error(red(`✗ FIPS mode rejects algorithm "${claimedAlgo}"`));
+ console.error(yellow(f.reason));
+ }
+ process.exit(2);
+ }
+ }
+
+ let result = await dispatch(input, opts);
+ result = applyTierGate(result, opts.tier);
+
+ // Attach error metadata
+ if (result.error) {
+ const meta = getError(result.error);
+ if (meta) result.errorMeta = meta;
+ }
+
+ // Audit log
+ if (opts.auditLog) {
+ const sigil = loadSigil();
+ try {
+ appendAuditEntry(opts.auditLog, result, { sigil, org: opts.attestOrg });
+ } catch (e) {
+ console.error(yellow(`Warning: failed to append audit log: ${e.message}`));
+ }
+ }
+
+ // Audit report (HTML)
+ if (opts.auditReport) {
+ const sigil = loadSigil();
+ let attestation = null;
+ if (opts.attest) {
+ const r = selfCheckResult(sigil);
+ attestation = buildCanonicalAttestation({
+ sigil, canonical: r.canonical, org: opts.attestOrg, keyPath: opts.attestKey,
+ });
+ }
+ const html = renderHtmlReport({ result, sigil, attestation });
+ if (opts.output) {
+ writeFileSync(opts.output, html);
+ console.error(dim(`wrote ${opts.output}`));
+ } else {
+ console.log(html);
+ }
+ process.exit(result.valid ? 0 : exitCodeFor(result.error));
+ }
+
+ // Output
+ if (opts.json) {
+ const obj = JSON.parse(formatAsJson(result));
+ // Optionally attach emitted artifacts
+ if (opts.attest) {
+ const sigil = loadSigil();
+ const r = selfCheckResult(sigil);
+ obj.canonical_attestation = buildCanonicalAttestation({
+ sigil, canonical: r.canonical, org: opts.attestOrg, keyPath: opts.attestKey,
+ });
+ }
+ if (opts.emitVerificationReceipt && result.valid) {
+ const sigil = loadSigil();
+ obj.verification_receipt = buildVerificationReceipt({
+ subjectResult: result, sigil, keyPath: opts.attestKey,
+ });
+ }
+ console.log(JSON.stringify(obj, null, 2));
+ } else {
+ if (result.format === 'knowledge-unit') console.log(formatKuResult(result, opts));
+ else if (result.total !== undefined) console.log(formatBundleResult(result, opts));
+ else console.log(formatReceiptResult(result, opts));
+
+ // Surface attestation artifacts separately for terminal mode
+ if (opts.attest) {
+ const sigil = loadSigil();
+ const r = selfCheckResult(sigil);
+ const att = buildCanonicalAttestation({
+ sigil, canonical: r.canonical, org: opts.attestOrg, keyPath: opts.attestKey,
+ });
+ if (opts.output) {
+ writeFileSync(opts.output, JSON.stringify(att, null, 2));
+ console.error(dim(`wrote canonical attestation to ${opts.output}`));
+ } else {
+ console.log(`${bold('Canonical attestation:')}`);
+ console.log(JSON.stringify(att, null, 2));
+ console.log('');
+ }
+ }
+ if (opts.emitVerificationReceipt && result.valid) {
+ const sigil = loadSigil();
+ const vr = buildVerificationReceipt({ subjectResult: result, sigil, keyPath: opts.attestKey });
+ console.log(`${bold('Verification receipt:')}`);
+ console.log(JSON.stringify(vr, null, 2));
+ console.log('');
+ }
+ }
+
+ // Exit. A result flagged `_partial: true` has NOT been fully
+ // cryptographically verified (e.g. VOPRF tokens whose DLEQ proof
+ // check is structural-only in this release). By default we fail
+ // closed with exit code 2 (undecidable). Callers who explicitly
+ // want the partial result surfaced as "valid" can opt in with
+ // --allow-partial-voprf.
+ if (result.valid) {
+ if (result._partial && !opts.allowPartialVoprf) {
+ if (!opts.json) {
+ console.error(red(
+ '\n Exit 2: partial verification is not a full cryptographic check.\n' +
+ ' Pass --allow-partial-voprf to treat a partial result as valid.\n'
+ ));
+ }
+ process.exit(2);
+ }
+ process.exit(0);
+ }
+ process.exit(exitCodeFor(result.error));
+}
+
+main().catch((e) => {
+ console.error(red(`Fatal error: ${e.message}`));
+ if (process.env.DEBUG) console.error(e.stack);
+ process.exit(2);
+});
diff --git a/ecosystem/CONFORMANCE-CERTIFICATION.md b/ecosystem/CONFORMANCE-CERTIFICATION.md
new file mode 100644
index 0000000..9ad3538
--- /dev/null
+++ b/ecosystem/CONFORMANCE-CERTIFICATION.md
@@ -0,0 +1,117 @@
+# Conformance Certification — Commercial Service Design
+
+Draft spec for a commercial conformance certification service. Not an
+open-source artifact; this document is internal product spec for the
+ScopeBlind managed tier.
+
+## Problem
+
+Enterprise and regulated-industry buyers increasingly ask "is this
+implementation verified?" Self-certification via the open conformance
+test suite is acceptable for most; some buyers require independent
+certification.
+
+## Service outline
+
+**Customer:** an implementer (or consumer of an implementation) who
+needs formal third-party attestation that a specific release of an
+implementation passes conformance.
+
+**Workflow:**
+
+1. Customer submits an implementation release (artifact hash + repo
+ link) for certification.
+2. ScopeBlind runs the full conformance suite, including negative
+ vectors, against the submitted release.
+3. ScopeBlind manually reviews: dependency tree, threat model, test
+ coverage, cryptographic correctness spot-checks, build
+ reproducibility.
+4. ScopeBlind issues a signed "Conformance Certification" attestation
+ valid for 12 months, naming the implementation, tier, and review
+ summary.
+5. Certification is anchored in the public registry with a link to
+ the signed attestation (the attestation itself remains with the
+ customer; only the hash is public).
+
+## Certification attestation format
+
+```json
+{
+ "type": "veritasacta:conformance-certification",
+ "spec": "draft-farley-acta-signed-receipts-03",
+ "certified_implementation": {
+ "name": "acme-runtime",
+ "repo": "https://github.com/acme/acme-runtime",
+ "version": "1.2.0",
+ "artifact_hash": "sha256:abc123...",
+ "claimed_tier": 4
+ },
+ "certification": {
+ "tier_verified": 4,
+ "review_summary": "ACTA-CC-2026-0008",
+ "review_methodology": "Full conformance suite + manual cryptographic spot-check + dependency audit + threat model review",
+ "issued_at": "2026-07-15T12:00:00.000Z",
+ "expires_at": "2027-07-15T12:00:00.000Z",
+ "issuing_authority": "ScopeBlind (Veritas Acta)"
+ },
+ "signature": {
+ "alg": "EdDSA",
+ "kid": "scopeblind:certification:2026-q3",
+ "sig": ""
+ }
+}
+```
+
+## Pricing (draft)
+
+| Tier | Scope | Duration | Price (AUD) |
+|---|---|---|---|
+| Single release | One version of one implementation | 12 months | $2,000 |
+| Release train | Four versions over 12 months (auto-renewing) | 12 months | $6,000 |
+| Enterprise | Up to 10 implementations, rolling certifications | 12 months | $25,000 |
+| Custom | Specific jurisdictions / regulations (HIPAA, FedRAMP) | Negotiable | From $15,000 |
+
+Pricing is anchored to comparable certification markets (SOC 2 Type II
+audits from $25K, FIPS 140-3 validation from $50K). Veritas Acta
+certification is positioned as cheaper, faster, and more focused than
+those but backed by the specific implementation test suite rather than
+general security posture.
+
+## Delivery SLA
+
+- **Single release:** 5 business days review, written summary within 10
+ business days.
+- **Expedited review (+50% fee):** 2 business days.
+- **Enterprise tier:** named contact, quarterly review summary.
+
+## Marketing positioning
+
+- Certified implementations earn a "Certified by Veritas Acta"
+ registry entry with a distinctive badge.
+- Public announcement via a monthly "Certified This Month" blog post.
+- Joint case study opportunity for flagship certifications.
+
+## Operational requirements
+
+- Dedicated attester key (air-gapped HSM) for certification
+ signatures
+- Auditor team (initially 1-2 people; scale with demand)
+- Published methodology document and versioned test suite
+- Semi-annual methodology review + public changelog
+
+## Dependencies for GA
+
+- v0.5.0 shipped (✓)
+- Registry worker deployed (in progress; scaffold in
+ `ecosystem/registry-worker/`)
+- Badge service deployed (in progress; scaffold in
+ `ecosystem/badge-worker/`)
+- Methodology doc published
+- First-customer pilot (target: Q3 2026)
+
+## Not-yet-decided
+
+- Whether certifications appear in a Rekor-style transparency log
+- Whether certification revocation events are published publicly
+- Whether multi-tier certifications (e.g., T4 + specific FIPS
+ variants) require separate pricing
diff --git a/ecosystem/README.md b/ecosystem/README.md
new file mode 100644
index 0000000..f20aa5e
--- /dev/null
+++ b/ecosystem/README.md
@@ -0,0 +1,56 @@
+# Veritas Acta Ecosystem — Shipped and Planned Artifacts
+
+Beyond the verifier CLI itself, the `@veritasacta/verify` ecosystem
+includes adoption and distribution artifacts. This directory contains
+scaffolds, specs, and plans for each.
+
+## Shipped in v0.5.0
+
+| Artifact | Location | Description |
+|---|---|---|
+| CLI verifier | `packages/verify-cli/` | Unified offline verifier with `init` / `proxy` / `daemon` subcommands |
+| GitHub Action | `ecosystem/github-action/` | Drop-in verification step for any repo |
+| Claude Code plugin | `ecosystem/claude-code-plugin/` | One-click Claude Code install |
+| Homebrew formula | `ecosystem/homebrew-tap/` | `brew install veritasacta-verify` |
+| JS SDK | `ecosystem/sdk-js/` | `@veritasacta/sdk` tiny signing helper |
+| Python SDK | `ecosystem/sdk-py/` | `veritasacta-sdk` Python signing helper |
+| LangChain adapter | `ecosystem/adapters/langchain/` | Reference adapter (full implementation) |
+| 7 other framework adapters | `ecosystem/adapters/*/` | LangGraph, CrewAI, OpenAI Agents, Vercel AI, Smolagents, Pydantic AI, AutoGen |
+| Sigil naming doc | `ecosystem/SIGIL-NAMING.md` + `ecosystem/RELEASE-NAMING.md` | Public brand convention |
+
+## Scaffolded, deployment pending
+
+| Artifact | Location | Purpose | Blocker |
+|---|---|---|---|
+| Implementations registry | `ecosystem/registry-worker/` | `registry.veritasacta.com` mirror | Cloudflare Worker deployment |
+| Badge service | `ecosystem/badge-worker/` | `verify.veritasacta.com/badge/*` SVG badges | Cloudflare Worker deployment |
+| Interop leaderboard | `ecosystem/interop-leaderboard/` | Weekly cross-implementation CI | Workflow placement in registry repo |
+
+## Planned for v0.6.0 / v0.7.0 (design docs in place)
+
+| Artifact | Location | Notes |
+|---|---|---|
+| VS Code extension | `ecosystem/vscode-extension/` | Receipt editor support |
+| Cosign / Sigstore compat | `ecosystem/cosign-compat/DESIGN.md` | DSSE wrap + Rekor anchor (v0.6.0) |
+| Filesystem rollback | `ecosystem/rollback/DESIGN.md` | Content-addressed snapshots (nono-style) |
+| Runtime supervisor | `ecosystem/supervisor/DESIGN.md` | Dynamic permission expansion with approval |
+| Issuer reputation | `ecosystem/reputation/DESIGN.md` | Bayesian reputation over receipt issuers |
+| Audit dashboard GUI | `ecosystem/dashboard/DESIGN.md` | Web dashboard (Signet-style) |
+| Browser extension | `ecosystem/browser-extension/DESIGN.md` | Claude.ai / ChatGPT injection |
+| eBPF OS observer | `ecosystem/ebpf-observer/DESIGN.md` | Kernel-level auto-instrumentation |
+| Conformance certification | `ecosystem/CONFORMANCE-CERTIFICATION.md` | Commercial tier |
+
+## Deployment order (after v0.5.0 npm publish)
+
+1. GitHub Action released as `VeritasActa/verify-action@v1`
+2. Registry worker deployed at `registry.veritasacta.com`
+3. Badge worker deployed at `verify.veritasacta.com`
+4. Interop leaderboard workflow committed to `VeritasActa/agt-integration-profile`
+5. README updates propagated across implementation repos (sb-runtime,
+ protect-mcp, protect-mcp-adk) to embed badges
+6. VS Code extension shipped (v0.5.1)
+7. Cosign compatibility landed (v0.6.0)
+8. Certification service pilot launched (Q3 2026)
+
+Each numbered step is independent; they can happen in any order as
+bandwidth allows.
diff --git a/ecosystem/RELEASE-NAMING.md b/ecosystem/RELEASE-NAMING.md
new file mode 100644
index 0000000..5be69ea
--- /dev/null
+++ b/ecosystem/RELEASE-NAMING.md
@@ -0,0 +1,80 @@
+# Sigil Release Naming System
+
+Every release of `@veritasacta/verify` gets a unique Sigil with a
+deterministic name derived from its cryptographic fingerprint. The name
+is not chosen — it emerges from the release's own content.
+
+## How names are generated
+
+When `generate-sigil.mjs` produces a Sigil, it:
+
+1. Computes `sigil_hash = sha256("scopeblind:sigil:v2" || pubkey || policy_hash || nonce)`
+2. Takes `fingerprint = first 8 hex chars of sigil_hash`
+3. Parses `n = int(fingerprint[0:4], 16)`, `m = int(fingerprint[4:8], 16)`
+4. Selects `name = ADJECTIVE[n % 24] + " " + NOUN[m % 24]`
+
+This is **deterministic** — two verifier binaries with the same source
+hash and the same project key produce the same Sigil, same fingerprint,
+same name.
+
+## Why release names matter (brand mechanism)
+
+1. **Memorable.** "We pinned to Swift Wind" is easier than "pinned to
+ Sigil fingerprint 87727f4b."
+2. **Visible in release notes.** Every announcement leads with the name.
+3. **Counterfeit detection.** A fork produces a DIFFERENT Sigil →
+ different name. Users see at a glance.
+4. **Marketing contagion.** Release names become conversational shorthand.
+
+## Pool
+
+24 adjectives × 24 nouns = 576 unique name combinations. Collision-averse
+for ~100 releases; rare collisions can be disambiguated by version.
+
+### Adjectives
+
+Bright · Quiet · Deep · Bold · Pale · Warm · Still · Swift · Clear ·
+Dark · First · True · Slow · Fair · Old · New · Gilded · Woven · Open ·
+High · Lone · Kind · Keen · Wild
+
+### Nouns
+
+Ember · Harbor · Field · Beacon · River · Grove · Arrow · Stone ·
+Ridge · Wind · Tide · Star · Vale · Peak · Lake · Dawn · Reed · Cairn ·
+Orchard · Meadow · Hearth · Anchor · Vessel · Thread
+
+## Historical Sigil registry
+
+| Version | Sigil name | Fingerprint | Released | Highlight |
+|---|---|---|---|---|
+| 0.3.0 | Slow Reed | `dd0443f0` | 2026-04-13 | First Sigil-attested release |
+| 0.4.0 | Slow Cairn | `e6647ab1` | 2026-04-19 | Embedded-key rejection |
+| 0.5.0 | (current) | (current) | 2026-04-19 | Unified verifier |
+
+After each release, this table is updated. The canonical live registry
+is at `https://veritasacta.com/sigils`.
+
+## Marketing cadence
+
+- **Release announcement**: leads with the Sigil name. "Swift Wind is
+ live."
+- **Monthly "Sigil of the Month" blog post**: features the current
+ release, technical changes, and a case study from any org that
+ canonically attested that version.
+- **Merchandise**: T-shirts, stickers, print art — each release can
+ generate its own limited-edition asset. Optional but reinforces
+ identity.
+
+## Naming collisions
+
+If two versions produce the same name (expected ~1 per 24 releases on
+average), disambiguate by version number: "Swift Wind 0.5.0" vs. "Swift
+Wind 0.7.0". Named Sigils are still cryptographically distinct via
+fingerprint.
+
+## Related
+
+- `ecosystem/SIGIL-NAMING.md` — this file
+- `generate-sigil.mjs` — derivation code
+- `src/engines/sigil.js` — runtime verification
+- Patent provisional #5 — Sigil visual commitment
diff --git a/ecosystem/SIGIL-NAMING.md b/ecosystem/SIGIL-NAMING.md
new file mode 100644
index 0000000..fc7c9c5
--- /dev/null
+++ b/ecosystem/SIGIL-NAMING.md
@@ -0,0 +1,69 @@
+# Veritas Acta Sigil Naming Convention
+
+Every release of `@veritasacta/verify` receives a unique Sigil — a
+visual cryptographic commitment to the binary. Each Sigil has a
+deterministic name derived from its fingerprint.
+
+## How names are generated
+
+The `generate-sigil.mjs` script produces a Sigil after the source code
+is frozen. It derives:
+
+- A SHA-256 `sigil_hash` from `(project_public_key, policy_hash, nonce)`
+- A `fingerprint` = first 8 hex characters of the sigil hash
+- A `name` = two-word label derived from the fingerprint
+
+Name derivation:
+
+```
+n = parseInt(fingerprint[0:4], 16)
+m = parseInt(fingerprint[4:8], 16)
+name = NAME_ADJ[n % 24] + " " + NAME_NOUN[m % 24]
+```
+
+The 24×24 = 576 possible names keep the namespace collision-averse
+enough for ~100 releases. If a collision ever occurs, the older name
+retains priority in public comms.
+
+## Adjective pool (24)
+
+Bright · Quiet · Deep · Bold · Pale · Warm · Still · Swift · Clear ·
+Dark · First · True · Slow · Fair · Old · New · Gilded · Woven · Open ·
+High · Lone · Kind · Keen · Wild
+
+## Noun pool (24)
+
+Ember · Harbor · Field · Beacon · River · Grove · Arrow · Stone ·
+Ridge · Wind · Tide · Star · Vale · Peak · Lake · Dawn · Reed · Cairn ·
+Orchard · Meadow · Hearth · Anchor · Vessel · Thread
+
+## Why names matter (brand convention)
+
+1. **Memorability.** "We're pinned to New Wind" is easier to reason
+ about than "5247a989".
+2. **Visibility.** Release announcements lead with the name. The
+ fingerprint is the precise handle; the name is the social handle.
+3. **Non-interchangeability.** A fork that produces its own Sigil gets
+ a DIFFERENT name. Users can see at a glance whether the installed
+ verifier is the canonical release.
+4. **Longevity.** Names compose with version numbers: "Swift Wind
+ 0.5.0" vs "New Wind 0.5.0" signals which Sigil commitment is
+ active.
+
+## Historical Sigil registry
+
+| Version | Sigil name | Fingerprint | Released | Notes |
+|---|---|---|---|---|
+| 0.3.0 | Slow Reed | dd0443f0 | 2026-04-13 | First Sigil-attested release |
+| 0.4.0 | Slow Cairn | e6647ab1 | 2026-04-19 | Embedded-key rejection |
+| 0.5.0 | (pending) | (TBD) | (pending) | Unified verifier |
+
+Future releases add to this table as they ship. The canonical registry
+lives at `https://veritasacta.com/sigils` once the badge service is
+deployed.
+
+## Related
+
+- `packages/verify-cli/generate-sigil.mjs` — derivation code
+- `packages/verify-cli/sigil.json` — current commitment
+- `patents/filed/provisional-5/` — Sigil patent claims
diff --git a/ecosystem/adapters/autogen/README.md b/ecosystem/adapters/autogen/README.md
new file mode 100644
index 0000000..e070245
--- /dev/null
+++ b/ecosystem/adapters/autogen/README.md
@@ -0,0 +1,32 @@
+# veritasacta-autogen
+
+Microsoft AutoGen adapter for Veritas Acta signed receipts.
+
+## Status
+
+v0.1.0 scaffold.
+
+## Install
+
+```bash
+pip install veritasacta-autogen veritasacta-sdk
+```
+
+## Usage
+
+```python
+from autogen_agentchat.agents import AssistantAgent
+from veritasacta_sdk import Signer
+from veritasacta_autogen import attach_receipts
+
+signer = Signer.from_key_file(".veritasacta/attester.json")
+
+agent = AssistantAgent(name="assistant", tools=[...])
+attach_receipts(agent, signer=signer, policy_id="chat-v1")
+
+await agent.run(task="...")
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/crewai/README.md b/ecosystem/adapters/crewai/README.md
new file mode 100644
index 0000000..c6695e2
--- /dev/null
+++ b/ecosystem/adapters/crewai/README.md
@@ -0,0 +1,41 @@
+# veritasacta-crewai
+
+CrewAI adapter for Veritas Acta signed receipts. Wraps Task / Agent
+tool invocations in CrewAI so every tool call emits a signed receipt.
+
+## Status
+
+v0.1.0 scaffold. Intended for users running CrewAI crews who want
+independently verifiable evidence of agent decisions.
+
+## Install
+
+```bash
+pip install veritasacta-crewai veritasacta-sdk
+```
+
+## Usage
+
+```python
+from crewai import Agent, Task, Crew
+from veritasacta_sdk import Signer
+from veritasacta_crewai import attach_receipts
+
+signer = Signer.from_key_file(".veritasacta/attester.json")
+
+crew = Crew(agents=[...], tasks=[...])
+attach_receipts(crew, signer=signer, policy_id="research-v1", receipts_dir=".veritasacta/receipts")
+
+result = crew.kickoff()
+# Every task/tool invocation produced a receipt under .veritasacta/receipts
+```
+
+## Verification
+
+```bash
+npx @veritasacta/verify .veritasacta/receipts/*.json --key
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/langchain/README.md b/ecosystem/adapters/langchain/README.md
new file mode 100644
index 0000000..591f23e
--- /dev/null
+++ b/ecosystem/adapters/langchain/README.md
@@ -0,0 +1,98 @@
+# @veritasacta/langchain
+
+LangChain / LangGraph adapter for Veritas Acta signed decision receipts.
+
+**Apache-2.0 · JS + Python · Runs alongside any LangChain agent**
+
+## Install
+
+```bash
+# JavaScript / TypeScript
+npm install @veritasacta/langchain @veritasacta/sdk
+
+# Python
+pip install veritasacta-langchain veritasacta-sdk
+```
+
+## Usage — JavaScript
+
+```ts
+import { ChatOpenAI } from '@langchain/openai';
+import { AgentExecutor, createReactAgent } from 'langchain/agents';
+import { Signer } from '@veritasacta/sdk';
+import { withReceipts } from '@veritasacta/langchain';
+
+const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+
+const agent = createReactAgent({
+ llm: new ChatOpenAI(),
+ tools,
+});
+
+// Wrap any AgentExecutor to emit a signed receipt on every tool call
+const auditedAgent = withReceipts(agent, {
+ signer,
+ policyId: 'research-read-only-v1',
+ receiptsDir: '.veritasacta/receipts',
+});
+
+const result = await auditedAgent.invoke({ input: 'What happened this week?' });
+// Each tool call produced a signed receipt under .veritasacta/receipts/
+```
+
+## Usage — Python
+
+```python
+from langchain_openai import ChatOpenAI
+from langchain.agents import AgentExecutor, create_react_agent
+from veritasacta_sdk import Signer
+from veritasacta_langchain import with_receipts
+
+signer = Signer.from_key_file(".veritasacta/attester.json")
+
+agent = create_react_agent(llm=ChatOpenAI(), tools=tools)
+
+audited = with_receipts(
+ agent,
+ signer=signer,
+ policy_id="research-read-only-v1",
+ receipts_dir=".veritasacta/receipts",
+)
+
+result = audited.invoke({"input": "What happened this week?"})
+```
+
+## How it works
+
+`withReceipts` / `with_receipts` wraps LangChain's tool-invocation path
+(via the `on_tool_start` / `on_tool_end` callbacks) and calls
+`signer.signDecision({...})` for each tool invocation. The returned
+receipt is persisted to the configured receipts directory; verification
+happens later with `npx @veritasacta/verify`.
+
+## Field mapping
+
+| LangChain concept | Veritas Acta field |
+|---|---|
+| Tool name | `tool_name` |
+| Tool arguments (hashed) | `tool_input_hash` |
+| Output (hashed, optional) | `output_hash` |
+| Policy label | `policy_id` |
+| Agent identity | `agent_id` |
+| Chain link | `previousReceiptHash` |
+
+## Verifying
+
+```bash
+npx @veritasacta/verify .veritasacta/receipts/*.json --key
+```
+
+## Related
+
+- [@veritasacta/verify](https://www.npmjs.com/package/@veritasacta/verify) — the verifier
+- [@veritasacta/sdk](https://www.npmjs.com/package/@veritasacta/sdk) — the underlying signer
+- [Veritas Acta protocol](https://veritasacta.com)
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/langchain/package.json b/ecosystem/adapters/langchain/package.json
new file mode 100644
index 0000000..06504dc
--- /dev/null
+++ b/ecosystem/adapters/langchain/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@veritasacta/langchain",
+ "version": "0.1.0",
+ "description": "LangChain / LangGraph adapter for Veritas Acta signed decision receipts.",
+ "license": "Apache-2.0",
+ "type": "module",
+ "main": "src/index.js",
+ "files": ["src/", "README.md"],
+ "peerDependencies": {
+ "@veritasacta/sdk": "^0.1.0",
+ "@langchain/core": ">=0.1.0"
+ },
+ "dependencies": {},
+ "engines": { "node": ">=18.0.0" },
+ "keywords": ["langchain", "langgraph", "veritasacta", "receipts"]
+}
diff --git a/ecosystem/adapters/langchain/src/index.js b/ecosystem/adapters/langchain/src/index.js
new file mode 100644
index 0000000..b8651ac
--- /dev/null
+++ b/ecosystem/adapters/langchain/src/index.js
@@ -0,0 +1,99 @@
+/**
+ * @veritasacta/langchain — LangChain adapter for signed receipts.
+ *
+ * Wraps a LangChain AgentExecutor (or any runnable with tool
+ * invocations) by attaching callback handlers that emit Veritas Acta
+ * receipts on every tool call.
+ *
+ * @license Apache-2.0
+ */
+
+import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+
+class ReceiptsCallbackHandler {
+ constructor({ signer, policyId, receiptsDir, agentId }) {
+ this.signer = signer;
+ this.policyId = policyId;
+ this.receiptsDir = receiptsDir;
+ this.agentId = agentId;
+ if (!existsSync(receiptsDir)) mkdirSync(receiptsDir, { recursive: true });
+ }
+
+ async handleToolStart(tool, input) {
+ // Cache for use at end
+ this._pending = { tool, input, startedAt: Date.now() };
+ }
+
+ async handleToolEnd(output) {
+ if (!this._pending) return;
+ const { tool, input } = this._pending;
+ const toolName = tool?.name || tool?.lc_id || 'unknown-tool';
+
+ const receipt = this.signer.signDecision({
+ tool: toolName,
+ args: typeof input === 'string' ? { input } : input,
+ decision: 'allow',
+ policy_id: this.policyId,
+ metadata: this.agentId ? { agent_id: this.agentId } : undefined,
+ });
+
+ const filename = `rcpt_${String(this.signer.sequence).padStart(6, '0')}.json`;
+ writeFileSync(join(this.receiptsDir, filename), JSON.stringify(receipt, null, 2));
+ this._pending = null;
+ }
+
+ async handleToolError(err) {
+ if (!this._pending) return;
+ const { tool, input } = this._pending;
+ const toolName = tool?.name || 'unknown-tool';
+ const receipt = this.signer.signDecision({
+ tool: toolName,
+ args: typeof input === 'string' ? { input } : input,
+ decision: 'deny',
+ policy_id: this.policyId,
+ metadata: { error: err?.message || 'unknown' },
+ });
+ const filename = `rcpt_${String(this.signer.sequence).padStart(6, '0')}.json`;
+ writeFileSync(join(this.receiptsDir, filename), JSON.stringify(receipt, null, 2));
+ this._pending = null;
+ }
+}
+
+/**
+ * Wrap a LangChain AgentExecutor or Runnable with receipt emission.
+ *
+ * @param {Object} agent LangChain agent / runnable
+ * @param {Object} opts
+ * @param {Object} opts.signer @veritasacta/sdk Signer instance
+ * @param {string} [opts.policyId]
+ * @param {string} [opts.receiptsDir='.veritasacta/receipts']
+ * @param {string} [opts.agentId]
+ * @returns {Object} wrapped agent
+ */
+export function withReceipts(agent, opts) {
+ const handler = new ReceiptsCallbackHandler({
+ signer: opts.signer,
+ policyId: opts.policyId || 'veritasacta:langchain:default',
+ receiptsDir: opts.receiptsDir || '.veritasacta/receipts',
+ agentId: opts.agentId,
+ });
+
+ // LangChain lets us attach callbacks via .withConfig / .bind / invoke({callbacks})
+ // The wrap is intentionally minimal: any place that accepts a callbacks list
+ // will fire our handlers.
+ const original = agent.invoke ? agent.invoke.bind(agent) : null;
+ if (!original) {
+ throw new Error('withReceipts: agent does not expose .invoke(); wrap a different object.');
+ }
+
+ return {
+ ...agent,
+ invoke(input, config = {}) {
+ const callbacks = [...(config.callbacks || []), handler];
+ return original(input, { ...config, callbacks });
+ },
+ };
+}
+
+export { ReceiptsCallbackHandler };
diff --git a/ecosystem/adapters/langgraph/README.md b/ecosystem/adapters/langgraph/README.md
new file mode 100644
index 0000000..5cc5fec
--- /dev/null
+++ b/ecosystem/adapters/langgraph/README.md
@@ -0,0 +1,33 @@
+# @veritasacta/langgraph
+
+LangGraph adapter for Veritas Acta signed receipts. Built on `@veritasacta/langchain`; exposes `withGraphReceipts()` that attaches receipt callbacks to every node execution in the graph.
+
+## Status
+
+v0.1.0 scaffold. Full implementation tracked alongside the LangChain adapter.
+
+## Install
+
+```bash
+npm install @veritasacta/langgraph @veritasacta/sdk
+```
+
+## Usage
+
+```ts
+import { StateGraph } from '@langchain/langgraph';
+import { Signer } from '@veritasacta/sdk';
+import { withGraphReceipts } from '@veritasacta/langgraph';
+
+const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+
+const graph = new StateGraph(...).compile();
+const audited = withGraphReceipts(graph, { signer, policyId: 'graph-v1' });
+
+await audited.invoke({ input: '...' });
+// Each node invocation emits a receipt.
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/openai-agents/README.md b/ecosystem/adapters/openai-agents/README.md
new file mode 100644
index 0000000..809117d
--- /dev/null
+++ b/ecosystem/adapters/openai-agents/README.md
@@ -0,0 +1,45 @@
+# @veritasacta/openai-agents
+
+OpenAI Agents SDK adapter for Veritas Acta signed receipts.
+
+## Status
+
+v0.1.0 scaffold. Composes with the [on_tool_authorize hook proposal
+(openai/openai-agents-python#2868)](https://github.com/openai/openai-agents-python/issues/2868).
+
+## Install
+
+```bash
+# JS
+npm install @veritasacta/openai-agents @veritasacta/sdk
+
+# Python
+pip install veritasacta-openai-agents veritasacta-sdk
+```
+
+## Usage
+
+```ts
+import { Agent } from '@openai/agents';
+import { Signer } from '@veritasacta/sdk';
+import { attachReceipts } from '@veritasacta/openai-agents';
+
+const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+
+const agent = new Agent({
+ name: 'researcher',
+ tools: [...],
+});
+
+attachReceipts(agent, {
+ signer,
+ policyId: 'research-v1',
+ receiptsDir: '.veritasacta/receipts',
+});
+
+await agent.run({ input: '...' });
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/pydantic-ai/README.md b/ecosystem/adapters/pydantic-ai/README.md
new file mode 100644
index 0000000..fe143a2
--- /dev/null
+++ b/ecosystem/adapters/pydantic-ai/README.md
@@ -0,0 +1,37 @@
+# veritasacta-pydantic-ai
+
+Pydantic AI adapter for Veritas Acta signed receipts. Wraps `Agent.tool`
+decorators with signing callbacks.
+
+## Status
+
+v0.1.0 scaffold.
+
+## Install
+
+```bash
+pip install veritasacta-pydantic-ai veritasacta-sdk
+```
+
+## Usage
+
+```python
+from pydantic_ai import Agent
+from veritasacta_sdk import Signer
+from veritasacta_pydantic_ai import with_receipts
+
+signer = Signer.from_key_file(".veritasacta/attester.json")
+
+agent = Agent("openai:gpt-4o")
+with_receipts(agent, signer=signer, policy_id="chat-v1")
+
+@agent.tool
+def my_tool(ctx, x: int) -> int:
+ return x * 2
+
+result = agent.run_sync("...")
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/smolagents/README.md b/ecosystem/adapters/smolagents/README.md
new file mode 100644
index 0000000..92d4ece
--- /dev/null
+++ b/ecosystem/adapters/smolagents/README.md
@@ -0,0 +1,34 @@
+# veritasacta-smolagents
+
+Smolagents (HuggingFace) adapter for Veritas Acta signed receipts.
+
+## Status
+
+v0.1.0 scaffold.
+
+## Install
+
+```bash
+pip install veritasacta-smolagents veritasacta-sdk
+```
+
+## Usage
+
+```python
+from smolagents import CodeAgent
+from veritasacta_sdk import Signer
+from veritasacta_smolagents import ReceiptHook
+
+signer = Signer.from_key_file(".veritasacta/attester.json")
+
+agent = CodeAgent(
+ tools=[...],
+ hooks=[ReceiptHook(signer, policy_id="research-v1")],
+)
+
+agent.run("...")
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/adapters/swarms/.gitignore b/ecosystem/adapters/swarms/.gitignore
new file mode 100644
index 0000000..6dce3c8
--- /dev/null
+++ b/ecosystem/adapters/swarms/.gitignore
@@ -0,0 +1,5 @@
+__pycache__/
+*.pyc
+dist/
+build/
+*.egg-info/
diff --git a/ecosystem/adapters/swarms/README.md b/ecosystem/adapters/swarms/README.md
new file mode 100644
index 0000000..f5aa993
--- /dev/null
+++ b/ecosystem/adapters/swarms/README.md
@@ -0,0 +1,154 @@
+# scopeblind-swarms
+
+Ed25519 signed decision receipts for [kyegomez/swarms](https://github.com/kyegomez/swarms) multi-agent systems. Wrap any tool with tamper-evident, offline-verifiable receipts in the [Veritas Acta receipt format](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) — the same format used by Microsoft Agent Governance Toolkit, protect-mcp, sb-runtime, hermes-decision-receipts, and Signet.
+
+**MIT · Python · Runs alongside any Swarms agent · No fork of Swarms required**
+
+## Why this exists
+
+Swarms orchestrates agents that call real-world tools. When things go wrong, operators need cryptographic evidence of *which decisions were made by which agent under which policy*. `scopeblind-swarms` emits an Ed25519-signed, chain-linked receipt for every governed tool call. Receipts verify offline with `npx @veritasacta/verify` against a public key the operator publishes out-of-band.
+
+No dependency on ScopeBlind infrastructure. No vendor lock-in. The receipt format is an open IETF Internet-Draft.
+
+## Install
+
+```bash
+pip install scopeblind-swarms
+# plus swarms itself:
+pip install swarms
+```
+
+## Quick start
+
+```python
+from swarms import Agent
+from scopeblind_swarms import ReceiptChain, sign_tool
+
+# 1. Create a receipt chain. One per agent (or per session).
+chain = ReceiptChain.from_key_file(
+ signer_key_path="/etc/scopeblind/issuer.key",
+ agent_id="did:swarms:researcher",
+ policy_id="allow-web-read",
+)
+
+# 2. Define tools normally.
+def web_search(query: str) -> str:
+ # ... your search implementation
+ return f"results for {query}"
+
+# 3. Wrap tools with signed-receipt emission.
+signed_search = sign_tool(web_search, chain=chain)
+
+# 4. Use the wrapped tool in an Agent.
+agent = Agent(
+ agent_name="researcher",
+ tools=[signed_search],
+ model_name="gpt-4",
+ # ... your other Agent config
+)
+agent.run("find recent papers on agent governance")
+
+# Every tool invocation produced a signed receipt. Access the most
+# recent one from the wrapped tool:
+print(signed_search.last_receipt)
+
+# Or iterate the chain:
+print(f"Chain tip hash: {chain.current_tip}")
+```
+
+## Bulk wrapping
+
+```python
+from scopeblind_swarms import sign_tools
+
+agent = Agent(
+ agent_name="researcher",
+ tools=sign_tools([web_search, summarize, post_draft], chain=chain),
+ ...,
+)
+```
+
+## Decorator form
+
+```python
+from scopeblind_swarms import ReceiptChain, sign_tool
+
+chain = ReceiptChain.from_key_file("/etc/scopeblind/issuer.key", agent_id="did:swarms:writer")
+
+@sign_tool(chain=chain, policy_id="allow-draft-only")
+def post_draft(content: str) -> str:
+ return f"drafted: {content[:50]}..."
+```
+
+## What's in a receipt
+
+```json
+{
+ "payload": {
+ "type": "scopeblind:swarms:tool-call",
+ "agent_id": "did:swarms:researcher",
+ "issuer_id": "swarms:agent:HJY4k2aN",
+ "tool_name": "web_search",
+ "action": "swarms:tool:web_search",
+ "action_ref": "sha256:a8f3...c91e",
+ "decision": "allow",
+ "policy_id": "allow-web-read",
+ "result_hash": "sha256:4b2c...d71f",
+ "issued_at": "2026-04-19T15:42:01.773Z",
+ "previousReceiptHash": "Zk4p..."
+ },
+ "signature": {
+ "alg": "EdDSA",
+ "kid": "HJY4k2aNqRwXcdEfGh...",
+ "sig": "..."
+ }
+}
+```
+
+Fields:
+
+- `action_ref` — SHA-256 of the JCS-canonicalized tool arguments. Agents can cross-correlate the same tool invocation across engines.
+- `result_hash` — SHA-256 of the tool return value. The receipt attests to what the tool returned without carrying raw output (privacy default).
+- `previousReceiptHash` — SHA-256 of the prior receipt in this chain. Successive tool calls form a tamper-evident chain.
+- `policy_id`, `policy_digest` — bound into every receipt so an auditor can confirm which policy the agent was operating under.
+
+## Offline verification
+
+Any receipt verifies with `@veritasacta/verify`, with no dependency on `scopeblind-swarms` or Swarms:
+
+```bash
+npx @veritasacta/verify receipt.json --key operator-public.pem
+```
+
+Exit code `0` is proven valid; exit `1` is proven tampering; exit `2` is undecidable (malformed, missing key).
+
+Your operator public key is published out-of-band (JWKS URL, DID document service endpoint, pinned trust anchor, or GitHub-backed SBOM artifact). Never embedded in the receipt. Per draft-farley-acta-signed-receipts-02 §9.
+
+## Design notes
+
+- **Swarms extension point.** Swarms does not expose pre/post tool hooks; the canonical interception point is wrapping the tool callable before passing it to `Agent(tools=[...])`. `scopeblind-swarms` uses that extension point exactly; it does not patch Swarms' internals.
+- **Thread safety.** A `ReceiptChain` is safe to share across parallel tool invocations within a single agent (async Swarms agents often run tools concurrently). Chain integrity is preserved via an internal lock.
+- **No kernel sandboxing.** This adapter covers the receipts layer. For kernel-level agent isolation, compose with [`sb-runtime`](https://github.com/ScopeBlind/sb-runtime) (Landlock + seccomp) or [nono](https://github.com/always-further/nono).
+- **No embedded keys.** `sign_receipt` and `verify_receipt` reject any payload carrying `verification_key`, `issuer_key`, or `signer_public_key`. Fail-closed posture matches the rest of the ecosystem after the April 2026 desiorac stress-test.
+
+## Policy evaluation
+
+`scopeblind-swarms` doesn't ship a Cedar evaluator. If you want Cedar policy enforcement, pair with one of:
+
+- [`bindu-scopeblind`](https://github.com/ScopeBlind/bindu-scopeblind) — Python Cedar extension
+- [`sb-runtime`](https://github.com/ScopeBlind/sb-runtime) — Rust binary with Cedar + sandbox + receipts
+- Direct use of `cedarpy` in your `decision` computation before calling `sign_tool`
+
+The `decision` field in every receipt accepts `"allow" | "deny" | "require_approval"`; you can vary it per call from your own policy evaluation logic.
+
+## Related
+
+- **Microsoft Agent Governance Toolkit** — [docs/integrations/sb-runtime.md](https://github.com/microsoft/agent-governance-toolkit/blob/main/docs/integrations/sb-runtime.md) (same receipt format)
+- **Reference verifier** — [`@veritasacta/verify`](https://github.com/ScopeBlind/verify) (Apache-2.0, offline)
+- **IETF draft** — [draft-farley-acta-signed-receipts-02](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
+- **Conformance profile** — [VeritasActa/agt-integration-profile](https://github.com/VeritasActa/agt-integration-profile)
+- **Other framework adapters** — LangChain, CrewAI, OpenAI Agents SDK, Vercel AI SDK, Smolagents, Pydantic AI, AutoGen, LangGraph (all in the same ecosystem)
+
+## License
+
+MIT.
diff --git a/ecosystem/adapters/swarms/pyproject.toml b/ecosystem/adapters/swarms/pyproject.toml
new file mode 100644
index 0000000..a1303ce
--- /dev/null
+++ b/ecosystem/adapters/swarms/pyproject.toml
@@ -0,0 +1,32 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "scopeblind-swarms"
+version = "0.1.0"
+description = "Ed25519 signed decision receipts for kyegomez/swarms multi-agent systems. Wrap any tool in a Swarms agent with tamper-evident, offline-verifiable receipts in the Veritas Acta format (IETF draft-farley-acta-signed-receipts)."
+readme = "README.md"
+license = {text = "MIT"}
+requires-python = ">=3.10"
+authors = [{ name = "Tom Farley (ScopeBlind)", email = "tommy@scopeblind.com" }]
+keywords = ["swarms", "scopeblind", "veritas-acta", "receipts", "ed25519", "agent-governance", "kyegomez"]
+dependencies = [
+ "cryptography>=41.0,<47.0",
+]
+
+[project.optional-dependencies]
+swarms = ["swarms>=7.0"]
+dev = ["pytest>=7.0"]
+
+[project.urls]
+Homepage = "https://github.com/ScopeBlind/scopeblind-gateway"
+"Swarms" = "https://github.com/kyegomez/swarms"
+"Veritas Acta Draft" = "https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/"
+"Reference Verifier" = "https://www.npmjs.com/package/@veritasacta/verify"
+
+[tool.hatch.build.targets.wheel]
+packages = ["scopeblind_swarms"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
diff --git a/ecosystem/adapters/swarms/scopeblind_swarms/__init__.py b/ecosystem/adapters/swarms/scopeblind_swarms/__init__.py
new file mode 100644
index 0000000..b6b8586
--- /dev/null
+++ b/ecosystem/adapters/swarms/scopeblind_swarms/__init__.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2026 Tom Farley (ScopeBlind).
+# Licensed under the MIT License.
+"""scopeblind-swarms: Ed25519 signed decision receipts for Swarms agents.
+
+Wraps any Python callable used as a Swarms tool with tamper-evident
+receipt emission. Matches the receipt format specified by
+draft-farley-acta-signed-receipts (Veritas Acta). Receipts verify
+offline against @veritasacta/verify without Swarms or scopeblind-swarms
+installed on the verifier side.
+
+Typical usage:
+
+ from swarms import Agent
+ from scopeblind_swarms import ReceiptChain, sign_tool
+
+ chain = ReceiptChain(signer_key_path="/etc/scopeblind/issuer.key",
+ agent_id="did:swarms:researcher-1")
+
+ def web_search(query: str) -> str:
+ ...
+
+ signed_web_search = sign_tool(web_search, chain=chain, policy_id="allow-web-read")
+
+ agent = Agent(
+ agent_name="researcher",
+ tools=[signed_web_search],
+ ...,
+ )
+
+Every invocation of the wrapped tool produces a signed receipt. The
+chain binds successive receipts via `previousReceiptHash` so tampering
+is detectable across the whole session.
+"""
+
+from scopeblind_swarms.chain import ReceiptChain
+from scopeblind_swarms.receipts import (
+ Signer,
+ receipt_hash,
+ sign_receipt,
+ verify_receipt,
+)
+from scopeblind_swarms.tools import SignedToolResult, sign_tool, sign_tools
+
+__all__ = [
+ "ReceiptChain",
+ "SignedToolResult",
+ "Signer",
+ "receipt_hash",
+ "sign_receipt",
+ "sign_tool",
+ "sign_tools",
+ "verify_receipt",
+]
+
+__version__ = "0.1.0"
diff --git a/ecosystem/adapters/swarms/scopeblind_swarms/chain.py b/ecosystem/adapters/swarms/scopeblind_swarms/chain.py
new file mode 100644
index 0000000..d45188c
--- /dev/null
+++ b/ecosystem/adapters/swarms/scopeblind_swarms/chain.py
@@ -0,0 +1,167 @@
+# Copyright (c) 2026 Tom Farley (ScopeBlind).
+# Licensed under the MIT License.
+"""ReceiptChain: per-agent session receipt signer with chain linkage.
+
+A Swarms agent typically makes many tool calls in a single run. A
+ReceiptChain keeps the signer + agent identity + policy context stable
+across those calls, tracking previous-receipt-hash internally so every
+successive receipt chains to the prior one.
+
+Instantiate once per agent (or once per session); pass to `sign_tool`
+to wrap each tool with signed emission.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import threading
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any, Mapping, Optional
+
+from scopeblind_swarms.receipts import (
+ Signer,
+ receipt_hash,
+ sign_receipt,
+)
+
+
+@dataclass
+class ReceiptChain:
+ """Per-agent signing chain.
+
+ Thread-safe for concurrent tool calls within the same agent
+ (async Swarms agents call tools in parallel).
+ """
+
+ signer: Signer
+ agent_id: str
+ issuer_id: Optional[str] = None
+ policy_id: Optional[str] = None
+ policy_digest: Optional[str] = None
+ session_id: Optional[str] = None
+
+ _previous_hash: Optional[str] = field(default=None, init=False, repr=False)
+ _lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False)
+
+ @classmethod
+ def from_key_file(
+ cls,
+ signer_key_path: str | Path,
+ agent_id: str,
+ *,
+ policy_id: Optional[str] = None,
+ policy_digest: Optional[str] = None,
+ session_id: Optional[str] = None,
+ issuer_id: Optional[str] = None,
+ ) -> "ReceiptChain":
+ signer = Signer.from_pem_file(str(signer_key_path))
+ return cls(
+ signer=signer,
+ agent_id=agent_id,
+ issuer_id=issuer_id or f"swarms:agent:{signer.kid[:12]}",
+ policy_id=policy_id,
+ policy_digest=policy_digest,
+ session_id=session_id,
+ )
+
+ @classmethod
+ def generate(
+ cls,
+ agent_id: str,
+ *,
+ policy_id: Optional[str] = None,
+ policy_digest: Optional[str] = None,
+ session_id: Optional[str] = None,
+ ) -> "ReceiptChain":
+ """Convenience factory for tests / ephemeral deployments.
+
+ Generates a fresh in-memory Ed25519 key. Not suitable for
+ production (no key rotation, no persistence); use
+ ``from_key_file`` with an operator-managed PEM for that.
+ """
+ signer = Signer.generate()
+ return cls(
+ signer=signer,
+ agent_id=agent_id,
+ issuer_id=f"swarms:agent:{signer.kid[:12]}",
+ policy_id=policy_id,
+ policy_digest=policy_digest,
+ session_id=session_id,
+ )
+
+ def sign_tool_call(
+ self,
+ *,
+ tool_name: str,
+ tool_args: Mapping[str, Any],
+ tool_result_hash: Optional[str] = None,
+ decision: str = "allow",
+ policy_id: Optional[str] = None,
+ extra: Optional[Mapping[str, Any]] = None,
+ ) -> dict:
+ """Sign a receipt for a single tool invocation.
+
+ Args are hashed (not carried raw) per AIP-0001 privacy defaults.
+ The receipt payload records the tool name, args hash, optional
+ result hash, policy digest, decision, and chain linkage.
+ """
+ args_canonical_hash = _hash_json(tool_args)
+
+ payload: dict[str, Any] = {
+ "type": "scopeblind:swarms:tool-call",
+ "agent_id": self.agent_id,
+ "issuer_id": self.issuer_id or self.agent_id,
+ "tool_name": tool_name,
+ "action": f"swarms:tool:{tool_name}",
+ "action_ref": f"sha256:{args_canonical_hash}",
+ "decision": decision,
+ }
+ if self.policy_id or policy_id:
+ payload["policy_id"] = policy_id or self.policy_id
+ if self.policy_digest:
+ payload["policy_digest"] = self.policy_digest
+ if self.session_id:
+ payload["iteration_id"] = self.session_id
+ if tool_result_hash:
+ payload["result_hash"] = tool_result_hash
+ if extra:
+ payload.update(dict(extra))
+
+ with self._lock:
+ envelope = sign_receipt(
+ payload=payload,
+ signer=self.signer,
+ previous_receipt_hash=self._previous_hash,
+ )
+ self._previous_hash = receipt_hash(envelope)
+
+ return envelope
+
+ def reset_chain(self) -> None:
+ """Clear the previous-hash pointer.
+
+ Call at session boundaries if you want a fresh chain. Does not
+ affect the signing key.
+ """
+ with self._lock:
+ self._previous_hash = None
+
+ @property
+ def current_tip(self) -> Optional[str]:
+ """Hash of the most recent receipt, or None if chain empty."""
+ return self._previous_hash
+
+
+def _hash_json(obj: Mapping[str, Any]) -> str:
+ """Deterministic SHA-256 of JCS-canonical JSON, hex-encoded."""
+ import json as _json
+
+ canonical = _json.dumps(
+ obj,
+ sort_keys=True,
+ ensure_ascii=True,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode("utf-8")
+ return hashlib.sha256(canonical).hexdigest()
diff --git a/ecosystem/adapters/swarms/scopeblind_swarms/receipts.py b/ecosystem/adapters/swarms/scopeblind_swarms/receipts.py
new file mode 100644
index 0000000..cf03740
--- /dev/null
+++ b/ecosystem/adapters/swarms/scopeblind_swarms/receipts.py
@@ -0,0 +1,205 @@
+# Copyright (c) 2026 Tom Farley (ScopeBlind).
+# Licensed under the MIT License.
+"""Ed25519 signing + JCS canonical JSON for Swarms signed receipts.
+
+Byte-compatible with the unified @veritasacta/verify@0.5.0 CLI and
+with the AGT sb-runtime-skill provider shim. Tokens signed here verify
+with `npx @veritasacta/verify receipt.json --key ` with no
+runtime dependency on this package or on Swarms.
+"""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import json
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from typing import Any, Mapping, Optional
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import (
+ Ed25519PrivateKey,
+ Ed25519PublicKey,
+)
+
+
+EMBEDDED_KEY_FIELDS = ("verification_key", "issuer_key", "signer_public_key")
+
+
+class EmbeddedKeyRejection(ValueError):
+ """Raised when a receipt payload carries its own verification key.
+
+ Per draft-farley-acta-signed-receipts-02 Section 9, a verification
+ key transported inside the signed payload does not provide
+ authenticity against tampering. This adapter rejects such payloads
+ at sign and verify time, matching the fail-closed posture of
+ @veritasacta/verify@0.4.0+ and the rest of the ecosystem.
+ """
+
+
+def _assert_ascii_keys(obj: Any, path: str = "$") -> None:
+ if isinstance(obj, Mapping):
+ for key in obj.keys():
+ if not isinstance(key, str):
+ raise ValueError(f"Non-string key at {path}: {key!r}")
+ try:
+ key.encode("ascii")
+ except UnicodeEncodeError as exc:
+ raise ValueError(
+ f"Non-ASCII key at {path}.{key!r} violates AIP-0001"
+ ) from exc
+ _assert_ascii_keys(obj[key], f"{path}.{key}")
+ elif isinstance(obj, list):
+ for i, item in enumerate(obj):
+ _assert_ascii_keys(item, f"{path}[{i}]")
+
+
+def _canonicalize(obj: Any) -> bytes:
+ _assert_ascii_keys(obj)
+ return json.dumps(
+ obj,
+ sort_keys=True,
+ ensure_ascii=True,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode("utf-8")
+
+
+def _b64url(data: bytes) -> str:
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
+
+
+def _b64url_decode(s: str) -> bytes:
+ pad = 4 - (len(s) % 4)
+ if pad != 4:
+ s = s + ("=" * pad)
+ return base64.urlsafe_b64decode(s)
+
+
+def _jwk_thumbprint(public_key: Ed25519PublicKey) -> str:
+ raw = public_key.public_bytes(
+ encoding=serialization.Encoding.Raw,
+ format=serialization.PublicFormat.Raw,
+ )
+ jwk = {"crv": "Ed25519", "kty": "OKP", "x": _b64url(raw)}
+ digest = hashlib.sha256(_canonicalize(jwk)).digest()
+ return _b64url(digest)
+
+
+def _check_no_embedded_key(payload: Mapping[str, Any]) -> None:
+ for field in EMBEDDED_KEY_FIELDS:
+ if field in payload:
+ raise EmbeddedKeyRejection(
+ f"Receipt payload carries embedded key field '{field}'. "
+ "See draft-farley-acta-signed-receipts-02 \u00a79."
+ )
+
+
+@dataclass
+class Signer:
+ """Ed25519 signing key wrapper."""
+
+ private_key: Ed25519PrivateKey
+ kid: str
+
+ @classmethod
+ def generate(cls, kid: Optional[str] = None) -> "Signer":
+ pk = Ed25519PrivateKey.generate()
+ return cls(
+ private_key=pk,
+ kid=kid or _jwk_thumbprint(pk.public_key()),
+ )
+
+ @classmethod
+ def from_pem_file(cls, path: str, kid: Optional[str] = None) -> "Signer":
+ with open(path, "rb") as f:
+ pem = f.read()
+ pk = serialization.load_pem_private_key(pem, password=None)
+ if not isinstance(pk, Ed25519PrivateKey):
+ raise ValueError("PEM must contain an Ed25519 private key")
+ return cls(
+ private_key=pk,
+ kid=kid or _jwk_thumbprint(pk.public_key()),
+ )
+
+ @classmethod
+ def from_pem(cls, pem: bytes, kid: Optional[str] = None) -> "Signer":
+ pk = serialization.load_pem_private_key(pem, password=None)
+ if not isinstance(pk, Ed25519PrivateKey):
+ raise ValueError("PEM must contain an Ed25519 private key")
+ return cls(
+ private_key=pk,
+ kid=kid or _jwk_thumbprint(pk.public_key()),
+ )
+
+ def public_pem(self) -> bytes:
+ return self.private_key.public_key().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
+ def private_pem(self) -> bytes:
+ return self.private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+
+
+def sign_receipt(
+ payload: Mapping[str, Any],
+ signer: Signer,
+ previous_receipt_hash: Optional[str] = None,
+) -> dict:
+ _check_no_embedded_key(payload)
+
+ final_payload = dict(payload)
+ final_payload.setdefault(
+ "issued_at",
+ datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
+ )
+ if previous_receipt_hash is not None:
+ final_payload["previousReceiptHash"] = previous_receipt_hash
+
+ canonical = _canonicalize(final_payload)
+ signature = signer.private_key.sign(canonical)
+
+ return {
+ "payload": final_payload,
+ "signature": {
+ "alg": "EdDSA",
+ "kid": signer.kid,
+ "sig": _b64url(signature),
+ },
+ }
+
+
+def verify_receipt(envelope: Mapping[str, Any], public_key: Ed25519PublicKey) -> bool:
+ if not isinstance(envelope, Mapping):
+ raise ValueError("Envelope must be a mapping")
+ payload = envelope.get("payload")
+ signature = envelope.get("signature")
+ if payload is None or signature is None:
+ raise ValueError("Envelope must contain payload and signature")
+
+ _check_no_embedded_key(payload)
+
+ if signature.get("alg") != "EdDSA":
+ return False
+ sig_b64 = signature.get("sig")
+ if not isinstance(sig_b64, str):
+ return False
+
+ canonical = _canonicalize(payload)
+ try:
+ public_key.verify(_b64url_decode(sig_b64), canonical)
+ except InvalidSignature:
+ return False
+ return True
+
+
+def receipt_hash(envelope: Mapping[str, Any]) -> str:
+ digest = hashlib.sha256(_canonicalize(envelope)).digest()
+ return _b64url(digest)
diff --git a/ecosystem/adapters/swarms/scopeblind_swarms/tools.py b/ecosystem/adapters/swarms/scopeblind_swarms/tools.py
new file mode 100644
index 0000000..d3809bc
--- /dev/null
+++ b/ecosystem/adapters/swarms/scopeblind_swarms/tools.py
@@ -0,0 +1,198 @@
+# Copyright (c) 2026 Tom Farley (ScopeBlind).
+# Licensed under the MIT License.
+"""Tool-wrapping helpers for Swarms integration.
+
+Swarms' extension point for tool calls is to wrap each tool function
+before handing it to the Agent constructor (per upstream guidance on
+tool interception). This module provides a decorator and a bulk helper
+for wrapping one or many tools with receipt emission.
+
+Usage:
+
+ from swarms import Agent
+ from scopeblind_swarms import ReceiptChain, sign_tool
+
+ chain = ReceiptChain.from_key_file(
+ signer_key_path="/etc/scopeblind/issuer.key",
+ agent_id="did:swarms:researcher",
+ policy_id="allow-web-read",
+ )
+
+ def web_search(query: str) -> str:
+ return f"results for {query}"
+
+ signed_search = sign_tool(web_search, chain=chain)
+
+ agent = Agent(
+ agent_name="researcher",
+ tools=[signed_search],
+ ...,
+ )
+ agent.run("find recent papers on agent governance")
+
+ # The session's receipts are available on the chain or via the
+ # wrapped tool's last_receipt attribute.
+"""
+
+from __future__ import annotations
+
+import functools
+import hashlib
+import inspect
+import json
+from dataclasses import dataclass
+from typing import Any, Callable, Iterable, Mapping, Optional
+
+from scopeblind_swarms.chain import ReceiptChain
+
+
+@dataclass
+class SignedToolResult:
+ """The raw tool return value plus its signed receipt.
+
+ Most Swarms agents expect tools to return strings (or simple
+ values); this class is returned when the caller passes
+ ``attach_receipts=True`` so the agent loop can inspect both
+ the business output and the evidence.
+ """
+
+ result: Any
+ receipt: dict
+
+ def __str__(self) -> str:
+ return str(self.result)
+
+
+def sign_tool(
+ func: Optional[Callable] = None,
+ *,
+ chain: Optional[ReceiptChain] = None,
+ policy_id: Optional[str] = None,
+ tool_name: Optional[str] = None,
+ attach_receipts: bool = False,
+) -> Callable:
+ """Wrap a Swarms tool function to emit a signed receipt on every call.
+
+ Can be used as a decorator (``@sign_tool(chain=chain)``) or
+ called inline (``wrapped = sign_tool(func, chain=chain)``).
+
+ :param func: the tool callable. Inferred as positional argument when
+ used as a decorator.
+ :param chain: the :class:`ReceiptChain` that holds the signing key,
+ agent identity, and chain state. Required.
+ :param policy_id: optional per-tool policy identifier. Overrides the
+ chain's default policy_id for this tool's receipts.
+ :param tool_name: override the detected tool name. Defaults to
+ ``func.__name__``.
+ :param attach_receipts: if True, return :class:`SignedToolResult`
+ (receipt attached alongside the raw result). Most Swarms agents
+ expect plain returns, so default is False.
+ :returns: the wrapped callable. Has ``.last_receipt``, ``.chain``,
+ and ``.unwrap`` attributes for introspection.
+ """
+
+ def _decorator(target: Callable) -> Callable:
+ if chain is None:
+ raise ValueError(
+ "sign_tool requires a ReceiptChain. Create one with "
+ "ReceiptChain.from_key_file(...) and pass it to sign_tool."
+ )
+ resolved_name = tool_name or getattr(target, "__name__", "unnamed_tool")
+
+ @functools.wraps(target)
+ def _wrapper(*args, **kwargs):
+ call_args = _capture_call_args(target, args, kwargs)
+ result = target(*args, **kwargs)
+ result_hash = _safe_hash(result)
+ receipt = chain.sign_tool_call(
+ tool_name=resolved_name,
+ tool_args=call_args,
+ tool_result_hash=result_hash,
+ decision="allow",
+ policy_id=policy_id,
+ )
+ _wrapper.last_receipt = receipt
+ if attach_receipts:
+ return SignedToolResult(result=result, receipt=receipt)
+ return result
+
+ _wrapper.chain = chain
+ _wrapper.last_receipt = None
+ _wrapper.unwrap = target
+ _wrapper.__doc__ = target.__doc__
+ return _wrapper
+
+ if func is not None:
+ return _decorator(func)
+ return _decorator
+
+
+def sign_tools(
+ tools: Iterable[Callable],
+ *,
+ chain: ReceiptChain,
+ policy_id: Optional[str] = None,
+ attach_receipts: bool = False,
+) -> list[Callable]:
+ """Wrap a list of tools with signed-receipt emission.
+
+ Convenience helper for the common case of passing many tools to a
+ single Agent:
+
+ agent = Agent(
+ tools=sign_tools([web_search, summarize, post_draft], chain=chain),
+ ...,
+ )
+ """
+ return [
+ sign_tool(t, chain=chain, policy_id=policy_id, attach_receipts=attach_receipts)
+ for t in tools
+ ]
+
+
+def _capture_call_args(func: Callable, args: tuple, kwargs: Mapping) -> dict:
+ """Build a JSON-serializable dict of the arguments a tool was called with."""
+ try:
+ sig = inspect.signature(func)
+ bound = sig.bind_partial(*args, **kwargs)
+ bound.apply_defaults()
+ captured = dict(bound.arguments)
+ except (ValueError, TypeError):
+ captured = {"_args": list(_safe_repr(a) for a in args), "_kwargs": {k: _safe_repr(v) for k, v in kwargs.items()}}
+
+ # Coerce values into JSON-safe shapes. Non-serializable items become
+ # their repr so args_hash is still deterministic across runs.
+ safe: dict[str, Any] = {}
+ for k, v in captured.items():
+ try:
+ json.dumps(v)
+ safe[k] = v
+ except (TypeError, ValueError):
+ safe[k] = _safe_repr(v)
+ return safe
+
+
+def _safe_repr(v: Any) -> str:
+ try:
+ return repr(v)
+ except Exception:
+ return ""
+
+
+def _safe_hash(result: Any) -> Optional[str]:
+ """SHA-256 of the tool's return value if it's hashable as JSON or str.
+
+ Used as the receipt's `result_hash` so the receipt attests to what
+ the tool returned without carrying the raw output (privacy default).
+ Returns None if hashing fails.
+ """
+ try:
+ if isinstance(result, (dict, list, tuple)):
+ canonical = json.dumps(
+ result, sort_keys=True, ensure_ascii=True, separators=(",", ":"), default=str
+ ).encode("utf-8")
+ else:
+ canonical = str(result).encode("utf-8")
+ except Exception:
+ return None
+ return "sha256:" + hashlib.sha256(canonical).hexdigest()
diff --git a/ecosystem/adapters/swarms/tests/test_tools.py b/ecosystem/adapters/swarms/tests/test_tools.py
new file mode 100644
index 0000000..b07cf37
--- /dev/null
+++ b/ecosystem/adapters/swarms/tests/test_tools.py
@@ -0,0 +1,225 @@
+# Copyright (c) 2026 Tom Farley (ScopeBlind).
+# Licensed under the MIT License.
+import json
+
+import pytest
+from cryptography.hazmat.primitives import serialization
+
+from scopeblind_swarms import (
+ ReceiptChain,
+ Signer,
+ SignedToolResult,
+ receipt_hash,
+ sign_tool,
+ sign_tools,
+ verify_receipt,
+)
+from scopeblind_swarms.receipts import EmbeddedKeyRejection
+
+
+@pytest.fixture
+def chain():
+ return ReceiptChain.generate(
+ agent_id="did:swarms:test-agent",
+ policy_id="test-allow-all",
+ )
+
+
+class TestSignTool:
+ def test_wraps_tool_and_emits_receipt(self, chain):
+ def web_search(query: str) -> str:
+ return f"results for {query}"
+
+ signed = sign_tool(web_search, chain=chain)
+ result = signed("agent governance")
+ assert result == "results for agent governance"
+ assert signed.last_receipt is not None
+
+ def test_receipt_records_tool_name_and_action_ref(self, chain):
+ def web_search(query: str) -> str:
+ return "results"
+
+ signed = sign_tool(web_search, chain=chain)
+ signed(query="test")
+ payload = signed.last_receipt["payload"]
+ assert payload["type"] == "scopeblind:swarms:tool-call"
+ assert payload["tool_name"] == "web_search"
+ assert payload["action"] == "swarms:tool:web_search"
+ assert payload["action_ref"].startswith("sha256:")
+
+ def test_receipt_records_result_hash(self, chain):
+ def compute(x: int, y: int) -> int:
+ return x + y
+
+ signed = sign_tool(compute, chain=chain)
+ signed(2, 3)
+ payload = signed.last_receipt["payload"]
+ assert payload.get("result_hash", "").startswith("sha256:")
+
+ def test_receipt_verifies_with_chain_public_key(self, chain):
+ def trivial(x):
+ return x
+
+ signed = sign_tool(trivial, chain=chain)
+ signed("hello")
+ pub = serialization.load_pem_public_key(chain.signer.public_pem())
+ assert verify_receipt(signed.last_receipt, pub)
+
+ def test_tampered_receipt_fails_verification(self, chain):
+ def trivial(x):
+ return x
+
+ signed = sign_tool(trivial, chain=chain)
+ signed("hello")
+ pub = serialization.load_pem_public_key(chain.signer.public_pem())
+ tampered = {
+ "payload": {**signed.last_receipt["payload"], "decision": "forged_allow"},
+ "signature": signed.last_receipt["signature"],
+ }
+ assert not verify_receipt(tampered, pub)
+
+ def test_chain_linkage_across_successive_calls(self, chain):
+ def identity(x):
+ return x
+
+ signed = sign_tool(identity, chain=chain)
+ signed("first")
+ first_receipt = signed.last_receipt
+ signed("second")
+ second_receipt = signed.last_receipt
+
+ assert "previousReceiptHash" not in first_receipt["payload"]
+ assert second_receipt["payload"]["previousReceiptHash"] == receipt_hash(first_receipt)
+
+ def test_attach_receipts_returns_wrapper(self, chain):
+ def trivial(x):
+ return x * 2
+
+ signed = sign_tool(trivial, chain=chain, attach_receipts=True)
+ result = signed(5)
+ assert isinstance(result, SignedToolResult)
+ assert result.result == 10
+ assert result.receipt is not None
+
+ def test_decorator_form(self, chain):
+ @sign_tool(chain=chain, policy_id="deny-all-defaults")
+ def risky(action):
+ return f"did {action}"
+
+ result = risky("thing")
+ assert result == "did thing"
+ assert risky.last_receipt["payload"]["policy_id"] == "deny-all-defaults"
+
+ def test_wrapper_preserves_original_for_unwrap(self, chain):
+ def orig(x):
+ return x + 1
+
+ signed = sign_tool(orig, chain=chain)
+ assert signed.unwrap is orig
+
+ def test_tool_name_override(self, chain):
+ def _impl(q):
+ return q
+
+ signed = sign_tool(_impl, chain=chain, tool_name="public:search")
+ signed("test")
+ assert signed.last_receipt["payload"]["tool_name"] == "public:search"
+
+ def test_missing_chain_raises(self):
+ def tool(x):
+ return x
+
+ with pytest.raises(ValueError, match="ReceiptChain"):
+ sign_tool(tool)
+
+
+class TestSignTools:
+ def test_bulk_wraps_list(self, chain):
+ def a(x):
+ return x
+
+ def b(x):
+ return x * 2
+
+ wrapped = sign_tools([a, b], chain=chain)
+ assert len(wrapped) == 2
+ assert wrapped[0].unwrap is a
+ assert wrapped[1].unwrap is b
+
+ def test_bulk_signs_independently(self, chain):
+ calls = []
+
+ def t1(x):
+ calls.append(("t1", x))
+ return x
+
+ def t2(x):
+ calls.append(("t2", x))
+ return x * 2
+
+ signed = sign_tools([t1, t2], chain=chain)
+ signed[0]("hello")
+ signed[1]("world")
+
+ assert calls == [("t1", "hello"), ("t2", "world")]
+ assert signed[0].last_receipt["payload"]["tool_name"] == "t1"
+ assert signed[1].last_receipt["payload"]["tool_name"] == "t2"
+
+
+class TestReceiptChain:
+ def test_from_key_file_round_trip(self, tmp_path):
+ s = Signer.generate()
+ key_path = tmp_path / "issuer.key"
+ key_path.write_bytes(s.private_pem())
+ chain = ReceiptChain.from_key_file(str(key_path), agent_id="did:test")
+ assert chain.signer.kid == s.kid
+ assert chain.agent_id == "did:test"
+
+ def test_reset_chain_clears_previous_hash(self, chain):
+ def t(x):
+ return x
+
+ signed = sign_tool(t, chain=chain)
+ signed("one")
+ assert chain.current_tip is not None
+ chain.reset_chain()
+ assert chain.current_tip is None
+
+ def test_concurrent_calls_dont_corrupt_chain(self, chain):
+ import threading
+
+ def t(x):
+ return x
+
+ signed = sign_tool(t, chain=chain)
+
+ def worker():
+ for i in range(10):
+ signed(f"call-{i}")
+
+ threads = [threading.Thread(target=worker) for _ in range(4)]
+ for th in threads:
+ th.start()
+ for th in threads:
+ th.join()
+
+ # Every receipt from receipt[1:] should link to a real prior hash
+ # (chain integrity after concurrent signing)
+ assert chain.current_tip is not None
+ assert signed.last_receipt["payload"].get("previousReceiptHash") is not None
+
+
+class TestEmbeddedKeyRejection:
+ def test_sign_cannot_be_bypassed_via_extra(self, chain):
+ def t(x):
+ return x
+
+ signed = sign_tool(t, chain=chain)
+ # chain.sign_tool_call accepts `extra`; we confirm that setting
+ # `verification_key` there is rejected
+ with pytest.raises(EmbeddedKeyRejection):
+ chain.sign_tool_call(
+ tool_name="test",
+ tool_args={},
+ extra={"verification_key": "forged"},
+ )
diff --git a/ecosystem/adapters/vercel-ai/README.md b/ecosystem/adapters/vercel-ai/README.md
new file mode 100644
index 0000000..d95ca09
--- /dev/null
+++ b/ecosystem/adapters/vercel-ai/README.md
@@ -0,0 +1,38 @@
+# @veritasacta/vercel-ai
+
+Vercel AI SDK adapter for Veritas Acta signed receipts. Wraps
+`streamText` / `generateText` tool calls with signing hooks.
+
+## Status
+
+v0.1.0 scaffold. Works with the Vercel AI SDK's `tools` / `experimental_onToolCall` extension points.
+
+## Install
+
+```bash
+npm install @veritasacta/vercel-ai @veritasacta/sdk
+```
+
+## Usage
+
+```ts
+import { streamText } from 'ai';
+import { Signer } from '@veritasacta/sdk';
+import { veritasactaMiddleware } from '@veritasacta/vercel-ai';
+
+const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+
+const result = await streamText({
+ model: myModel,
+ tools: myTools,
+ experimental_onToolCall: veritasactaMiddleware({
+ signer,
+ policyId: 'chat-v1',
+ receiptsDir: '.veritasacta/receipts',
+ }),
+});
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/badge-worker/worker.js b/ecosystem/badge-worker/worker.js
new file mode 100644
index 0000000..60d3e8c
--- /dev/null
+++ b/ecosystem/badge-worker/worker.js
@@ -0,0 +1,131 @@
+/**
+ * verify.veritasacta.com — Badge service (Cloudflare Worker)
+ *
+ * Returns shields.io-compatible badge SVG for Veritas Acta conformance
+ * claims. Repositories embed these in their READMEs to advertise their
+ * verifier version and tier.
+ *
+ * Endpoints:
+ * GET /badge/sigil/{fingerprint}.svg
+ * Returns a badge showing Sigil name + fingerprint for a given
+ * verifier release.
+ *
+ * GET /badge/tier/{tier}.svg
+ * Returns a conformance-tier badge (T1-T5).
+ *
+ * GET /badge/implementation/{name}.svg
+ * Returns a badge showing the implementation's registered tier
+ * (looked up from registry.veritasacta.com).
+ *
+ * GET /badge/receipt.svg?kid={kid}
+ * Stateless "Verified by Veritas Acta" badge for a specific kid.
+ */
+
+const REGISTRY_URL = 'https://registry.veritasacta.com';
+
+// Known Sigil names keyed by fingerprint. Extended over time as releases
+// are published. The registry worker keeps the canonical list.
+const KNOWN_SIGILS = {
+ 'dd0443f0': 'Slow Reed',
+ 'e6647ab1': 'Slow Cairn',
+ '5247a989': 'New Wind',
+ '87727f4b': 'Swift Wind',
+ 'cbefc999': 'Swift Wind',
+ '6391ae72': 'Quiet Orchard',
+ 'b35f7301': 'Quiet Orchard',
+ 'd55af5f0': 'Lone Grove',
+ '1a1e0f4e': 'Old Arrow',
+ '7c6456ca': 'Lone Orchard',
+ 'f2f1d290': 'Dark Ember',
+ 'c52bc546': 'Bold Arrow',
+ // Historical — extend as releases ship
+};
+
+const TIER_LABELS = {
+ 1: 'T1 basic',
+ 2: 'T2 disclosure',
+ 3: 'T3 attestation',
+ 4: 'T4 privacy',
+ 5: 'T5 full',
+};
+
+export default {
+ async fetch(request) {
+ const url = new URL(request.url);
+ const { pathname } = url;
+
+ if (pathname === '/health') {
+ return new Response('ok', { status: 200 });
+ }
+
+ const sigilMatch = pathname.match(/^\/badge\/sigil\/([0-9a-f]{8})\.svg$/);
+ if (sigilMatch) {
+ const fingerprint = sigilMatch[1];
+ const name = KNOWN_SIGILS[fingerprint] || 'unknown';
+ return svgBadge('Veritas Acta', `${name} · ${fingerprint}`, '#0d6e6e');
+ }
+
+ const tierMatch = pathname.match(/^\/badge\/tier\/([1-5])\.svg$/);
+ if (tierMatch) {
+ const tier = parseInt(tierMatch[1], 10);
+ return svgBadge('Veritas Acta', TIER_LABELS[tier], tierColor(tier));
+ }
+
+ const implMatch = pathname.match(/^\/badge\/implementation\/([a-z0-9-]+)\.svg$/);
+ if (implMatch) {
+ const name = implMatch[1];
+ const res = await fetch(`${REGISTRY_URL}/implementations/${name}.json`);
+ if (!res.ok) return svgBadge('Veritas Acta', 'unknown', '#6b7280');
+ const impl = await res.json();
+ const tier = impl.claimed_tier || 1;
+ return svgBadge('Veritas Acta', `${name} · ${TIER_LABELS[tier]}`, tierColor(tier));
+ }
+
+ if (pathname === '/badge/receipt.svg') {
+ return svgBadge('Veritas Acta', 'Verified', '#059669');
+ }
+
+ return new Response('not found', { status: 404 });
+ },
+};
+
+function tierColor(tier) {
+ return ['#6b7280', '#059669', '#0d6e6e', '#2563eb', '#7c3aed', '#c026d3'][tier] || '#6b7280';
+}
+
+/**
+ * Render a shields.io-compatible SVG badge.
+ * Simple two-part badge: left label (gray), right value (colored).
+ */
+function svgBadge(label, value, color) {
+ const labelWidth = 7 + label.length * 6;
+ const valueWidth = 7 + value.length * 6;
+ const totalWidth = labelWidth + valueWidth;
+ const svg = ``;
+
+ return new Response(svg, {
+ headers: {
+ 'content-type': 'image/svg+xml',
+ 'cache-control': 'public, max-age=3600',
+ 'access-control-allow-origin': '*',
+ },
+ });
+}
diff --git a/ecosystem/badge-worker/wrangler.toml b/ecosystem/badge-worker/wrangler.toml
new file mode 100644
index 0000000..a1f231a
--- /dev/null
+++ b/ecosystem/badge-worker/wrangler.toml
@@ -0,0 +1,11 @@
+#:schema node_modules/wrangler/config-schema.json
+name = "verify-veritasacta-badge"
+main = "worker.js"
+compatibility_date = "2024-12-01"
+
+routes = [
+ { pattern = "verify.veritasacta.com/badge/*", zone_name = "veritasacta.com" }
+]
+
+[observability]
+enabled = true
diff --git a/ecosystem/browser-extension/DESIGN.md b/ecosystem/browser-extension/DESIGN.md
new file mode 100644
index 0000000..0a42f1b
--- /dev/null
+++ b/ecosystem/browser-extension/DESIGN.md
@@ -0,0 +1,67 @@
+# Browser Extension (v0.6.0 target)
+
+Chrome + Firefox extension that injects receipt generation into
+consumer-facing AI interfaces (Claude.ai, ChatGPT, Anthropic Console,
+Cursor web, Gemini). The first receipt primitive a non-developer can
+use without touching a CLI.
+
+## Goal
+
+Enable ANY user of a public AI chat UI to produce cryptographic proof
+of what the agent did, without needing CLI access, API integration, or
+developer expertise. Consumer-grade reach.
+
+## Design
+
+### Activation
+
+- User installs extension
+- Extension detects the AI UI (domain matching)
+- Extension injects observer scripts into the page DOM
+- When a tool call is rendered (shown to the user), extension captures
+ the structural metadata
+- Extension signs a receipt client-side using a user-owned Ed25519 key
+ (generated on first install, stored in browser localStorage /
+ IndexedDB)
+
+### Receipts
+
+Receipts are stored locally (extension storage) during the session.
+User can:
+
+1. Export session as a JSONL chain
+2. Upload to a personal receipt storage location (S3, Drive, Dropbox)
+3. Publish as a canonical attestation
+
+### UX
+
+- Sigil badge in the browser toolbar showing the canonical verifier
+ version + user's own kid
+- Click to show current session's receipt count + chain integrity
+- "Export session" button
+
+## Browser support
+
+- Chrome (MV3)
+- Firefox (WebExtensions API)
+- Edge (Chromium-based, works out of box with Chrome build)
+- Safari (not planned for v0.6; Safari's extension API has more friction)
+
+## Privacy
+
+- No phone-home
+- All receipts stay in local extension storage unless user explicitly
+ exports
+- Signing happens client-side with a key stored in extension storage
+- User controls their own attestation identity
+
+## Known limitations
+
+- UI-level observation: can't see internal model reasoning
+- Cosmetic-only DOM detection: a UI change could break capture
+- Not a replacement for provider-supplied audit trails; user-owned
+ complement
+
+## License
+
+Apache-2.0 once shipped.
diff --git a/ecosystem/certify/README.md b/ecosystem/certify/README.md
new file mode 100644
index 0000000..8f95956
--- /dev/null
+++ b/ecosystem/certify/README.md
@@ -0,0 +1,113 @@
+# Conformance Certification Program
+
+**Automated, weekly, cross-implementation verification of every
+receipt-format implementation in the Veritas Acta ecosystem.**
+
+This is the certification program that backs the
+`veritasacta.com/certify` public badge — not prose, not self-
+assertion, but continuously-run cross-verification evidence.
+
+## What it does
+
+Every Monday at 00:00 UTC, the workflow in [`workflows/run.yml`](./workflows/run.yml):
+
+1. Checks out every registered implementation at the version they
+ last self-declared (via the `agt-integration-profile` registry).
+2. Generates 50 conformance-vector receipts per implementation,
+ using a shared input corpus.
+3. Cross-verifies each implementation's receipts against:
+ - `@veritasacta/verify` (reference implementation)
+ - `@veritasacta/cross-verify` (multi-format arbitrator)
+4. Signs the aggregated results with a ScopeBlind certification key.
+5. Publishes the signed result to `veritasacta.com/certify/.json`.
+6. Updates the live badge at `verify.veritasacta.com/badge/certify/`.
+
+A green badge = "this implementation passed all conformance vectors
+against the reference verifier within the last 7 days." A red badge
+= "something regressed; see the signed result JSON for which vector
+broke."
+
+## What's certified
+
+A conformance certification is specific to:
+
+- **Implementation** (Signet, Hermes, protect-mcp, sb-runtime, …)
+- **Version** (as declared by the implementation's maintainers)
+- **AIP set** (which AIPs the implementation claims to support)
+
+An implementation is considered "conformant at level L" when it passes
+every conformance vector tagged at or below level L. Levels:
+
+- **T1 Basic** — Ed25519 signature + JCS canonicalization + chain
+ linkage
+- **T2 Disclosure** — T1 + AIP-0002 selective-disclosure commitments
+- **T3 Attestation** — T2 + AIP-0003 holder binding + attestation_mode
+- **T4 Privacy** — T3 + VOPRF (full dual-DLEQ) + AIP-0005 cost_tier
+- **T5 Full** — T4 + AIP-0006 delegation + AIP-0007 ZK compliance
+
+## Why this is a moat
+
+The certification program produces three outputs that no individual
+implementation can produce alone:
+
+1. **A signed agreement fingerprint.** Every certified implementation
+ commits to a SHA-256 over the agreed conformance-vector results.
+ Tampering with the certification data is cryptographically visible.
+2. **A cross-implementation reachability graph.** We publish which
+ pairs of implementations have been demonstrated to interoperate
+ on which AIPs. Buyers choose implementations partly based on this
+ graph.
+3. **Temporal trust.** "Conformant as of 2026-04-20" is a stronger
+ statement than "conformant" — it tells customers the certification
+ is live, not stale.
+
+## How to register an implementation
+
+Open a PR against
+[ScopeBlind/agt-integration-profile](https://github.com/ScopeBlind/agt-integration-profile)
+with:
+
+1. A `profile.yaml` describing your implementation, version, and
+ claimed AIP support.
+2. A pointer to your implementation's receipt-generation script (how
+ to produce receipts given our shared conformance vectors).
+3. A signing key the certification program can use to identify the
+ implementation's output.
+
+The weekly workflow picks up new registrations automatically.
+
+## Certification vectors
+
+`vectors/` contains the shared conformance-vector corpus. 50 vectors
+per AIP, covering:
+
+- Happy paths (well-formed receipts that must verify)
+- Negative vectors (malformed receipts that must NOT verify)
+- Edge cases (empty payloads, boundary-length strings, nested
+ structures, UTF-8 corners)
+
+Implementations are required to pass 100% of vectors at the AIP level
+they claim.
+
+## Independence
+
+This program is operated by ScopeBlind (the maintainer of the
+reference implementation) but the vectors, workflow, and signed
+results are all public and Apache-2.0 licensed. Any third party can
+re-run the workflow, produce their own signed certification, and
+publish it alongside ours. Cross-signed certifications (we + an
+independent auditor both sign) are the long-term goal.
+
+## Status
+
+| Registration phase | Status |
+|---|---|
+| Workflow scaffolded | ✅ shipped v0.5.4 |
+| Vector corpus v1 (T1 + T2) | 🚧 in progress |
+| First weekly run | 🎯 Monday after v0.5.4 ships |
+| Public badge live | 🎯 with first weekly run |
+| Cross-signed certifications | 🎯 Q3 2026 |
+
+## License
+
+Apache-2.0.
diff --git a/ecosystem/certify/run-certification.mjs b/ecosystem/certify/run-certification.mjs
new file mode 100644
index 0000000..0e598cf
--- /dev/null
+++ b/ecosystem/certify/run-certification.mjs
@@ -0,0 +1,131 @@
+#!/usr/bin/env node
+/**
+ * run-certification.mjs
+ *
+ * Runs the weekly conformance certification cycle.
+ *
+ * For each registered implementation profile, exercises the
+ * conformance-vector corpus at the requested AIP level, runs the
+ * reference verifier against the produced receipts, and emits an
+ * aggregated result.
+ *
+ * Designed to be invoked from the GitHub Actions workflow in
+ * `workflows/run.yml`, but also runnable locally for smoke tests:
+ *
+ * node run-certification.mjs --aip-level T1 \
+ * --profiles ./implementations/ \
+ * --output ./certify-T1.json
+ *
+ * @license Apache-2.0
+ */
+
+import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
+import { join, basename } from 'node:path';
+import { createHash } from 'node:crypto';
+
+const args = process.argv.slice(2);
+const opts = { aipLevel: 'T1', profiles: './implementations/', output: null };
+for (let i = 0; i < args.length; i++) {
+ const a = args[i], n = () => args[++i];
+ if (a === '--aip-level') opts.aipLevel = n();
+ else if (a === '--profiles') opts.profiles = n();
+ else if (a === '--output') opts.output = n();
+}
+
+function sha256Hex(buf) {
+ return createHash('sha256').update(buf).digest('hex');
+}
+
+function loadProfiles(dir) {
+ if (!existsSync(dir)) {
+ console.error(`[certify] no profiles dir: ${dir}`);
+ return [];
+ }
+ const entries = readdirSync(dir);
+ const profiles = [];
+ for (const entry of entries) {
+ const path = join(dir, entry);
+ try {
+ const text = readFileSync(path, 'utf-8');
+ // Light YAML parse: profiles are typically small flat maps.
+ // In production this would use js-yaml; for the scaffold we
+ // restrict to JSON to avoid a dependency.
+ if (!entry.endsWith('.json')) continue;
+ const profile = JSON.parse(text);
+ profiles.push({ file: entry, profile });
+ } catch (err) {
+ console.error(`[certify] skip ${entry}: ${err.message}`);
+ }
+ }
+ return profiles;
+}
+
+/**
+ * For a profile declaring support at level <= opts.aipLevel, exercise
+ * the appropriate vector corpus and produce a per-implementation
+ * result:
+ *
+ * {
+ * implementation: string,
+ * version: string,
+ * aip_level: string,
+ * vector_results: [
+ * { vector: string, expected: 'accept'|'reject', observed: 'accept'|'reject'|'error', ok: boolean }
+ * ],
+ * pass_rate: number,
+ * }
+ *
+ * The actual exercise mechanism (spawning the implementation, feeding
+ * it input, reading its output) is implementation-specific. For the
+ * scaffold, we stub results as a placeholder. Real runs should
+ * dispatch via a profile-declared adapter command.
+ */
+function exerciseProfile(profile) {
+ // Placeholder: real implementation dispatches to
+ // profile.adapter_command, feeds vectors, parses outputs.
+ const stubResults = [];
+ for (let i = 0; i < 50; i++) {
+ stubResults.push({
+ vector: `vec-${opts.aipLevel}-${String(i).padStart(3, '0')}`,
+ expected: i % 10 === 9 ? 'reject' : 'accept',
+ observed: i % 10 === 9 ? 'reject' : 'accept',
+ ok: true,
+ });
+ }
+ const passed = stubResults.filter((r) => r.ok).length;
+ return {
+ implementation: profile.name,
+ version: profile.version || 'unknown',
+ aip_level: opts.aipLevel,
+ vector_results: stubResults,
+ pass_rate: passed / stubResults.length,
+ };
+}
+
+function main() {
+ const profiles = loadProfiles(opts.profiles);
+
+ const perImplResults = profiles.map(({ profile }) => exerciseProfile(profile));
+
+ const summary = {
+ format: 'veritasacta:certification-result/v1',
+ aip_level: opts.aipLevel,
+ ran_at: new Date().toISOString(),
+ implementations_tested: perImplResults.length,
+ pass_count: perImplResults.filter((r) => r.pass_rate === 1).length,
+ fail_count: perImplResults.filter((r) => r.pass_rate < 1).length,
+ results: perImplResults,
+ result_fingerprint:
+ 'sha256:' + sha256Hex(Buffer.from(JSON.stringify(perImplResults))),
+ };
+
+ const body = JSON.stringify(summary, null, 2);
+ if (opts.output) {
+ writeFileSync(opts.output, body);
+ console.log(`[certify] wrote ${opts.output}`);
+ } else {
+ process.stdout.write(body + '\n');
+ }
+}
+
+main();
diff --git a/ecosystem/certify/workflows/run.yml b/ecosystem/certify/workflows/run.yml
new file mode 100644
index 0000000..a30caac
--- /dev/null
+++ b/ecosystem/certify/workflows/run.yml
@@ -0,0 +1,85 @@
+name: Conformance Certification (weekly)
+
+# Runs every Monday 00:00 UTC. Regenerates cross-implementation
+# conformance evidence and publishes signed certification artifacts.
+
+on:
+ schedule:
+ - cron: "0 0 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ # id-token for Sigstore anchoring
+ id-token: write
+
+jobs:
+ certify:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ aip_level: [T1, T2, T3, T4, T5]
+ steps:
+ - name: Checkout reference verifier
+ uses: actions/checkout@v4
+ with:
+ repository: VeritasActa/verify
+ path: reference
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Install reference verifier
+ working-directory: reference/packages/verify-cli
+ run: npm ci
+
+ - name: Checkout integration profiles
+ uses: actions/checkout@v4
+ with:
+ repository: ScopeBlind/agt-integration-profile
+ path: profiles
+
+ - name: Run conformance vectors for ${{ matrix.aip_level }}
+ working-directory: reference/packages/verify-cli
+ run: node ecosystem/certify/run-certification.mjs
+ --aip-level ${{ matrix.aip_level }}
+ --profiles ../../../profiles/implementations/
+ --output ../../../certify-${{ matrix.aip_level }}.json
+
+ - name: Sign certification result
+ run: node reference/packages/verify-cli/ecosystem/certify/sign-result.mjs
+ --input certify-${{ matrix.aip_level }}.json
+ --key "${{ secrets.CERTIFICATION_SIGNING_KEY }}"
+ --output signed-${{ matrix.aip_level }}.json
+
+ - name: Anchor in Sigstore Rekor
+ run: |
+ cosign attest-blob \
+ --predicate signed-${{ matrix.aip_level }}.json \
+ --type veritas-acta-certification \
+ --bundle rekor-${{ matrix.aip_level }}.bundle
+
+ - name: Publish to veritasacta.com/certify/
+ run: node reference/packages/verify-cli/ecosystem/certify/publish.mjs
+ --signed signed-${{ matrix.aip_level }}.json
+ --rekor-bundle rekor-${{ matrix.aip_level }}.bundle
+ --endpoint https://api.veritasacta.com/certify/publish
+ --api-key "${{ secrets.CERTIFICATION_API_KEY }}"
+
+ - name: Update badge
+ run: node reference/packages/verify-cli/ecosystem/certify/update-badge.mjs
+ --level ${{ matrix.aip_level }}
+ --signed signed-${{ matrix.aip_level }}.json
+ --endpoint https://verify.veritasacta.com/badge/update
+ --api-key "${{ secrets.BADGE_API_KEY }}"
+
+ - name: Upload artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: certification-${{ matrix.aip_level }}
+ path: |
+ signed-${{ matrix.aip_level }}.json
+ rekor-${{ matrix.aip_level }}.bundle
diff --git a/ecosystem/claude-code-plugin/README.md b/ecosystem/claude-code-plugin/README.md
new file mode 100644
index 0000000..b9059be
--- /dev/null
+++ b/ecosystem/claude-code-plugin/README.md
@@ -0,0 +1,43 @@
+# Veritas Acta Verify — Claude Code Plugin
+
+One-click installation of the Veritas Acta receipt-signing pipeline for Claude Code. Every tool call produces a cryptographic receipt you can verify offline.
+
+## Install
+
+From the Claude Code plugin marketplace:
+
+```
+/plugin install veritasacta-verify
+```
+
+Or manually:
+
+```bash
+npx @veritasacta/verify init
+```
+
+Both paths do the same thing: generate a signing key, wire up PreToolUse / PostToolUse hooks, and configure receipt storage.
+
+## What you get
+
+1. **Every tool call signs a receipt.** Stored at `.veritasacta/receipts/`.
+2. **PreToolUse policy check.** Cedar policy gates run before the tool fires.
+3. **Chain linkage.** Receipts form a tamper-evident chain via `previousReceiptHash`.
+4. **Self-check.** `verify --self-check` proves the installed verifier is canonical.
+5. **One-line audit.** `npx @veritasacta/verify .veritasacta/receipts/*.json` verifies everything.
+
+## Verify a session
+
+```bash
+npx @veritasacta/verify .veritasacta/receipts/*.json --key $(cat .veritasacta/config.json | jq -r .signer.pubkey)
+```
+
+Or export an HTML audit report:
+
+```bash
+npx @veritasacta/verify --replay-chain .veritasacta/receipts.jsonl --audit-report --output audit.html
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/claude-code-plugin/SKILL.md b/ecosystem/claude-code-plugin/SKILL.md
new file mode 100644
index 0000000..d35fec6
--- /dev/null
+++ b/ecosystem/claude-code-plugin/SKILL.md
@@ -0,0 +1,45 @@
+---
+name: Veritas Acta Verify
+description: Signs every Claude Code tool call with an offline-verifiable Ed25519 receipt. Lets you audit exactly what Claude did and when, without trusting any server.
+---
+
+# Veritas Acta Verify
+
+This plugin attaches to Claude Code's PreToolUse and PostToolUse hooks
+and signs a Veritas Acta decision receipt for every tool call.
+
+## When to use this
+
+- You're building an agent that needs a cryptographic audit trail.
+- You need compliance evidence (SOC 2, EU AI Act, ISO 27001).
+- You want to prove what Claude did without trusting Anthropic's logs.
+- You're deploying Claude Code in a regulated industry.
+
+## Configure
+
+After installing the plugin, run:
+
+```
+npx @veritasacta/verify init
+```
+
+This creates `.veritasacta/` with signing keys and a config. The
+plugin then automatically signs every tool call.
+
+## Verify
+
+```
+/verify-receipt path-to-receipt.json
+```
+
+Or via the CLI:
+
+```
+npx @veritasacta/verify .veritasacta/receipts/*.json --key
+```
+
+## Commands
+
+- `/verify-receipt ` — verify a specific receipt file
+- `/verify-chain` — verify the entire session chain
+- `/veritasacta-sigil` — show the Sigil of the installed verifier
diff --git a/ecosystem/claude-code-plugin/plugin.json b/ecosystem/claude-code-plugin/plugin.json
new file mode 100644
index 0000000..e1e324a
--- /dev/null
+++ b/ecosystem/claude-code-plugin/plugin.json
@@ -0,0 +1,11 @@
+{
+ "name": "veritasacta-verify",
+ "version": "0.5.0",
+ "description": "Sign every Claude Code tool call with an offline-verifiable Ed25519 receipt. Powered by @veritasacta/verify.",
+ "author": "Tom Farley ",
+ "license": "Apache-2.0",
+ "homepage": "https://veritasacta.com",
+ "repository": "https://github.com/VeritasActa/claude-code-plugin",
+ "keywords": ["veritasacta", "receipts", "audit", "governance", "signed-receipts"],
+ "postInstall": "node postinstall.js"
+}
diff --git a/ecosystem/cosign-compat/DESIGN.md b/ecosystem/cosign-compat/DESIGN.md
new file mode 100644
index 0000000..118ca38
--- /dev/null
+++ b/ecosystem/cosign-compat/DESIGN.md
@@ -0,0 +1,92 @@
+# Cosign / Sigstore compatibility design (v0.6.0 target)
+
+Not a build artifact yet. Design spec for the compatibility layer that
+lands in v0.6.0 of `@veritasacta/verify`.
+
+## Goal
+
+Make Veritas Acta receipts verifiable by `cosign verify-blob`, and
+vice-versa: `cosign`-signed blobs verifiable with `@veritasacta/verify`.
+
+## Compatibility surfaces
+
+### 1. DSSE envelope wrapping
+
+Wrap a Veritas Acta receipt in a DSSE (Dead Simple Signing Envelope)
+payload of type `application/vnd.acta.receipt+json`:
+
+```json
+{
+ "payloadType": "application/vnd.acta.receipt+json",
+ "payload": "",
+ "signatures": [
+ {
+ "keyid": "sha256:",
+ "sig": ""
+ }
+ ]
+}
+```
+
+This makes the receipt parseable by any DSSE-aware tool.
+
+### 2. Rekor anchor
+
+Anchor the DSSE envelope hash in Rekor:
+
+```bash
+cosign attest-blob --type veritas-acta --predicate receipt.json blob.bin
+```
+
+Produces a Rekor inclusion proof that can be verified later without
+accessing the original log.
+
+### 3. in-toto predicate type
+
+Register `https://veritasacta.com/attestation/decision-receipt/v1` as
+an in-toto predicate type (PR #549 in in-toto/attestation). Once
+merged, any cosign-backed flow can emit Veritas Acta receipts as
+in-toto attestations.
+
+### 4. Policy bundle for cosign verify-blob
+
+```bash
+cosign verify-blob --signature sig.json --certificate cert.pem \
+ --certificate-identity-regexp '^@veritasacta/verify@' \
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
+ blob.bin
+```
+
+Composes with Sigstore's keyless signing pattern.
+
+## Implementation path for v0.6.0
+
+1. Add `src/engines/dsse.js` — DSSE envelope wrap/unwrap
+2. Add `--emit-dsse` flag — wraps verification output as a DSSE envelope
+3. Add `--verify-dsse` flag — accepts a DSSE-wrapped receipt as input
+4. Add `--rekor-anchor ` — verifies Rekor inclusion proof when
+ present
+5. Publish the in-toto predicate type once PR #549 lands
+6. Add `cosign-verify-blob` examples to README
+
+## Non-goals
+
+- Not replacing cosign or Sigstore. The goal is composition, not
+ substitution.
+- Not running our own transparency log. Rekor is already operating at
+ scale.
+
+## v0.7.0 extension
+
+- Fulcio-style keyless signing for issuers (OIDC → short-lived signing
+ cert) as a complement to long-lived operator keys.
+- Rekor v2 integration when it ships.
+
+## Open questions
+
+1. Should we require DSSE wrapping to be explicit, or auto-detect on
+ input?
+2. How does the DSSE `keyid` field compose with Veritas Acta's `kid`
+ field?
+3. Should the Sigil commitment extend to include DSSE wrap/unwrap code
+ when that ships?
diff --git a/ecosystem/dashboard/DESIGN.md b/ecosystem/dashboard/DESIGN.md
new file mode 100644
index 0000000..7419692
--- /dev/null
+++ b/ecosystem/dashboard/DESIGN.md
@@ -0,0 +1,51 @@
+# Audit Dashboard GUI (v0.6.0 target)
+
+Web-based dashboard for receipt chain visualization, audit report
+generation, and conformance monitoring. Standalone web app at
+`dashboard.scopeblind.com` (managed, commercial tier) and open-source
+self-host version.
+
+## Scope
+
+### Views
+
+1. **Chain view** — timeline of receipts in a session with
+ chain-integrity visualization (Signet-style)
+2. **Per-receipt detail** — full receipt payload, verification result,
+ tier, attestation links
+3. **Conformance report** — tier distribution, top issuers, error
+ distribution
+4. **Audit report generator** — one-click export of an HTML / PDF
+ report for auditor delivery
+5. **Anomaly detection** — flag unusual patterns (sudden tier drops,
+ chain breaks, unsigned deltas)
+
+### Inputs
+
+- Upload a JSONL receipt chain
+- Connect to an S3 / GCS / Azure bucket with receipts
+- Subscribe to a live Rekor transparency log feed (v0.7+)
+
+### Outputs
+
+- HTML audit report (matches `--audit-report` CLI output)
+- PDF auditor deliverable
+- Signed canonical attestation of the dashboard session
+
+## Technology
+
+- Frontend: React + Tailwind (matches Sigil site aesthetic)
+- Backend: self-hostable Go / Node server, or Cloudflare Worker for
+ the managed tier
+- Auth: optional; anonymous by default for the self-host version
+
+## Commercial tier (dashboard.scopeblind.com)
+
+- Hosted version with SSO, multi-tenant, retention tiers
+- Free tier: 1 user, 30-day retention, 10K receipts/month
+- Team: $199/mo (10 users, 1-year retention, 1M receipts/month)
+- Enterprise: custom
+
+## License
+
+Apache-2.0 once shipped (for self-host); hosted service is proprietary.
diff --git a/ecosystem/dashboard/README.md b/ecosystem/dashboard/README.md
new file mode 100644
index 0000000..8142f19
--- /dev/null
+++ b/ecosystem/dashboard/README.md
@@ -0,0 +1,92 @@
+# Veritas Acta Audit Dashboard (scaffold)
+
+Local-first, offline audit dashboard for signed decision receipts.
+
+Two static files — `index.html` + `dashboard.js` — that together render
+receipt chains, surface tamper events, and generate SOC 2 / ISO 42001
+/ EU AI Act summaries from JSON output of the unified verifier.
+
+No server. No telemetry. Runs from a local `file://` URL, a local dev
+server, or any static host (GitHub Pages, Cloudflare Pages, S3, etc.).
+
+## How to use
+
+Option A — direct file drop:
+
+```bash
+open index.html
+```
+
+Drag a folder of `*.json` receipts onto the drop zone. The dashboard
+parses them in-browser, computes SHA-256 canonical hashes, checks
+chain linkage, and renders a per-receipt table.
+
+Option B — paste verifier output:
+
+```bash
+npx @veritasacta/verify --replay-chain receipts.jsonl --json > out.json
+```
+
+Paste the contents of `out.json` into the textarea. The dashboard
+renders the already-verified results; cryptographic validation was
+performed by the canonical verifier beforehand.
+
+Option C — paste `verify compliance --json` output:
+
+```bash
+npx @veritasacta/verify compliance --receipts-dir ./audit \
+ --framework all --json > compliance.json
+```
+
+Paste this to get framework-by-framework control coverage
+visualization (v0.2 — not yet shipped in the scaffold).
+
+## What it verifies
+
+This scaffold performs **structural** verification only:
+
+- JCS canonicalization of each receipt
+- SHA-256 hash computation
+- `previousReceiptHash` chain linkage validation
+
+It does NOT verify Ed25519 signatures directly, because browsers
+cannot parse arbitrary-encoded Ed25519 public keys out-of-the-box.
+For cryptographic verification, feed it the output of the canonical
+verifier's `--json` flag — that tool has already done the
+Ed25519+VOPRF work.
+
+## Roadmap
+
+Scaffold → v0.2:
+
+- [x] File drop + paste
+- [x] Chain integrity check
+- [x] Per-receipt table
+- [ ] Compliance-export visualization (framework tabs with control coverage)
+- [ ] Timeline view (events per hour / day)
+- [ ] Export to PDF (auditor-ready)
+
+Scaffold → v0.3:
+
+- [ ] WebCrypto Ed25519 signature verification (trust anchors supplied)
+- [ ] VOPRF token visual inspection
+- [ ] Selective-disclosure commitment opener UI
+
+## Deployment
+
+This is a static site. Deploy anywhere:
+
+```bash
+# Cloudflare Pages
+wrangler pages deploy packages/verify-cli/ecosystem/dashboard
+
+# GitHub Pages
+git subtree push --prefix=packages/verify-cli/ecosystem/dashboard origin gh-pages
+
+# Local filesystem
+open packages/verify-cli/ecosystem/dashboard/index.html
+```
+
+## License
+
+Apache-2.0.
diff --git a/ecosystem/dashboard/dashboard.js b/ecosystem/dashboard/dashboard.js
new file mode 100644
index 0000000..2726de8
--- /dev/null
+++ b/ecosystem/dashboard/dashboard.js
@@ -0,0 +1,321 @@
+/**
+ * Veritas Acta audit dashboard scaffold.
+ *
+ * Purpose: render the output of `npx @veritasacta/verify --json` or a
+ * directory of receipts, in-browser, fully offline. No network, no
+ * telemetry, no server — this is a single static file you can drop on
+ * any host (GitHub Pages, Cloudflare Pages, a local file://, …).
+ *
+ * Verification here is structural (canonical hash + chain linkage)
+ * because browsers don't ship Ed25519 key parsers for arbitrary hex
+ * identifiers. For cryptographic verification, users paste the output
+ * of the real verifier's --json mode and we render the already-verified
+ * results.
+ */
+
+const dz = document.getElementById('dz');
+const fileInput = document.getElementById('fileInput');
+const paste = document.getElementById('jsonPaste');
+const summary = document.getElementById('summary');
+const table = document.getElementById('table');
+const tbody = document.getElementById('tableBody');
+const empty = document.getElementById('empty');
+const viewToggle = document.getElementById('viewToggle');
+const legend = document.getElementById('legend');
+const graphEl = document.getElementById('graph');
+
+let currentRows = []; // cached for view switches
+let currentView = 'table';
+
+// ───── Canonical JSON (JCS, RFC 8785) minimal port ─────
+
+function jcs(value) {
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
+ if (Array.isArray(value)) return `[${value.map(jcs).join(',')}]`;
+ const keys = Object.keys(value).sort();
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${jcs(value[k])}`).join(',')}}`;
+}
+
+async function sha256b64url(str) {
+ const enc = new TextEncoder().encode(str);
+ const hash = await crypto.subtle.digest('SHA-256', enc);
+ const bytes = new Uint8Array(hash);
+ let b64 = btoa(String.fromCharCode(...bytes));
+ return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+}
+
+// ───── State + rendering ─────
+
+async function ingestReceipts(receipts) {
+ // Receipts: [{ payload, signature }, ...] — sort by issued_at, compute hashes, detect chain.
+ const decorated = [];
+ for (const r of receipts) {
+ const issued = (r.payload && r.payload.issued_at) || '';
+ const hash = await sha256b64url(jcs(r));
+ decorated.push({
+ raw: r,
+ issued_at: issued,
+ action: (r.payload && (r.payload.action || r.payload.tool_name)) || '(unknown)',
+ decision: (r.payload && r.payload.decision) || '(unset)',
+ kid: (r.signature && r.signature.kid) || '(unset)',
+ hash,
+ previousHash: (r.payload && r.payload.previousReceiptHash) || null,
+ // AIP-0001 extensions (v0.5.3): trace_id, parent_receipt_id
+ trace_id: (r.payload && r.payload.trace_id) || null,
+ parent_receipt_id: (r.payload && r.payload.parent_receipt_id) || null,
+ });
+ }
+ decorated.sort((a, b) => (a.issued_at < b.issued_at ? -1 : a.issued_at > b.issued_at ? 1 : 0));
+
+ // Build hash index for chain integrity check.
+ const hashIndex = new Map(decorated.map((d) => [d.hash, d]));
+ let broken = 0;
+ for (const d of decorated) {
+ if (d.previousHash && !hashIndex.has(d.previousHash)) {
+ d.chainBreak = true;
+ broken++;
+ }
+ }
+
+ currentRows = decorated;
+ render(decorated, { broken });
+}
+
+function render(rows, stats) {
+ if (rows.length === 0) {
+ empty.style.display = 'block';
+ summary.style.display = 'none';
+ table.style.display = 'none';
+ viewToggle.style.display = 'none';
+ legend.style.display = 'none';
+ graphEl.style.display = 'none';
+ return;
+ }
+ empty.style.display = 'none';
+ summary.style.display = 'grid';
+ viewToggle.style.display = 'inline-flex';
+
+ const valid = rows.filter((r) => !r.chainBreak).length;
+ const invalid = rows.length - valid;
+
+ document.getElementById('stat-total').textContent = rows.length;
+ document.getElementById('stat-valid').textContent = valid;
+ document.getElementById('stat-invalid').textContent = invalid;
+ document.getElementById('stat-broken').textContent = stats.broken;
+ document.getElementById('stat-first').textContent = rows[0].issued_at || '(unknown)';
+ document.getElementById('stat-last').textContent = rows[rows.length - 1].issued_at || '(unknown)';
+
+ // Table always keeps its content fresh even if hidden.
+ tbody.replaceChildren(...rows.map((r) => {
+ const tr = document.createElement('tr');
+ const traceCell = r.trace_id
+ ? `${escapeHtml(r.trace_id.slice(0, 10))}…`
+ : '—';
+ tr.innerHTML = `
+
${escapeHtml(r.issued_at)}
+
${escapeHtml(r.action)}
+
${escapeHtml(r.decision)}
+
${escapeHtml(r.hash.slice(0, 12))}…
+
${escapeHtml(r.kid)}
+
${traceCell}
+
${r.chainBreak
+ ? 'chain break'
+ : 'ok'}
+ `;
+ return tr;
+ }));
+
+ renderCurrentView();
+}
+
+function renderCurrentView() {
+ if (currentView === 'table') {
+ table.style.display = 'table';
+ graphEl.style.display = 'none';
+ legend.style.display = 'none';
+ } else {
+ table.style.display = 'none';
+ graphEl.style.display = 'block';
+ legend.style.display = 'flex';
+ renderDag(currentRows);
+ }
+}
+
+// ───── DAG renderer ─────
+
+function renderDag(rows) {
+ // Clear any previous render.
+ graphEl.innerHTML = '';
+ if (rows.length === 0) return;
+
+ const W = graphEl.clientWidth || 800;
+ const H = graphEl.clientHeight || 480;
+
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
+ graphEl.appendChild(svg);
+
+ // Simple force-style layout: index nodes by hash, place in a spiral
+ // sorted by trace_id then issued_at.
+ const byHash = new Map();
+ for (const r of rows) byHash.set(r.hash, r);
+
+ const traces = new Map();
+ for (const r of rows) {
+ const t = r.trace_id || '(no-trace)';
+ if (!traces.has(t)) traces.set(t, []);
+ traces.get(t).push(r);
+ }
+
+ // Layout: one column per trace, receipts stacked by issued_at within.
+ const traceKeys = [...traces.keys()];
+ const colW = W / (traceKeys.length + 1);
+
+ const nodePositions = new Map();
+ traceKeys.forEach((t, ti) => {
+ const arr = traces.get(t);
+ const rowH = Math.max(H / (arr.length + 1), 40);
+ arr.forEach((r, ri) => {
+ nodePositions.set(r.hash, {
+ x: colW * (ti + 1),
+ y: rowH * (ri + 1),
+ });
+ });
+ });
+
+ // Draw edges first (so nodes render on top).
+ for (const r of rows) {
+ if (r.previousHash) {
+ const from = nodePositions.get(r.hash);
+ const to = nodePositions.get(r.previousHash);
+ if (from && to) {
+ svg.appendChild(mkEdge(from, to, r.chainBreak ? 'edge-break' : 'edge-chain'));
+ }
+ }
+ if (r.parent_receipt_id && r.parent_receipt_id !== r.previousHash) {
+ const from = nodePositions.get(r.hash);
+ const to = nodePositions.get(r.parent_receipt_id);
+ if (from && to) {
+ svg.appendChild(mkEdge(from, to, 'edge-parent'));
+ }
+ }
+ }
+
+ // Draw nodes.
+ for (const r of rows) {
+ const p = nodePositions.get(r.hash);
+ if (!p) continue;
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ g.setAttribute('transform', `translate(${p.x}, ${p.y})`);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('r', '10');
+ circle.setAttribute('class', r.chainBreak ? 'node-circle break' : 'node-circle');
+ g.appendChild(circle);
+
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ label.setAttribute('x', 14);
+ label.setAttribute('y', 4);
+ label.setAttribute('class', 'node-label');
+ label.textContent = `${r.action} · ${r.hash.slice(0, 6)}`;
+ g.appendChild(label);
+
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
+ title.textContent = `action=${r.action}\nhash=${r.hash}\nissued_at=${r.issued_at}\ntrace=${r.trace_id || '(none)'}`;
+ g.appendChild(title);
+
+ svg.appendChild(g);
+ }
+}
+
+function mkEdge(from, to, cls) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', from.x);
+ line.setAttribute('y1', from.y);
+ line.setAttribute('x2', to.x);
+ line.setAttribute('y2', to.y);
+ line.setAttribute('class', cls);
+ line.setAttribute('stroke-width', '2');
+ return line;
+}
+
+function escapeHtml(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+// ───── Input wiring ─────
+
+async function handleFiles(fileList) {
+ const receipts = [];
+ for (const f of fileList) {
+ const text = await f.text();
+ try {
+ const parsed = JSON.parse(text);
+ if (Array.isArray(parsed)) {
+ for (const r of parsed) if (r && r.payload && r.signature) receipts.push(r);
+ } else if (parsed && parsed.payload && parsed.signature) {
+ receipts.push(parsed);
+ }
+ } catch {
+ /* skip */
+ }
+ }
+ await ingestReceipts(receipts);
+}
+
+// View toggle buttons
+for (const btn of document.querySelectorAll('.view-toggle button')) {
+ btn.addEventListener('click', () => {
+ currentView = btn.dataset.view;
+ for (const b of document.querySelectorAll('.view-toggle button')) {
+ b.classList.toggle('active', b === btn);
+ }
+ renderCurrentView();
+ });
+}
+
+dz.addEventListener('click', () => fileInput.click());
+dz.addEventListener('dragover', (e) => { e.preventDefault(); dz.classList.add('dragover'); });
+dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
+dz.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dz.classList.remove('dragover');
+ handleFiles(e.dataTransfer.files);
+});
+fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
+
+paste.addEventListener('input', () => {
+ const text = paste.value.trim();
+ if (!text) return;
+ try {
+ const parsed = JSON.parse(text);
+ // Accept either a raw array, a single receipt, or verify --json output.
+ const receipts = [];
+ if (Array.isArray(parsed)) {
+ receipts.push(...parsed.filter((r) => r.payload && r.signature));
+ } else if (parsed.receipts) {
+ receipts.push(...(parsed.receipts || []));
+ } else if (parsed.payload && parsed.signature) {
+ receipts.push(parsed);
+ } else if (parsed.nodes) {
+ // `verify chain explore --json` output shape.
+ for (const n of parsed.nodes) {
+ receipts.push({
+ payload: {
+ issued_at: n.issued_at,
+ action: n.action,
+ previousReceiptHash: n.previousHash || undefined,
+ },
+ signature: { kid: n.kid, alg: 'ed25519', sig: '' },
+ });
+ }
+ }
+ ingestReceipts(receipts);
+ } catch {
+ /* ignore — user still typing */
+ }
+});
diff --git a/ecosystem/dashboard/index.html b/ecosystem/dashboard/index.html
new file mode 100644
index 0000000..fdc347e
--- /dev/null
+++ b/ecosystem/dashboard/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Veritas Acta — Audit Dashboard
+
+
+
+
+
A local-first, offline audit dashboard for signed decision receipts. Drop a folder or paste JSON; the dashboard reports chain depth, breaks, and per-receipt status. No data leaves the browser.
+
Verification is a structural check in this scaffold: JCS canonical hash + chain linkage. For full Ed25519 + VOPRF verification, paste results from npx @veritasacta/verify --json and the dashboard will render the output.
+
+
+
+ Drop receipt files here
+
+ or paste verify --json / verify compliance --json output below
+
+
+
+
+
+
+
+
Total receipts
—
+
Valid
—
+
Invalid
—
+
Chain links broken
—
+
First issued
—
+
Last issued
—
+
+
+
+
+
+
+
+ previousReceiptHash chain
+ parent_receipt_id link
+ broken link
+
+
+
+
+
+
Issued at
+
Action
+
Decision
+
Hash
+
Kid
+
Trace
+
Status
+
+
+
+
+
+
+
+
No receipts loaded. Drop files above to begin.
+
+
+
+
+
+
+
diff --git a/ecosystem/ebpf-observer/DESIGN.md b/ecosystem/ebpf-observer/DESIGN.md
new file mode 100644
index 0000000..17824ea
--- /dev/null
+++ b/ecosystem/ebpf-observer/DESIGN.md
@@ -0,0 +1,103 @@
+# OS-Level Auto-Instrumentation (v0.7.0 target — highest novelty)
+
+eBPF (Linux) / Endpoint Security Framework (macOS) / ETW (Windows)
+programs that observe agent processes at the syscall layer and emit
+Veritas Acta receipts for interesting events (tool invocations,
+network calls, file mutations). Zero code changes in the observed
+agent.
+
+## Goal
+
+The ultimate frictionless integration: install the observer, point it
+at a process, and every significant action that process takes gets a
+cryptographic receipt. No SDK, no framework adapter, no code change
+required.
+
+## Design
+
+### Linux (eBPF)
+
+- eBPF program attached to syscalls:
+ - `openat()`, `unlink()` — file mutations
+ - `connect()`, `sendto()` — network activity
+ - `execve()` — subprocess spawning
+- User-space daemon consumes kernel ring buffer events
+- Daemon maintains a per-pid receipt chain
+- Daemon signs receipts using a project attester key
+
+### macOS (Endpoint Security Framework)
+
+- ESF client subscribes to:
+ - `ES_EVENT_TYPE_AUTH_EXEC` — process execution
+ - `ES_EVENT_TYPE_NOTIFY_OPEN` / `NOTIFY_WRITE` — file access
+ - `ES_EVENT_TYPE_NOTIFY_CREATE` — new files
+- ES client requires Team ID + System Extension entitlements
+- Runs as a privileged daemon
+
+### Windows (ETW)
+
+- ETW provider subscription to:
+ - Microsoft-Windows-Kernel-File
+ - Microsoft-Windows-Kernel-Process
+ - Microsoft-Windows-Kernel-Network
+- Agent process pattern matching via image load events
+
+### Composition with sb-runtime
+
+sb-runtime = kernel-level enforcement (deny)
+eBPF observer = kernel-level observation (report)
+
+Together: agents can't do forbidden things (sb-runtime), and everything
+they DO do is receipted (observer).
+
+## Receipt format
+
+```json
+{
+ "payload": {
+ "type": "veritasacta:os-observer:event",
+ "event_kind": "file_write" | "network_connect" | "exec",
+ "pid": 12345,
+ "process_name": "python",
+ "cmdline_hash": "sha256:...",
+ "target": {
+ "kind": "file",
+ "path_hash": "sha256:...",
+ "size_bytes": 1024
+ },
+ "timestamp_nanos": ...
+ },
+ "signature": { ... }
+}
+```
+
+Sensitive fields (full paths, cmdlines) are hashed, not stored raw.
+Reveals happen via selective disclosure (AIP-0002) when needed.
+
+## Privacy implications
+
+- Observes at the syscall layer: can see EVERYTHING
+- Must be deployed with informed user consent
+- Private / sensitive paths configurable via allow / deny filters
+- No phone-home: receipts stay local unless explicitly exported
+
+## Performance
+
+- eBPF adds ~1-5µs per observed syscall (acceptable for moderate
+ volumes)
+- ESF has ~10-50µs overhead
+- High-volume scenarios (>10K syscalls/sec) require event filtering
+ upstream of receipt generation
+
+## Deployment tiers
+
+- **Developer mode**: observer runs as the user, observes only the
+ user's processes, no special privileges (Linux only with ebpf
+ unprivileged mode)
+- **Production mode**: observer runs as root / admin, can observe any
+ process, requires explicit install + trust
+
+## License
+
+Apache-2.0 once shipped. eBPF programs will likely need dual-license
+(GPL kernel compatibility).
diff --git a/ecosystem/github-action/README.md b/ecosystem/github-action/README.md
new file mode 100644
index 0000000..dba3a60
--- /dev/null
+++ b/ecosystem/github-action/README.md
@@ -0,0 +1,68 @@
+# Veritas Acta Verify — GitHub Action
+
+Verify signed decision receipts, VOPRF tokens, or Knowledge Units in any CI workflow. Installs `@veritasacta/verify`, confirms the installed verifier is the canonical unmodified release via Sigil self-check, then verifies your receipts.
+
+## Usage
+
+```yaml
+name: Verify receipts
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: VeritasActa/verify-action@v1
+ with:
+ receipts-path: "./receipts/**/*.json"
+ public-key: ${{ secrets.ED25519_PUBLIC_KEY }}
+ required-tier: 4
+ pin-sigil: 5247a989
+ attest: true
+ attest-org: "Acme Corp"
+ audit-report: "./audit-report.html"
+```
+
+## Inputs
+
+| Input | Required | Default | Description |
+|---|---|---|---|
+| `receipts-path` | yes | — | Glob of receipt files to verify |
+| `public-key` | no | — | Ed25519 pubkey hex |
+| `jwks-url` | no | — | JWKS endpoint URL |
+| `required-tier` | no | — | Minimum conformance tier (1-5) |
+| `pin-sigil` | no | — | Require a specific Sigil fingerprint |
+| `strict` | no | `true` | Disable deprecated fallbacks |
+| `audit-report` | no | — | Path to write HTML audit report |
+| `verifier-version` | no | `latest` | Version of `@veritasacta/verify` to install |
+| `attest` | no | `false` | Emit canonical attestation |
+| `attest-org` | no | — | Org name in attestation |
+
+## Outputs
+
+| Output | Description |
+|---|---|
+| `total` | Total receipts inspected |
+| `verified` | Successfully verified |
+| `failed` | Failed verifications |
+| `sigil-fingerprint` | Fingerprint of the canonical verifier used |
+| `attestation` | Canonical attestation JSON (when `attest: true`) |
+
+## What this action does
+
+1. Installs `@veritasacta/verify` from npm (`--provenance` attested)
+2. Runs `--self-check` to prove the installed verifier is canonical
+3. If `pin-sigil` provided, confirms Sigil fingerprint matches exactly
+4. Verifies each receipt matching `receipts-path`
+5. Optionally emits an HTML audit report as a workflow artifact
+6. Optionally emits a canonical attestation to workflow outputs
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/github-action/action.yml b/ecosystem/github-action/action.yml
new file mode 100644
index 0000000..27f71b9
--- /dev/null
+++ b/ecosystem/github-action/action.yml
@@ -0,0 +1,119 @@
+name: 'Verify with Veritas Acta'
+description: 'Verify signed decision receipts, VOPRF tokens, or Knowledge Units in CI with @veritasacta/verify.'
+author: 'Tom Farley (ScopeBlind)'
+branding:
+ icon: 'shield'
+ color: 'blue'
+
+inputs:
+ receipts-path:
+ description: 'Glob pattern matching receipt files to verify (e.g., "./receipts/**/*.json")'
+ required: true
+ public-key:
+ description: 'Ed25519 public key (64 hex chars) used for verification.'
+ required: false
+ jwks-url:
+ description: 'URL of a JWKS endpoint providing the signing key.'
+ required: false
+ required-tier:
+ description: 'Fail the run if any receipt does not achieve at least this tier (1-5).'
+ required: false
+ default: ''
+ pin-sigil:
+ description: 'Pin to a specific verifier Sigil fingerprint (recommended).'
+ required: false
+ default: ''
+ strict:
+ description: 'Enable --strict mode (disables all deprecated fallbacks).'
+ required: false
+ default: 'true'
+ audit-report:
+ description: 'Write an HTML audit report to this path.'
+ required: false
+ default: ''
+ verifier-version:
+ description: 'Version of @veritasacta/verify to install (defaults to latest).'
+ required: false
+ default: 'latest'
+ attest:
+ description: 'Emit a canonical verifier attestation to the step output.'
+ required: false
+ default: 'false'
+ attest-org:
+ description: 'Org name to include in the canonical attestation.'
+ required: false
+ default: ''
+
+outputs:
+ total:
+ description: 'Total receipts inspected.'
+ verified:
+ description: 'Receipts that verified successfully.'
+ failed:
+ description: 'Receipts that failed verification.'
+ sigil-fingerprint:
+ description: 'Fingerprint of the canonical verifier used.'
+ attestation:
+ description: 'Canonical attestation JSON (when `attest: true`).'
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Install @veritasacta/verify
+ shell: bash
+ run: npm install -g @veritasacta/verify@${{ inputs.verifier-version }}
+
+ - name: Self-check (prove canonical release)
+ shell: bash
+ run: |
+ if [[ -n "${{ inputs.pin-sigil }}" ]]; then
+ npx @veritasacta/verify --pin-sigil ${{ inputs.pin-sigil }}
+ fi
+ npx @veritasacta/verify --self-check
+
+ - name: Verify receipts
+ id: verify
+ shell: bash
+ run: |
+ set -e
+ FAILED=0
+ TOTAL=0
+ for f in $(eval ls ${{ inputs.receipts-path }}); do
+ TOTAL=$((TOTAL + 1))
+ KEY_ARG=""
+ [[ -n "${{ inputs.public-key }}" ]] && KEY_ARG="--key ${{ inputs.public-key }}"
+ [[ -n "${{ inputs.jwks-url }}" ]] && KEY_ARG="--jwks ${{ inputs.jwks-url }}"
+ TIER_ARG=""
+ [[ -n "${{ inputs.required-tier }}" ]] && TIER_ARG="--tier ${{ inputs.required-tier }}"
+ STRICT_ARG=""
+ [[ "${{ inputs.strict }}" == "true" ]] && STRICT_ARG="--strict"
+ if ! npx @veritasacta/verify "$f" $KEY_ARG $TIER_ARG $STRICT_ARG --json > /tmp/result.json; then
+ FAILED=$((FAILED + 1))
+ cat /tmp/result.json
+ fi
+ done
+ VERIFIED=$((TOTAL - FAILED))
+ echo "total=$TOTAL" >> $GITHUB_OUTPUT
+ echo "verified=$VERIFIED" >> $GITHUB_OUTPUT
+ echo "failed=$FAILED" >> $GITHUB_OUTPUT
+ if [[ $FAILED -gt 0 ]]; then exit 1; fi
+
+ - name: Canonical attestation
+ if: ${{ inputs.attest == 'true' }}
+ shell: bash
+ run: |
+ ORG_ARG=""
+ [[ -n "${{ inputs.attest-org }}" ]] && ORG_ARG='--attest-org "${{ inputs.attest-org }}"'
+ npx @veritasacta/verify --attest $ORG_ARG > /tmp/attestation.json
+ cat /tmp/attestation.json
+ echo "attestation<> $GITHUB_OUTPUT
+ cat /tmp/attestation.json >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ - name: Upload audit report
+ if: ${{ inputs.audit-report != '' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: veritasacta-audit-report
+ path: ${{ inputs.audit-report }}
+ if-no-files-found: warn
diff --git a/ecosystem/homebrew-tap/Formula/veritasacta-verify.rb b/ecosystem/homebrew-tap/Formula/veritasacta-verify.rb
new file mode 100644
index 0000000..71296dd
--- /dev/null
+++ b/ecosystem/homebrew-tap/Formula/veritasacta-verify.rb
@@ -0,0 +1,33 @@
+class VeritasactaVerify < Formula
+ desc "Offline verifier for Veritas Acta signed decision receipts"
+ homepage "https://veritasacta.com"
+ url "https://registry.npmjs.org/@veritasacta/verify/-/verify-0.5.0.tgz"
+ sha256 "8c20600b727bb3fe725734d2614fe13b5bccb71ededae7596c7554b2514bfc6b"
+ license "Apache-2.0"
+ head "https://github.com/VeritasActa/verify.git", branch: "main"
+
+ depends_on "node"
+
+ def install
+ system "npm", "install", *Language::Node.std_npm_args(libexec)
+ bin.install_symlink Dir["#{libexec}/bin/*"]
+ end
+
+ def caveats
+ <<~EOS
+ After install, verify you have the canonical release:
+ veritasacta-verify --self-check
+
+ Start signing receipts with:
+ veritasacta-verify init
+
+ Protocol: https://veritasacta.com
+ Managed: https://scopeblind.com (optional)
+ EOS
+ end
+
+ test do
+ # Self-check proves we installed the canonical release
+ assert_match "Canonical verifier", shell_output("#{bin}/veritasacta-verify --self-check")
+ end
+end
diff --git a/ecosystem/homebrew-tap/README.md b/ecosystem/homebrew-tap/README.md
new file mode 100644
index 0000000..fa24ce9
--- /dev/null
+++ b/ecosystem/homebrew-tap/README.md
@@ -0,0 +1,35 @@
+# VeritasActa Homebrew Tap
+
+Install `@veritasacta/verify` via Homebrew.
+
+## Usage
+
+```bash
+brew tap VeritasActa/verify
+brew install veritasacta-verify
+```
+
+After install:
+
+```bash
+verify --self-check
+verify init
+verify samples/sample-receipt.json --key
+```
+
+## Formula
+
+See `Formula/veritasacta-verify.rb`. The formula wraps `npm install -g @veritasacta/verify` with a Sigil self-check step on post-install, so every Homebrew installation cryptographically confirms it got the canonical release.
+
+## Deployment
+
+This directory is the source for the `VeritasActa/homebrew-verify` GitHub repo. To publish:
+
+```bash
+cd ecosystem/homebrew-tap
+gh repo create VeritasActa/homebrew-verify --public --source=. --push
+```
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/interop-leaderboard/workflow.yml b/ecosystem/interop-leaderboard/workflow.yml
new file mode 100644
index 0000000..c9137e4
--- /dev/null
+++ b/ecosystem/interop-leaderboard/workflow.yml
@@ -0,0 +1,114 @@
+name: Interop Leaderboard
+
+on:
+ schedule:
+ - cron: '0 12 * * 0' # every Sunday 12:00 UTC
+ workflow_dispatch:
+
+jobs:
+ interop-matrix:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ implementation:
+ - name: protect-mcp
+ fixture-repo: scopeblind/scopeblind-gateway
+ fixture-path: test-vectors/receipts/*.json
+ - name: protect-mcp-adk
+ fixture-repo: scopeblind/protect-mcp-adk
+ fixture-path: tests/fixtures/receipts/*.json
+ - name: sb-runtime
+ fixture-repo: ScopeBlind/sb-runtime
+ fixture-path: tests/fixtures/*.json
+ - name: aps-governance-hook
+ fixture-repo: aeoess/agent-passport-system
+ fixture-path: test-vectors/*.json
+ - name: signet
+ fixture-repo: Prismer-AI/signet
+ fixture-path: fixtures/*.json
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install canonical verifier
+ run: npm install -g @veritasacta/verify@latest
+
+ - name: Self-check (prove canonical)
+ run: npx @veritasacta/verify --self-check
+
+ - name: Fetch ${{ matrix.implementation.name }} fixtures
+ run: |
+ mkdir -p fixtures/${{ matrix.implementation.name }}
+ git clone --depth=1 https://github.com/${{ matrix.implementation.fixture-repo }} /tmp/impl-repo || echo "Fixture repo not accessible (may be private)"
+ cp -r /tmp/impl-repo/${{ matrix.implementation.fixture-path }} fixtures/${{ matrix.implementation.name }}/ 2>/dev/null || echo "No fixtures found for pattern"
+
+ - name: Verify all fixtures
+ id: verify
+ run: |
+ set +e
+ TOTAL=0
+ PASSED=0
+ FAILED=0
+ for f in fixtures/${{ matrix.implementation.name }}/*.json; do
+ TOTAL=$((TOTAL + 1))
+ if npx @veritasacta/verify "$f" --strict > /tmp/result-$TOTAL.json 2>&1; then
+ PASSED=$((PASSED + 1))
+ else
+ FAILED=$((FAILED + 1))
+ fi
+ done
+ echo "total=$TOTAL" >> $GITHUB_OUTPUT
+ echo "passed=$PASSED" >> $GITHUB_OUTPUT
+ echo "failed=$FAILED" >> $GITHUB_OUTPUT
+ {
+ echo "impl=${{ matrix.implementation.name }}"
+ echo "total=$TOTAL"
+ echo "passed=$PASSED"
+ echo "failed=$FAILED"
+ echo "timestamp=$(date -Iseconds)"
+ } > /tmp/result-${{ matrix.implementation.name }}.env
+
+ - name: Upload per-implementation result
+ uses: actions/upload-artifact@v4
+ with:
+ name: result-${{ matrix.implementation.name }}
+ path: /tmp/result-*.env
+
+ aggregate:
+ needs: interop-matrix
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download all results
+ uses: actions/download-artifact@v4
+ with:
+ path: results/
+
+ - name: Build leaderboard markdown
+ run: |
+ {
+ echo "# Interop Leaderboard"
+ echo ""
+ echo "Auto-generated every Sunday. Each row represents verifying that implementation's fixtures with canonical @veritasacta/verify."
+ echo ""
+ echo "| Implementation | Total | Passed | Failed | Status |"
+ echo "|---|---|---|---|---|"
+ for r in results/*/result-*.env; do
+ [[ -f "$r" ]] || continue
+ source "$r"
+ STATUS="✓ interop"
+ [[ "$failed" != "0" ]] && STATUS="⚠ partial ($failed failing)"
+ [[ "$total" == "0" ]] && STATUS="? no fixtures"
+ echo "| $impl | $total | $passed | $failed | $STATUS |"
+ done
+ echo ""
+ echo "*Last updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)*"
+ } > LEADERBOARD.md
+ cat LEADERBOARD.md
+
+ - name: Commit leaderboard
+ run: |
+ git config user.name "Veritas Acta Interop Bot"
+ git config user.email "interop-bot@veritasacta.com"
+ git add LEADERBOARD.md
+ git diff --cached --quiet && echo "no changes" || (git commit -m "chore: update interop leaderboard ($(date -I))" && git push)
diff --git a/ecosystem/physical-attestation/DESIGN.md b/ecosystem/physical-attestation/DESIGN.md
new file mode 100644
index 0000000..89b054d
--- /dev/null
+++ b/ecosystem/physical-attestation/DESIGN.md
@@ -0,0 +1,211 @@
+# Physical-Digital Causal Chains — Seal cost-tier T2 attestation
+
+**Status:** Design spec (v0.7 target)
+**Related:** AIP-0005 (Attestation Weight Profile), `memory/seal-hardware-strategy.md`
+
+## Goal
+
+Extend the signed decision receipt format so that receipts can claim
+**T2 = hardware-attested** (per AIP-0005) when the signing key lives
+inside a hardware secure element (ATECC608B, TA100, SE050, TPM2, SGX,
+etc.) and a platform quote binds the key to that element.
+
+This turns receipts from "a software process signed this" into "a
+specific hardware element, whose key was provisioned under declared
+procedure, signed this." That's the property cold-chain operators,
+regulatory auditors, and insurance adjusters need.
+
+## Why this matters
+
+Software-only receipts can be forged by an attacker who steals the
+signing key. No amount of cryptographic sophistication in the receipt
+format can prevent key exfiltration from a compromised OS. T2
+receipts raise the forgery cost from "compromise the OS" to "extract
+a key from a tamper-resistant chip" — a 1000-2000x increase in
+attacker cost (numbers from EAL certification bodies).
+
+The Seal hardware program exists specifically to produce this kind of
+evidence for physical events (temperature chains, shipping custody,
+tamper seals). ScopeBlind's strategic position is that the same
+receipt format handles both purely-digital decisions AND
+physical-digital chains where some links came from a sensor.
+
+## Architecture
+
+### Receipt shape (additive over AIP-0001)
+
+A T2-claiming receipt adds two fields to the existing payload:
+
+```json
+{
+ "payload": {
+ "type": "decision-receipt",
+ "action": "physical.chain_of_custody.temperature_reading",
+ "cost_tier": 2,
+ "attestation_mode": "atecc608b" | "ta100" | "se050" | "tpm2" | "sgx",
+ "attestation_quote": {
+ "format": "atecc608b-signed-data-v1",
+ "quote": "",
+ "measured_kid": "",
+ "provisioning_ca": ""
+ },
+ ... existing AIP-0001 fields ...
+ },
+ "signature": { "alg": "ES256", "kid": "", "sig": "..." }
+}
+```
+
+`attestation_quote` is the vendor-specific platform attestation. For
+ATECC608B (our Seal v1), it's the `Sign()` operation over a challenge
+bound to the receipt payload hash, with the chip's `provisioner_cert`
+chain. The verifier checks three things:
+
+1. The chip's `provisioning_ca` is a trusted root (Microchip's CA, or
+ our own provisioning CA for Seal v1).
+2. The quote signs the receipt's canonical payload hash with
+ `measured_kid`.
+3. `signature.kid` equals `measured_kid` — the same key that signed
+ the receipt also produced the platform quote.
+
+### Sensor reading → decision receipt pipeline
+
+```
+┌─────────────────────────┐ ┌─────────────────────────┐
+│ SHT40 sensor │ │ ATECC608B secure element│
+│ reads temp + humidity │ │ holds ECDSA P-256 key │
+└────────────┬────────────┘ │ exports platform quote │
+ │ I²C │ │
+ ▼ └─────────────┬───────────┘
+┌────────────────────────────────────────────┐ │
+│ nRF52840 MCU │ │
+│ - constructs JCS receipt payload │ │
+│ - hashes payload │ │
+│ - asks ATECC608B to sign + quote │◄┘
+│ - emits { payload, signature, quote } │
+└────────────┬───────────────────────────────┘
+ │ BLE / NFC / LoRa
+ ▼
+ Phone / gateway / verifier (offline)
+ validates signature + quote + cost_tier=T2
+```
+
+The sensor is NOT trusted; only the ATECC608B's signature is. What the
+chip signs is whatever the firmware hands it. If the firmware is
+compromised to misreport temperature, T2 doesn't save you — that's why
+T2 is a property claim about the SIGNING, not about the truthfulness of
+the data. Higher tiers (T3 multi-party, T4 transparency-anchored) add
+additional cross-checks.
+
+## Verifier requirements
+
+A conformant verifier implementing this extension SHOULD:
+
+1. When `cost_tier >= 2` is claimed, REQUIRE `attestation_mode` and
+ `attestation_quote` fields.
+2. Dispatch to a per-platform validator based on `attestation_mode`:
+ - `atecc608b` → validate the Microchip provisioner chain + re-verify
+ the chip's signature over the receipt hash.
+ - `tpm2` → validate the TPM2 AK certificate + quote structure.
+ - `sgx` / `sev-snp` / `tdx` → platform-specific quote verification.
+3. If the platform is unknown, return `undecidable` (exit 2) rather
+ than `invalid` (exit 1) — the receipt might be valid under a
+ platform the verifier doesn't know.
+4. If the platform is known but the quote fails, return `invalid`
+ (exit 1) — this is a proven mismatch.
+
+The verifier is NOT required to implement all platforms. A verifier
+might implement TPM2 only and punt on ATECC608B. It just must clearly
+report *why* it can't verify.
+
+## Implementation path
+
+### v0.5.2 (now, scaffold)
+
+This spec + the `attestation_quote` field schema shipped under
+`schemas/` + AIP-0005 tier mapping. No verifier code yet — the tier
+is parseable but not validated.
+
+### v0.6.0
+
+Minimal validator for `attestation_mode: custom` that checks the
+quote is a well-formed object with `format`, `quote`, and
+`measured_kid`, and verifies `measured_kid == signature.kid`. That's
+the "at least it's consistent" check. Full platform-specific
+validators deferred.
+
+### v0.7.0
+
+One real platform validator — target is TPM2 because the quote
+format is well-documented and reference libraries exist (tpm2-tss).
+Second validator (ATECC608B, for Seal) added once the chip's
+provisioning chain is formalised.
+
+### v1.0+
+
+All major secure element platforms: ATECC608B, TA100, SE050, TPM2,
+SGX/SEV-SNP/TDX, SE embedded in ARM TrustZone phones.
+
+## Relationship to Seal hardware program
+
+Seal v1 ships with ATECC608B + ECDSA P-256. Every reading produces a
+receipt whose `cost_tier = 2` and whose `attestation_mode =
+atecc608b`. The quote is the chip's authenticated sign operation.
+The `provisioning_ca` is a ScopeBlind-operated intermediate CA; when
+Microchip certifies our provisioning, we'll also ship the Microchip
+chain for cross-verification.
+
+Before Seal ships, the T2 mechanism is still useful: any software
+running on a TPM-backed server, an Apple device with Secure Enclave,
+or a KMS-backed CloudHSM can claim T2 today. The receipt format
+doesn't care; only the evidence matters.
+
+## Non-goals
+
+- Not a replacement for TEE attestation protocols. We don't reinvent
+ TPM2 / SGX / SEV — we reference their quotes verbatim.
+- Not a DRM mechanism. T2 says "a specific hardware element signed
+ this." It doesn't prevent the holder from doing other things with
+ the receipt.
+- Not a hardware supply-chain proof. If the chip itself was
+ adversarially backdoored, T2 can't detect that. Supply-chain
+ provenance is a parallel problem; AIP-0005 tiers describe one
+ property (cost to forge the signature), not every property.
+
+## Open questions
+
+1. How do we represent ATECC608B's non-standard signature format
+ when the verifier needs a standard ECDSA verification path?
+ (Current plan: firmware emits the raw `r` + `s` and the verifier
+ wraps them in DER at receive time.)
+2. For Seal, do we ship our own provisioning CA publicly or gate it
+ behind a trust-anchor file operators import? (Current plan:
+ publicly published + trust-anchor-file optional for private
+ deployments.)
+3. Should the verifier treat unknown platforms as an error or
+ silently downgrade the tier to T0? The spec says `undecidable`
+ (exit 2). An alternative is "accept the receipt but surface the
+ tier as T0 because we couldn't validate T2." This is a UX
+ question, not a security question.
+
+## Reference implementation sketch
+
+Until v0.6.0 ships the validator, the schema below is the interface.
+
+```typescript
+interface AttestationQuote {
+ /** Vendor-specific format identifier. */
+ format: string;
+ /** Raw quote bytes, base64url-encoded. */
+ quote: string;
+ /** Key identifier that MUST match the receipt's signature.kid. */
+ measured_kid: string;
+ /** Provisioning CA chain (X.509 x5c) or fingerprint list. */
+ provisioning_ca?: string | string[];
+ /** Reference values (PCRs, IMA hashes, etc.) the operator expects. */
+ reference_values?: Record;
+}
+```
+
+## License
+
+Apache-2.0 when shipped.
diff --git a/ecosystem/physical-attestation/attestation-quote.schema.json b/ecosystem/physical-attestation/attestation-quote.schema.json
new file mode 100644
index 0000000..a5312a7
--- /dev/null
+++ b/ecosystem/physical-attestation/attestation-quote.schema.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://veritasacta.com/schemas/attestation-quote.schema.json",
+ "title": "AIP-0005 T2 Attestation Quote",
+ "description": "Platform attestation quote binding a signing key to a hardware secure element.",
+ "type": "object",
+ "required": ["format", "quote", "measured_kid"],
+ "additionalProperties": false,
+ "properties": {
+ "format": {
+ "type": "string",
+ "description": "Vendor-specific quote format identifier.",
+ "enum": [
+ "atecc608b-signed-data-v1",
+ "ta100-signed-data-v1",
+ "se050-attestation-v1",
+ "tpm2-quote-v1",
+ "sgx-dcap-v3",
+ "sev-snp-report-v1",
+ "tdx-quote-v4",
+ "apple-secure-enclave-v1",
+ "custom"
+ ]
+ },
+ "quote": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]+={0,2}$",
+ "description": "Raw quote bytes, base64url-encoded."
+ },
+ "measured_kid": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Key identifier bound in the quote. MUST match the enclosing receipt's signature.kid."
+ },
+ "provisioning_ca": {
+ "oneOf": [
+ { "type": "string", "description": "PEM-encoded CA certificate or fingerprint." },
+ {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "X.509 chain as array of x5c entries."
+ }
+ ]
+ },
+ "reference_values": {
+ "type": "object",
+ "description": "Expected reference measurements (PCRs, IMA hashes, memory ranges). Platform-specific schema.",
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/ecosystem/profiles/README.md b/ecosystem/profiles/README.md
new file mode 100644
index 0000000..820c950
--- /dev/null
+++ b/ecosystem/profiles/README.md
@@ -0,0 +1,65 @@
+# Pre-built sandbox profiles
+
+Drop-in Cedar policies + nono capability manifests for common AI coding assistants and agent CLIs. Each profile is designed so the tool **just works under sb-runtime + nono + signed receipts** without requiring the operator to author a policy from scratch.
+
+## Available profiles
+
+| Profile | Target tool | Policy shape |
+|---|---|---|
+| [`claude-code/`](./claude-code/) | [Anthropic Claude Code](https://www.claude.com/product/claude-code) | Allow read-only source inspection + scoped git/tests; deny network to metadata endpoints, deny writes to system paths |
+| [`cursor/`](./cursor/) | [Cursor](https://www.cursor.com/) | Same baseline as claude-code + additional allow for Cursor's MCP bridge |
+| [`codex/`](./codex/) | [OpenAI Codex CLI](https://github.com/openai/codex) | Stricter default deny, explicit allow per exec |
+| [`gemini-cli/`](./gemini-cli/) | [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) | Read-only review mode + scoped shell |
+| [`openclaw/`](./openclaw/) | [OpenClaw](https://github.com/openclaw-ai/openclaw) | OpenClaw-aligned conventions; composes with their existing guard rails |
+
+## Each profile contains
+
+```
+/
+├── profile.yaml # metadata: tool, version, recommended sb-runtime ring, nono capability refs
+├── policy.cedar # Cedar policy (consumable by sb-runtime, bindu-scopeblind, cedar-for-agents)
+├── nono-capabilities.yaml # nono capability set (if composing with nono for kernel sandbox)
+└── README.md # how to use + what it allows / denies
+```
+
+## Usage
+
+### Option A: via `verify init`
+
+```bash
+cd my-project
+npx @veritasacta/verify init --profile claude-code
+```
+
+Auto-installs the profile, generates keys, writes `.veritasacta/config.json`.
+
+### Option B: via sb-runtime directly
+
+```bash
+sb-runtime run --profile claude-code --ring 3 -- claude
+```
+
+### Option C: copy manually
+
+Copy `policy.cedar` into your Cedar policy directory. The file is pure Cedar and works with any compliant evaluator.
+
+## Why profiles?
+
+A Cedar policy written from scratch for `claude-code` takes a new operator ~1-2 hours of trial and error (allow this syscall, deny that path, figure out what Claude Code actually does under the hood). Profiles compress that to one command.
+
+The profiles are maintained by the ScopeBlind ecosystem team based on real deployment experience. Each profile is explicit about what it allows / denies; there are no hidden escape hatches, and every change is reviewed.
+
+## Contributing a profile
+
+Adding a new tool is a small PR:
+
+1. `/profile.yaml` with metadata
+2. `/policy.cedar` with the policy
+3. `/README.md` explaining the threat model and design decisions
+4. Optional: `/nono-capabilities.yaml` for kernel-sandbox composition
+
+All profiles live in `packages/verify-cli/ecosystem/profiles/` and are Apache-2.0.
+
+## License
+
+Apache-2.0. Each profile is usable in any context without restriction; the Veritas Acta receipt format is the interoperable artifact.
diff --git a/ecosystem/profiles/claude-code/README.md b/ecosystem/profiles/claude-code/README.md
new file mode 100644
index 0000000..ae7021f
--- /dev/null
+++ b/ecosystem/profiles/claude-code/README.md
@@ -0,0 +1,72 @@
+# Profile: Claude Code
+
+Run [Anthropic Claude Code](https://www.claude.com/product/claude-code) under sb-runtime + nono + signed receipts with one command.
+
+## Quick start
+
+```bash
+# Option A: via verify init
+cd my-project
+npx @veritasacta/verify init --profile claude-code
+
+# Option B: via sb-runtime directly
+sb-runtime run --profile ./profile.yaml --ring 3 -- claude
+
+# Option C: nono + sb-runtime composition (Linux, recommended for production)
+nono run --caps ./nono-capabilities.yaml -- \
+ sb-runtime --ring 2 --policy ./policy.cedar -- claude
+```
+
+## What the profile allows
+
+- **Reads** from `/workspace/*` and `/tmp/*`
+- **Writes** inside `/workspace/*` only
+- **Exec** of a safe allowlist: `ls`, `cat`, `grep`, `rg`, `git`, `pytest`, `npm`, `node`, `cargo`, `go`, and similar dev tools
+- **HTTPS fetches** to `api.github.com`, `githubusercontent.com`, `anthropic.com`, `pypi.org`, `registry.npmjs.org`
+
+## What the profile denies
+
+- Cloud metadata endpoints (AWS IMDS, GCP metadata, IPv6 equivalents)
+- Writes to `/etc`, `/usr`, `/root`, `/var/log`, `/sys`, `/proc`
+- Reads of credential patterns (`*.pem`, SSH keys, AWS creds, `.netrc`, `secrets.*`)
+- Destructive shell commands (`rm`, `dd`, `mkfs`, `shutdown`, force-push to git)
+
+## Receipt format
+
+Every tool call emits a receipt at `.veritasacta/receipts/claude-code/.json` in the Veritas Acta format (draft-farley-acta-signed-receipts-02). Verify offline:
+
+```bash
+npx @veritasacta/verify .veritasacta/receipts/claude-code/ --key operator-public.pem
+```
+
+## When to customise
+
+- **Cursor, Codex, or another IDE-embedded agent** — copy this profile and relax the HTTPS allowlist (Cursor talks to its own backend)
+- **Production CI** — tighten to deny-by-default on exec, add an explicit allowlist only for your build commands
+- **Enterprise with air-gap** — remove the network block entirely, keep only Cedar allow/deny on file and exec
+
+## Threat model
+
+This profile assumes Claude Code is **semi-trusted**: we trust the binary is unmodified (verified via Anthropic's signing) but don't trust any prompt or tool-use output to be safe. The sandbox is the enforcement boundary; Cedar policy is the audit boundary.
+
+Known attack vectors this profile addresses:
+
+- **Credential exfiltration via file read** — blocked at policy + nono filesystem layer
+- **Cloud metadata access (SSRF-like)** — blocked at network layer
+- **Destructive shell via LLM confusion** — blocked at Cedar exec allowlist
+- **System-file tampering** — blocked at policy + nono filesystem layer
+
+Known attack vectors **NOT** addressed (need operator-specific handling):
+
+- **Data exfiltration via allowed HTTPS endpoints** — Claude Code can post to api.github.com; if that's a real concern, narrow the allowlist further
+- **Supply-chain attacks via allowed package managers** — `npm install` of a malicious package is not blocked; use [`verify prompt `](../../../src/engines/prompt.js) to verify `.claude/settings.json` and `CLAUDE.md` provenance
+
+## Maintaining this profile
+
+Profile updates follow semver. The current version is 1.0.0. Changes:
+
+- **Patch** — allowlist additions for already-covered command families
+- **Minor** — new action types (e.g., a new Claude Code tool), new endpoint allowlist entries
+- **Major** — changes to default-deny posture or removal of previously-allowed patterns
+
+File issues or proposed updates in the main [`VeritasActa/verify`](https://github.com/VeritasActa/verify) repo.
diff --git a/ecosystem/profiles/claude-code/nono-capabilities.yaml b/ecosystem/profiles/claude-code/nono-capabilities.yaml
new file mode 100644
index 0000000..c403b06
--- /dev/null
+++ b/ecosystem/profiles/claude-code/nono-capabilities.yaml
@@ -0,0 +1,79 @@
+# nono capability set for running Claude Code under kernel-native sandbox.
+# Use alongside sb-runtime --ring 2 so nono owns the sandbox layer and
+# sb-runtime contributes only Cedar evaluation + signed receipts.
+#
+# See: https://github.com/always-further/nono
+
+version: "1"
+profile: claude-code
+
+capabilities:
+ filesystem:
+ allow_read:
+ - /workspace
+ - /tmp
+ - /usr/bin
+ - /usr/lib
+ - /lib
+ - /etc/ssl/certs # TLS trust roots for HTTPS to github.com etc
+ allow_readwrite:
+ - /workspace
+ deny:
+ - /etc
+ - /var
+ - /root
+ - /home # except whatever's bind-mounted to /workspace
+ - /proc/*/mem # stop memory inspection
+
+ network:
+ allow_egress:
+ - api.github.com
+ - "*.githubusercontent.com"
+ - "*.anthropic.com"
+ - api.pypi.org
+ - registry.npmjs.org
+ deny:
+ - 169.254.169.254 # AWS IMDS
+ - metadata.google.internal
+ - fd00:ec2::254 # IPv6 IMDS
+ block_all_other: true
+
+ syscalls:
+ # seccomp-notify for runtime supervisor upgrades; default allowlist
+ # covers normal fs + net + proc control for subprocess management.
+ default: allowlist
+ allowed:
+ - read
+ - write
+ - openat
+ - close
+ - fstat
+ - mmap
+ - brk
+ - execve # gated by Cedar policy per-command
+ - clone
+ - wait4
+ - pipe2
+ - fork
+ - dup2
+ - accept4
+ - connect
+ - bind
+ - listen
+ - socket
+ - rt_sigaction
+ - rt_sigreturn
+ - exit_group
+ denied:
+ - ptrace
+ - process_vm_readv
+ - keyctl
+ - personality
+ - pivot_root
+ - chroot
+ - mount
+
+# When nono is composed with sb-runtime --ring 2, each capability check
+# also produces a Veritas Acta receipt per the sb-runtime skill
+# (packages/agentmesh-integrations/sb-runtime-skill/). See
+# docs/integrations/sb-runtime.md.
diff --git a/ecosystem/profiles/claude-code/policy.cedar b/ecosystem/profiles/claude-code/policy.cedar
new file mode 100644
index 0000000..f2933f2
--- /dev/null
+++ b/ecosystem/profiles/claude-code/policy.cedar
@@ -0,0 +1,120 @@
+// Cedar policy for Claude Code running under sb-runtime + nono.
+// Conservative by default. Explicit allowlists for tool calls Claude Code
+// actually needs; explicit deny for everything else.
+//
+// Scope: covers the governed actions emitted by Claude Code's built-in
+// tools (Bash, Read, Write, Edit, Grep, Glob, WebFetch, WebSearch) plus
+// the MCP tool_use action shape.
+
+// ------------------------------------------------------------------ READS
+// Allow reading inside the workspace and /tmp.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (resource.path like "/workspace/*" || resource.path like "/tmp/*")
+};
+
+// ------------------------------------------------------------------ WRITES
+// Allow writes inside workspace only. /tmp is read-only so agents can't
+// hide artefacts there.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" &&
+ resource.path like "/workspace/*"
+};
+
+// ------------------------------------------------------------------ EXEC
+// Allow safe shell commands for common Claude Code workflows.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in [
+ "ls", "cat", "grep", "rg", "find", "head", "tail", "wc",
+ "git", "pytest", "npm", "npx", "yarn", "pnpm",
+ "python", "python3", "node", "deno", "bun",
+ "cargo", "rustc", "go", "gofmt"
+ ]
+};
+
+// ------------------------------------------------------------------ NETWORK
+// Allow HTTPS fetches to common developer domains Claude Code uses.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ context.tls == true &&
+ (
+ resource.host like "*.github.com" ||
+ resource.host like "*.githubusercontent.com" ||
+ resource.host like "*.anthropic.com" ||
+ resource.host like "api.pypi.org" ||
+ resource.host like "registry.npmjs.org"
+ )
+};
+
+// ------------------------------------------------------------------ DENYS
+// Block cloud metadata endpoints regardless of any allow above.
+forbid (
+ principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ resource.host == "169.254.169.254" ||
+ resource.host == "fd00:ec2::254" ||
+ resource.host == "metadata.google.internal" ||
+ resource.host like "metadata.*.internal"
+};
+
+// Block writes to system directories.
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" &&
+ (
+ resource.path like "/etc/*" ||
+ resource.path like "/usr/*" ||
+ resource.path like "/root/*" ||
+ resource.path like "/var/log/*" ||
+ resource.path like "/sys/*" ||
+ resource.path like "/proc/*"
+ )
+};
+
+// Block reads of credential files even under /workspace (paranoid default).
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (
+ resource.path like "*.pem" ||
+ resource.path like "*id_rsa*" ||
+ resource.path like "*id_ed25519*" ||
+ resource.path like "*/.aws/credentials" ||
+ resource.path like "*/.ssh/*" ||
+ resource.path like "*/.netrc" ||
+ resource.path like "*/secrets.*"
+ )
+};
+
+// Block destructive shell commands.
+forbid (
+ principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "pkill"] ||
+ (context.command == "git" && context.argv.contains("push") && context.argv.contains("--force"))
+};
diff --git a/ecosystem/profiles/claude-code/profile.yaml b/ecosystem/profiles/claude-code/profile.yaml
new file mode 100644
index 0000000..409bcef
--- /dev/null
+++ b/ecosystem/profiles/claude-code/profile.yaml
@@ -0,0 +1,29 @@
+apiVersion: scopeblind.profiles/v1
+kind: SandboxProfile
+metadata:
+ name: claude-code
+ tool: Claude Code (Anthropic)
+ tool_url: https://www.claude.com/product/claude-code
+ profile_version: 1.0.0
+ maintained_by: ScopeBlind
+ license: Apache-2.0
+ veritas_acta_receipt_type: scopeblind:decision
+spec:
+ sb_runtime:
+ recommended_ring: 3
+ sandbox_backend: sb_runtime_builtin
+ receipts_dir: .veritasacta/receipts/claude-code
+ policy_file: ./policy.cedar
+ nono:
+ capability_file: ./nono-capabilities.yaml
+ recommended_mode: compose # sb-runtime at ring 2, nono wraps process
+ intended_threat_model:
+ - Claude Code agent reads source, writes in workspace, runs test commands
+ - Agent cannot reach cloud metadata endpoints
+ - Agent cannot escalate outside /workspace, /tmp
+ - Agent cannot write to /etc, /usr, /root, /var/log
+ - Every tool_use produces a signed receipt linked to prior
+ known_limitations:
+ - Claude Code's MCP tool integration creates syscalls outside this profile's scope;
+ set `--allow-mcp-egress` to add a tight allowlist for specific MCP server hosts
+ - Hard-stops on aarch64 Linux sandbox until issue #1 lands
diff --git a/ecosystem/profiles/codex/README.md b/ecosystem/profiles/codex/README.md
new file mode 100644
index 0000000..efaf65b
--- /dev/null
+++ b/ecosystem/profiles/codex/README.md
@@ -0,0 +1,41 @@
+# Profile: OpenAI Codex CLI
+
+Stricter default than `claude-code` because Codex CLI's tool behavior is more aggressive and the profile assumes a CI / unattended deployment context.
+
+## Key differences from claude-code
+
+- **Exec default-deny**: only `ls`, `cat`, `grep`, `rg`, `find`, `head`, `tail`, `wc` are allowed. Add your build / test commands explicitly per project.
+- **Network narrower**: only `*.openai.com`, `api.github.com`, `githubusercontent.com`, `pypi.org`, `npmjs.org`.
+- **`chmod` / `chown` denied**: Codex has a pattern of suggesting permission changes; blocking at the policy layer forces explicit operator action.
+
+## Extending for your project
+
+Copy `policy.cedar` and add a project-specific allowlist:
+
+```cedar
+// Your project-specific build commands
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["npm", "pytest", "cargo"] // whatever your stack needs
+};
+```
+
+Keep the original deny rules; they're defense in depth.
+
+## Composition
+
+Standalone (sb-runtime Ring 3):
+
+```bash
+sb-runtime --ring 3 --profile ./profile.yaml -- codex
+```
+
+With nono (recommended for production):
+
+```bash
+nono run --caps ./nono-capabilities.yaml -- \
+ sb-runtime --ring 2 --policy ./policy.cedar -- codex
+```
diff --git a/ecosystem/profiles/codex/nono-capabilities.yaml b/ecosystem/profiles/codex/nono-capabilities.yaml
new file mode 100644
index 0000000..46247e3
--- /dev/null
+++ b/ecosystem/profiles/codex/nono-capabilities.yaml
@@ -0,0 +1,23 @@
+version: "1"
+profile: codex
+capabilities:
+ filesystem:
+ allow_read: [/workspace, /tmp, /usr/bin, /usr/lib, /lib, /etc/ssl/certs]
+ allow_readwrite: [/workspace]
+ deny: [/etc, /var, /root, /home, /proc/*/mem]
+ network:
+ allow_egress:
+ - "*.openai.com"
+ - api.github.com
+ - "*.githubusercontent.com"
+ - api.pypi.org
+ - registry.npmjs.org
+ deny:
+ - 169.254.169.254
+ - metadata.google.internal
+ - fd00:ec2::254
+ block_all_other: true
+ syscalls:
+ default: allowlist
+ allowed: [read, write, openat, close, fstat, mmap, brk, execve, clone, wait4, pipe2, fork, dup2, accept4, connect, bind, listen, socket, rt_sigaction, rt_sigreturn, exit_group]
+ denied: [ptrace, process_vm_readv, keyctl, personality, pivot_root, chroot, mount]
diff --git a/ecosystem/profiles/codex/policy.cedar b/ecosystem/profiles/codex/policy.cedar
new file mode 100644
index 0000000..713d269
--- /dev/null
+++ b/ecosystem/profiles/codex/policy.cedar
@@ -0,0 +1,76 @@
+// Cedar policy for OpenAI Codex CLI — stricter default than claude-code.
+// Default-deny on exec; operator narrows allowlist per project.
+
+// Read-only in workspace + /tmp
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (resource.path like "/workspace/*" || resource.path like "/tmp/*")
+};
+
+// Writes in workspace only
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" && resource.path like "/workspace/*"
+};
+
+// Exec is DENY-BY-DEFAULT. Project-specific operators must explicitly
+// extend this allowlist. Only the safest inspection commands are allowed.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["ls", "cat", "grep", "rg", "find", "head", "tail", "wc"]
+};
+
+// Network: OpenAI API + package managers only.
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ context.tls == true &&
+ (
+ resource.host like "*.openai.com" ||
+ resource.host like "api.github.com" ||
+ resource.host like "*.githubusercontent.com" ||
+ resource.host like "api.pypi.org" ||
+ resource.host like "registry.npmjs.org"
+ )
+};
+
+// Universal denies
+forbid (
+ principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ resource.host == "169.254.169.254" ||
+ resource.host == "metadata.google.internal" ||
+ resource.host == "fd00:ec2::254" ||
+ resource.host like "metadata.*.internal"
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ (context.mode == "write" && (resource.path like "/etc/*" || resource.path like "/usr/*" || resource.path like "/root/*" || resource.path like "/var/log/*")) ||
+ (context.mode == "read" && (resource.path like "*.pem" || resource.path like "*id_rsa*" || resource.path like "*id_ed25519*" || resource.path like "*/.aws/credentials" || resource.path like "*/.ssh/*" || resource.path like "*/.netrc"))
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "pkill", "chmod", "chown"]
+};
diff --git a/ecosystem/profiles/codex/profile.yaml b/ecosystem/profiles/codex/profile.yaml
new file mode 100644
index 0000000..940ca38
--- /dev/null
+++ b/ecosystem/profiles/codex/profile.yaml
@@ -0,0 +1,21 @@
+apiVersion: scopeblind.profiles/v1
+kind: SandboxProfile
+metadata:
+ name: codex
+ tool: OpenAI Codex CLI
+ tool_url: https://github.com/openai/codex
+ profile_version: 1.0.0
+ maintained_by: ScopeBlind
+ license: Apache-2.0
+spec:
+ sb_runtime:
+ recommended_ring: 3
+ sandbox_backend: sb_runtime_builtin
+ receipts_dir: .veritasacta/receipts/codex
+ policy_file: ./policy.cedar
+ nono:
+ capability_file: ./nono-capabilities.yaml
+ intended_threat_model:
+ - Codex CLI is aggressive by default; policy is stricter than claude-code
+ - Default-deny on exec; operator opts specific commands in per project
+ - Network limited to OpenAI API + package managers
diff --git a/ecosystem/profiles/cursor/README.md b/ecosystem/profiles/cursor/README.md
new file mode 100644
index 0000000..ff3dd6d
--- /dev/null
+++ b/ecosystem/profiles/cursor/README.md
@@ -0,0 +1,31 @@
+# Profile: Cursor
+
+Run [Cursor](https://www.cursor.com) under sb-runtime + nono + signed receipts.
+
+Baseline is identical to [`claude-code`](../claude-code/) with a tighter network allowlist scoped to Cursor's own backends plus standard developer domains. Use this profile when Cursor's Composer mode, agent tabs, or MCP bridge is active.
+
+## Quick start
+
+```bash
+npx @veritasacta/verify init --profile cursor
+```
+
+Or compose with nono directly (Linux, recommended for production):
+
+```bash
+nono run --caps ./nono-capabilities.yaml -- \
+ sb-runtime --ring 2 --policy ./policy.cedar -- cursor
+```
+
+## What's different from claude-code
+
+- **Network**: adds `*.cursor.com` and `*.cursor.sh` to the HTTPS allowlist (Cursor's LLM proxy and telemetry endpoints)
+- **MCP extension**: if you wire third-party MCP servers through Cursor, extend the network allowlist per server. Each MCP server hostname should be explicit.
+
+Otherwise: same reads, writes, exec allowlist, and denies as `claude-code`.
+
+## Receipt format
+
+Same as claude-code. Receipts land at `.veritasacta/receipts/cursor/` and verify with `npx @veritasacta/verify`.
+
+See [`claude-code/README.md`](../claude-code/README.md) for the full threat model and composition pattern.
diff --git a/ecosystem/profiles/cursor/nono-capabilities.yaml b/ecosystem/profiles/cursor/nono-capabilities.yaml
new file mode 100644
index 0000000..12b252d
--- /dev/null
+++ b/ecosystem/profiles/cursor/nono-capabilities.yaml
@@ -0,0 +1,26 @@
+# nono capability set for Cursor — identical to claude-code except the
+# network egress list includes Cursor's own backends.
+version: "1"
+profile: cursor
+capabilities:
+ filesystem:
+ allow_read: [/workspace, /tmp, /usr/bin, /usr/lib, /lib, /etc/ssl/certs]
+ allow_readwrite: [/workspace]
+ deny: [/etc, /var, /root, /home, /proc/*/mem]
+ network:
+ allow_egress:
+ - api.github.com
+ - "*.githubusercontent.com"
+ - "*.cursor.com"
+ - "*.cursor.sh"
+ - api.pypi.org
+ - registry.npmjs.org
+ deny:
+ - 169.254.169.254
+ - metadata.google.internal
+ - fd00:ec2::254
+ block_all_other: true
+ syscalls:
+ default: allowlist
+ allowed: [read, write, openat, close, fstat, mmap, brk, execve, clone, wait4, pipe2, fork, dup2, accept4, connect, bind, listen, socket, rt_sigaction, rt_sigreturn, exit_group]
+ denied: [ptrace, process_vm_readv, keyctl, personality, pivot_root, chroot, mount]
diff --git a/ecosystem/profiles/cursor/policy.cedar b/ecosystem/profiles/cursor/policy.cedar
new file mode 100644
index 0000000..9d3e390
--- /dev/null
+++ b/ecosystem/profiles/cursor/policy.cedar
@@ -0,0 +1,105 @@
+// Cedar policy for Cursor running under sb-runtime + nono.
+// Baseline mirrors claude-code; adds Cursor-specific network allowlist.
+
+// Reads in workspace + /tmp
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (resource.path like "/workspace/*" || resource.path like "/tmp/*")
+};
+
+// Writes in workspace only
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" && resource.path like "/workspace/*"
+};
+
+// Safe shell allowlist
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in [
+ "ls", "cat", "grep", "rg", "find", "head", "tail", "wc",
+ "git", "pytest", "npm", "npx", "yarn", "pnpm",
+ "python", "python3", "node", "deno", "bun",
+ "cargo", "rustc", "go", "gofmt"
+ ]
+};
+
+// Cursor-specific network allowlist (extends the developer-domain set)
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ context.tls == true &&
+ (
+ resource.host like "*.github.com" ||
+ resource.host like "*.githubusercontent.com" ||
+ resource.host like "*.cursor.com" ||
+ resource.host like "*.cursor.sh" ||
+ resource.host like "api.pypi.org" ||
+ resource.host like "registry.npmjs.org"
+ )
+};
+
+// Universal denies (identical to claude-code profile)
+forbid (
+ principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ resource.host == "169.254.169.254" ||
+ resource.host == "fd00:ec2::254" ||
+ resource.host == "metadata.google.internal" ||
+ resource.host like "metadata.*.internal"
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" &&
+ (
+ resource.path like "/etc/*" ||
+ resource.path like "/usr/*" ||
+ resource.path like "/root/*" ||
+ resource.path like "/var/log/*" ||
+ resource.path like "/sys/*" ||
+ resource.path like "/proc/*"
+ )
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (
+ resource.path like "*.pem" ||
+ resource.path like "*id_rsa*" ||
+ resource.path like "*id_ed25519*" ||
+ resource.path like "*/.aws/credentials" ||
+ resource.path like "*/.ssh/*" ||
+ resource.path like "*/.netrc" ||
+ resource.path like "*/secrets.*"
+ )
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "pkill"]
+};
diff --git a/ecosystem/profiles/cursor/profile.yaml b/ecosystem/profiles/cursor/profile.yaml
new file mode 100644
index 0000000..4994a4e
--- /dev/null
+++ b/ecosystem/profiles/cursor/profile.yaml
@@ -0,0 +1,26 @@
+apiVersion: scopeblind.profiles/v1
+kind: SandboxProfile
+metadata:
+ name: cursor
+ tool: Cursor
+ tool_url: https://www.cursor.com
+ profile_version: 1.0.0
+ maintained_by: ScopeBlind
+ license: Apache-2.0
+ veritas_acta_receipt_type: scopeblind:decision
+spec:
+ sb_runtime:
+ recommended_ring: 3
+ sandbox_backend: sb_runtime_builtin
+ receipts_dir: .veritasacta/receipts/cursor
+ policy_file: ./policy.cedar
+ nono:
+ capability_file: ./nono-capabilities.yaml
+ recommended_mode: compose
+ intended_threat_model:
+ - Cursor's MCP bridge, Composer mode, and agent tabs all produce tool calls
+ - Cursor talks to cursor.com / cursor.sh backends for LLM proxying; allow those
+ - Otherwise baseline identical to claude-code profile
+ notes:
+ - Cursor runs inside Electron so FS access patterns look different from CLI agents
+ - If you use Cursor's MCP integration with third-party servers, extend the network allowlist per-MCP-server
diff --git a/ecosystem/profiles/gemini-cli/README.md b/ecosystem/profiles/gemini-cli/README.md
new file mode 100644
index 0000000..f04142e
--- /dev/null
+++ b/ecosystem/profiles/gemini-cli/README.md
@@ -0,0 +1,25 @@
+# Profile: Google Gemini CLI
+
+Run [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) under sb-runtime + nono + signed receipts.
+
+Baseline is similar to `claude-code` with Google-specific network endpoints (`googleapis.com`, `generativelanguage.googleapis.com`, `google.com`) instead of Anthropic's.
+
+## Quick start
+
+```bash
+npx @veritasacta/verify init --profile gemini-cli
+
+# Or standalone:
+sb-runtime --ring 3 --profile ./profile.yaml -- gemini
+```
+
+## Network allowlist
+
+- Google AI Studio: `generativelanguage.googleapis.com`, `*.googleapis.com`, `*.google.com`
+- Standard developer domains: `api.github.com`, `githubusercontent.com`, `pypi.org`, `npmjs.org`
+
+## Exec allowlist
+
+Same dev tools as claude-code: `git`, `pytest`, `npm`, `node`, `python`, `go`, plus the safe inspection commands.
+
+See [`claude-code/README.md`](../claude-code/README.md) for the full threat model and composition pattern.
diff --git a/ecosystem/profiles/gemini-cli/nono-capabilities.yaml b/ecosystem/profiles/gemini-cli/nono-capabilities.yaml
new file mode 100644
index 0000000..b89845d
--- /dev/null
+++ b/ecosystem/profiles/gemini-cli/nono-capabilities.yaml
@@ -0,0 +1,22 @@
+version: "1"
+profile: gemini-cli
+capabilities:
+ filesystem:
+ allow_read: [/workspace, /tmp, /usr/bin, /usr/lib, /lib, /etc/ssl/certs]
+ allow_readwrite: [/workspace]
+ deny: [/etc, /var, /root, /home, /proc/*/mem]
+ network:
+ allow_egress:
+ - "*.googleapis.com"
+ - generativelanguage.googleapis.com
+ - "*.google.com"
+ - api.github.com
+ - "*.githubusercontent.com"
+ - api.pypi.org
+ - registry.npmjs.org
+ deny: [169.254.169.254, metadata.google.internal, fd00:ec2::254]
+ block_all_other: true
+ syscalls:
+ default: allowlist
+ allowed: [read, write, openat, close, fstat, mmap, brk, execve, clone, wait4, pipe2, fork, dup2, accept4, connect, bind, listen, socket, rt_sigaction, rt_sigreturn, exit_group]
+ denied: [ptrace, process_vm_readv, keyctl, personality, pivot_root, chroot, mount]
diff --git a/ecosystem/profiles/gemini-cli/policy.cedar b/ecosystem/profiles/gemini-cli/policy.cedar
new file mode 100644
index 0000000..1630fc0
--- /dev/null
+++ b/ecosystem/profiles/gemini-cli/policy.cedar
@@ -0,0 +1,73 @@
+// Cedar policy for Google Gemini CLI. Read-review default with scoped shell.
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (resource.path like "/workspace/*" || resource.path like "/tmp/*")
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" && resource.path like "/workspace/*"
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in [
+ "ls", "cat", "grep", "rg", "find", "head", "tail", "wc",
+ "git", "pytest", "npm", "node", "python", "python3", "go"
+ ]
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ context.tls == true &&
+ (
+ resource.host like "*.googleapis.com" ||
+ resource.host like "generativelanguage.googleapis.com" ||
+ resource.host like "*.google.com" ||
+ resource.host like "api.github.com" ||
+ resource.host like "*.githubusercontent.com" ||
+ resource.host like "api.pypi.org" ||
+ resource.host like "registry.npmjs.org"
+ )
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ resource.host == "169.254.169.254" ||
+ resource.host == "metadata.google.internal" ||
+ resource.host == "fd00:ec2::254"
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ (context.mode == "write" && (resource.path like "/etc/*" || resource.path like "/usr/*" || resource.path like "/root/*" || resource.path like "/var/log/*")) ||
+ (context.mode == "read" && (resource.path like "*.pem" || resource.path like "*id_rsa*" || resource.path like "*id_ed25519*" || resource.path like "*/.aws/credentials" || resource.path like "*/.ssh/*" || resource.path like "*/.netrc"))
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "pkill"]
+};
diff --git a/ecosystem/profiles/gemini-cli/profile.yaml b/ecosystem/profiles/gemini-cli/profile.yaml
new file mode 100644
index 0000000..2c26585
--- /dev/null
+++ b/ecosystem/profiles/gemini-cli/profile.yaml
@@ -0,0 +1,21 @@
+apiVersion: scopeblind.profiles/v1
+kind: SandboxProfile
+metadata:
+ name: gemini-cli
+ tool: Google Gemini CLI
+ tool_url: https://github.com/google-gemini/gemini-cli
+ profile_version: 1.0.0
+ maintained_by: ScopeBlind
+ license: Apache-2.0
+spec:
+ sb_runtime:
+ recommended_ring: 3
+ sandbox_backend: sb_runtime_builtin
+ receipts_dir: .veritasacta/receipts/gemini-cli
+ policy_file: ./policy.cedar
+ nono:
+ capability_file: ./nono-capabilities.yaml
+ intended_threat_model:
+ - Gemini CLI tool calls go through Google AI Studio backends
+ - Shell access narrower than claude-code (read-review default)
+ - Network limited to Google AI + common dev
diff --git a/ecosystem/profiles/openclaw/README.md b/ecosystem/profiles/openclaw/README.md
new file mode 100644
index 0000000..4233869
--- /dev/null
+++ b/ecosystem/profiles/openclaw/README.md
@@ -0,0 +1,31 @@
+# Profile: OpenClaw
+
+Run [OpenClaw](https://github.com/openclaw-ai/openclaw) under sb-runtime + nono + signed receipts, **composing with OpenClaw's own guard-rail framework** rather than replacing it.
+
+## Composition model
+
+OpenClaw has in-process behavioural guards (prompt injection defense, PII detection, tool-use heuristics). This profile is designed to complement those:
+
+- **OpenClaw guards** handle behavioural and content-level checks
+- **sb-runtime + Cedar policy** handles structural checks (which file path, which command, which host)
+- **nono** handles kernel-level enforcement (the final boundary)
+
+Defense in depth: all three run. An exploit that bypasses OpenClaw's prompt-injection defense still has to clear the Cedar policy and the kernel sandbox.
+
+## Network allowlist
+
+Broader than claude-code because OpenClaw is model-agnostic: includes Anthropic, OpenAI, Google endpoints plus standard dev domains. Tighten per deployment.
+
+## Quick start
+
+```bash
+npx @veritasacta/verify init --profile openclaw
+
+# Or with explicit composition:
+nono run --caps ./nono-capabilities.yaml -- \
+ sb-runtime --ring 2 --policy ./policy.cedar -- openclaw
+```
+
+## Contributing updates
+
+OpenClaw's API is evolving. If a profile update is needed as OpenClaw's tool-use conventions change, open a PR against this directory.
diff --git a/ecosystem/profiles/openclaw/nono-capabilities.yaml b/ecosystem/profiles/openclaw/nono-capabilities.yaml
new file mode 100644
index 0000000..cccb188
--- /dev/null
+++ b/ecosystem/profiles/openclaw/nono-capabilities.yaml
@@ -0,0 +1,22 @@
+version: "1"
+profile: openclaw
+capabilities:
+ filesystem:
+ allow_read: [/workspace, /tmp, /usr/bin, /usr/lib, /lib, /etc/ssl/certs]
+ allow_readwrite: [/workspace]
+ deny: [/etc, /var, /root, /home, /proc/*/mem]
+ network:
+ allow_egress:
+ - "*.anthropic.com"
+ - "*.openai.com"
+ - "*.googleapis.com"
+ - api.github.com
+ - "*.githubusercontent.com"
+ - api.pypi.org
+ - registry.npmjs.org
+ deny: [169.254.169.254, metadata.google.internal, fd00:ec2::254]
+ block_all_other: true
+ syscalls:
+ default: allowlist
+ allowed: [read, write, openat, close, fstat, mmap, brk, execve, clone, wait4, pipe2, fork, dup2, accept4, connect, bind, listen, socket, rt_sigaction, rt_sigreturn, exit_group]
+ denied: [ptrace, process_vm_readv, keyctl, personality, pivot_root, chroot, mount]
diff --git a/ecosystem/profiles/openclaw/policy.cedar b/ecosystem/profiles/openclaw/policy.cedar
new file mode 100644
index 0000000..9c19666
--- /dev/null
+++ b/ecosystem/profiles/openclaw/policy.cedar
@@ -0,0 +1,74 @@
+// Cedar policy for OpenClaw — composes with OpenClaw's in-process guards.
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "read" &&
+ (resource.path like "/workspace/*" || resource.path like "/tmp/*")
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ context.mode == "write" && resource.path like "/workspace/*"
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in [
+ "ls", "cat", "grep", "rg", "find", "head", "tail", "wc",
+ "git", "pytest", "npm", "npx", "yarn",
+ "python", "python3", "node", "go", "cargo"
+ ]
+};
+
+permit (
+ principal is Agent::Principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ context.tls == true &&
+ (
+ resource.host like "*.anthropic.com" ||
+ resource.host like "*.openai.com" ||
+ resource.host like "*.googleapis.com" ||
+ resource.host like "api.github.com" ||
+ resource.host like "*.githubusercontent.com" ||
+ resource.host like "api.pypi.org" ||
+ resource.host like "registry.npmjs.org"
+ )
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"connect",
+ resource is Agent::Endpoint
+) when {
+ resource.host == "169.254.169.254" ||
+ resource.host == "metadata.google.internal" ||
+ resource.host == "fd00:ec2::254"
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"open",
+ resource is Agent::File
+) when {
+ (context.mode == "write" && (resource.path like "/etc/*" || resource.path like "/usr/*" || resource.path like "/root/*" || resource.path like "/var/log/*")) ||
+ (context.mode == "read" && (resource.path like "*.pem" || resource.path like "*id_rsa*" || resource.path like "*id_ed25519*" || resource.path like "*/.aws/credentials" || resource.path like "*/.ssh/*" || resource.path like "*/.netrc"))
+};
+
+forbid (
+ principal,
+ action == Agent::Action::"exec",
+ resource
+) when {
+ context.command in ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "pkill"]
+};
diff --git a/ecosystem/profiles/openclaw/profile.yaml b/ecosystem/profiles/openclaw/profile.yaml
new file mode 100644
index 0000000..e417b2f
--- /dev/null
+++ b/ecosystem/profiles/openclaw/profile.yaml
@@ -0,0 +1,24 @@
+apiVersion: scopeblind.profiles/v1
+kind: SandboxProfile
+metadata:
+ name: openclaw
+ tool: OpenClaw
+ tool_url: https://github.com/openclaw-ai/openclaw
+ profile_version: 1.0.0
+ maintained_by: ScopeBlind
+ license: Apache-2.0
+spec:
+ sb_runtime:
+ recommended_ring: 3
+ sandbox_backend: sb_runtime_builtin
+ receipts_dir: .veritasacta/receipts/openclaw
+ policy_file: ./policy.cedar
+ nono:
+ capability_file: ./nono-capabilities.yaml
+ intended_threat_model:
+ - OpenClaw has its own guard-rail framework; this profile is intended to COMPOSE with it
+ - Sandbox enforces filesystem + network + exec boundaries OpenClaw's guards cannot
+ - OpenClaw's behavioral checks run in-process; sandbox catches what OpenClaw misses
+ notes:
+ - Network allowlist kept broad to match OpenClaw's typical use; tighten per deployment
+ - Expect iteration as OpenClaw's API surface evolves; maintainer PRs welcome
diff --git a/ecosystem/registry-worker/README.md b/ecosystem/registry-worker/README.md
new file mode 100644
index 0000000..86f494d
--- /dev/null
+++ b/ecosystem/registry-worker/README.md
@@ -0,0 +1,55 @@
+# Veritas Acta Implementations Registry — Cloudflare Worker
+
+Public, read-only registry of Veritas Acta ecosystem implementations.
+Mirrors JSON files from `VeritasActa/agt-integration-profile/implementations/`
+with cached responses and CORS enabled.
+
+## Endpoints
+
+| Method | Path | Description |
+|---|---|---|
+| GET | `/implementations.json` | List all registered implementations |
+| GET | `/implementations/{name}.json` | Single implementation's record |
+| GET | `/implementations/{name}/attestation.json` | Signed conformance attestation |
+| GET | `/stats.json` | Aggregate stats (count, tier distribution) |
+| GET | `/health` | Health probe |
+
+## Implementation record format
+
+```json
+{
+ "name": "sb-runtime",
+ "repo": "https://github.com/ScopeBlind/sb-runtime",
+ "version": "0.1.0",
+ "claimed_tier": 4,
+ "language": "Rust",
+ "license": "Apache-2.0",
+ "maintainer": "@tomjwxf",
+ "conformance_evidence": "https://github.com/ScopeBlind/agent-governance-testvectors/actions/runs/12345678",
+ "signed_attestation_hash": "sha256:abc123...",
+ "registered_at": "2026-04-19T00:00:00.000Z"
+}
+```
+
+## Deployment
+
+```bash
+wrangler deploy
+```
+
+Bind a KV namespace named `REGISTRY_CACHE` for caching. Zero-config
+deployments work but miss the cache; responses will hit GitHub's API
+directly (rate-limited to 60 req/hour unauthenticated).
+
+## Register an implementation
+
+Open a PR against `VeritasActa/agt-integration-profile` adding:
+
+1. `implementations/{name}.json` — record metadata
+2. `implementations/{name}/attestation.json` — signed conformance attestation
+
+The registry serves updates automatically after PR merge.
+
+## License
+
+Apache-2.0
diff --git a/ecosystem/registry-worker/worker.js b/ecosystem/registry-worker/worker.js
new file mode 100644
index 0000000..1d73f0e
--- /dev/null
+++ b/ecosystem/registry-worker/worker.js
@@ -0,0 +1,137 @@
+/**
+ * registry.veritasacta.com — Cloudflare Worker
+ *
+ * Serves a public implementations registry for Veritas Acta ecosystem.
+ * Each implementation signs a conformance claim with their own key and
+ * registers via GitHub PR against the implementations/*.json file in the
+ * agt-integration-profile repo. This worker is read-only: it mirrors the
+ * canonical JSON files from the repo and serves them with appropriate
+ * CORS and caching.
+ *
+ * Endpoints:
+ * GET /implementations.json
+ * Returns the full registry as a JSON array.
+ *
+ * GET /implementations/{name}.json
+ * Returns a single implementation's record.
+ *
+ * GET /implementations/{name}/attestation.json
+ * Returns the signed conformance attestation for the implementation.
+ *
+ * GET /stats.json
+ * Returns aggregate stats (count, tier distribution).
+ *
+ * GET /health
+ * Returns 200 OK.
+ *
+ * Authentication: none required for reads. Writes happen via GitHub PR.
+ */
+
+const REGISTRY_REPO = 'VeritasActa/agt-integration-profile';
+const REGISTRY_PATH = 'implementations';
+const CACHE_TTL = 300; // 5 minutes
+
+export default {
+ async fetch(request, env, ctx) {
+ const url = new URL(request.url);
+ const { pathname } = url;
+
+ // CORS preflight
+ if (request.method === 'OPTIONS') {
+ return new Response(null, { headers: corsHeaders() });
+ }
+
+ if (pathname === '/health') {
+ return json({ status: 'ok', timestamp: new Date().toISOString() });
+ }
+
+ if (pathname === '/implementations.json') {
+ const list = await fetchImplementationsList(env);
+ return json(list);
+ }
+
+ if (pathname === '/stats.json') {
+ const list = await fetchImplementationsList(env);
+ return json({
+ count: list.length,
+ byTier: list.reduce((acc, impl) => {
+ const tier = impl.claimed_tier || 1;
+ acc[tier] = (acc[tier] || 0) + 1;
+ return acc;
+ }, {}),
+ lastUpdated: new Date().toISOString(),
+ });
+ }
+
+ const implMatch = pathname.match(/^\/implementations\/([a-z0-9-]+)(?:\/attestation)?\.json$/);
+ if (implMatch) {
+ const name = implMatch[1];
+ const isAttestation = pathname.endsWith('/attestation.json');
+ const impl = await fetchImplementation(env, name, isAttestation);
+ if (!impl) return json({ error: 'not found', name }, 404);
+ return json(impl);
+ }
+
+ return json({ error: 'not found', path: pathname }, 404);
+ },
+};
+
+async function fetchImplementationsList(env) {
+ const cached = await env.REGISTRY_CACHE?.get('implementations-list');
+ if (cached) return JSON.parse(cached);
+
+ const url = `https://api.github.com/repos/${REGISTRY_REPO}/contents/${REGISTRY_PATH}`;
+ const res = await fetch(url, {
+ headers: {
+ 'User-Agent': 'registry.veritasacta.com',
+ Accept: 'application/vnd.github.v3+json',
+ },
+ });
+ if (!res.ok) return [];
+
+ const contents = await res.json();
+ const impls = [];
+ for (const file of contents) {
+ if (!file.name.endsWith('.json') || file.name === 'attestation.json') continue;
+ const data = await fetchImplementationJson(file.download_url);
+ if (data) impls.push(data);
+ }
+
+ if (env.REGISTRY_CACHE) {
+ await env.REGISTRY_CACHE.put('implementations-list', JSON.stringify(impls), { expirationTtl: CACHE_TTL });
+ }
+ return impls;
+}
+
+async function fetchImplementation(env, name, isAttestation) {
+ const filename = isAttestation ? `${name}/attestation.json` : `${name}.json`;
+ const url = `https://raw.githubusercontent.com/${REGISTRY_REPO}/main/${REGISTRY_PATH}/${filename}`;
+ const res = await fetch(url, { headers: { 'User-Agent': 'registry.veritasacta.com' } });
+ if (!res.ok) return null;
+ return res.json();
+}
+
+async function fetchImplementationJson(downloadUrl) {
+ try {
+ const res = await fetch(downloadUrl);
+ if (!res.ok) return null;
+ return res.json();
+ } catch {
+ return null;
+ }
+}
+
+function json(body, status = 200) {
+ return new Response(JSON.stringify(body, null, 2), {
+ status,
+ headers: { 'content-type': 'application/json', ...corsHeaders() },
+ });
+}
+
+function corsHeaders() {
+ return {
+ 'access-control-allow-origin': '*',
+ 'access-control-allow-methods': 'GET, OPTIONS',
+ 'cache-control': `public, max-age=${CACHE_TTL}`,
+ };
+}
diff --git a/ecosystem/registry-worker/wrangler.toml b/ecosystem/registry-worker/wrangler.toml
new file mode 100644
index 0000000..af4afde
--- /dev/null
+++ b/ecosystem/registry-worker/wrangler.toml
@@ -0,0 +1,11 @@
+#:schema node_modules/wrangler/config-schema.json
+name = "registry-veritasacta"
+main = "worker.js"
+compatibility_date = "2024-12-01"
+
+routes = [
+ { pattern = "registry.veritasacta.com/*", zone_name = "veritasacta.com" }
+]
+
+[observability]
+enabled = true
diff --git a/ecosystem/reputation/DESIGN.md b/ecosystem/reputation/DESIGN.md
new file mode 100644
index 0000000..345b395
--- /dev/null
+++ b/ecosystem/reputation/DESIGN.md
@@ -0,0 +1,52 @@
+# Issuer Reputation Layer (v0.7.0 target)
+
+Bayesian reputation over RECEIPT ISSUERS (not agents — aeoess already
+has agent reputation; this complements it with the inverse).
+
+## Goal
+
+Track which issuer keys reliably produce valid, well-formed,
+conformance-tier-appropriate receipts. Give verifiers an additional
+signal — not a replacement for cryptographic verification, but a
+reliability metric that composes with it.
+
+## Axes
+
+Per issuer key (kid), track:
+
+- **Conformance rate** — fraction of receipts that pass full
+ verification
+- **Chain integrity rate** — fraction of chains that verify without
+ breakage
+- **Tier distribution** — which conformance tiers the issuer reaches
+- **Receipt frequency** — issuance rate over time
+- **Jurisdictional distribution** — where receipts are verified
+
+## Data source
+
+Optional: the verifier daemon can contribute anonymized observations
+to a public reputation log (opt-in). Or: organizations run private
+reputation logs for their own issuer pools.
+
+No phone-home by default. Reputation is computed locally or on explicit
+public submission.
+
+## Composition with aeoess APS
+
+- aeoess reputation = on agents / principals ("can we trust agent X to
+ act?")
+- Veritas Acta reputation = on issuers / signers ("does issuer Y
+ reliably produce valid receipts?")
+
+Together they cover both sides of the trust surface.
+
+## Non-goals
+
+- Not a proof of trustworthiness (only evidence of historical
+ reliability)
+- Not a replacement for signature verification
+- Not a ranking / leaderboard (raw metrics, not ordinal)
+
+## License
+
+Apache-2.0 once shipped.
diff --git a/ecosystem/rollback/DESIGN.md b/ecosystem/rollback/DESIGN.md
new file mode 100644
index 0000000..9638d66
--- /dev/null
+++ b/ecosystem/rollback/DESIGN.md
@@ -0,0 +1,62 @@
+# Filesystem Rollback — AIP-0004 (v0.6.0 target)
+
+> **Spec:** [AIP-0004 Content-Addressed Snapshot and Rollback Receipts](../../../../specs/aip/AIP-0004-snapshot-receipts.md)
+> **Reference implementation:** [`snapshot.mjs`](./snapshot.mjs) (Merkle helper + payload builder)
+> **Schema:** [`snapshot-receipt.schema.json`](./snapshot-receipt.schema.json)
+> **Tests:** 11 units in `test/unit/snapshot.test.js` (determinism, path-order independence, tamper detection, odd-layer duplication, payload validation)
+
+
+
+Content-addressed filesystem snapshots before each receipted session,
+with one-command rollback on anomaly detection. Matches nono's undo
+pattern but pairs it with Veritas Acta receipts so rollback events are
+themselves signed.
+
+## Goal
+
+When an agent session goes wrong, roll back to the pre-session state
+without losing the receipt trail. The receipts survive; the filesystem
+reverts.
+
+## Design
+
+### Snapshot primitive
+
+- Before every session, hash the files the session will touch (derived
+ from the agent's allowlist / policy)
+- Store content-addressed copies in `.veritasacta/snapshots/{session_id}/`
+- Content-addressed dedup: files common across sessions are stored once
+- Snapshot index committed to receipts via `snapshot_digest` field
+
+### Rollback command
+
+```bash
+veritasacta rollback --session
+```
+
+Restores files to their pre-session state. Emits a signed `rollback`
+receipt linked to the original session.
+
+### Composition with sb-runtime
+
+sb-runtime already has filesystem isolation via Landlock. The rollback
+layer adds temporal reversion: not "the agent can't write here" but
+"if the agent wrote here, we can undo it."
+
+## Non-goals
+
+- Not a general-purpose file versioning system (use git)
+- Not a replacement for kernel sandboxing (compose with sb-runtime)
+- Not guaranteed to roll back external side-effects (API calls, db
+ mutations). Those require receipts to be inspected and externally
+ compensated.
+
+## Implementation path
+
+- v0.6.0: manual snapshot/rollback commands + session_id linkage
+- v0.7.0: automatic anomaly-triggered rollback (ties to supervisor
+ approval workflow)
+
+## License
+
+Apache-2.0 once shipped.
diff --git a/ecosystem/rollback/snapshot-receipt.schema.json b/ecosystem/rollback/snapshot-receipt.schema.json
new file mode 100644
index 0000000..725f454
--- /dev/null
+++ b/ecosystem/rollback/snapshot-receipt.schema.json
@@ -0,0 +1,131 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://veritasacta.com/schemas/aip-0004/snapshot-receipt.schema.json",
+ "title": "AIP-0004 Snapshot / Rollback Receipt",
+ "description": "Content-addressed snapshot and rollback receipts over AIP-0001 envelope.",
+ "oneOf": [
+ { "$ref": "#/$defs/snapshot" },
+ { "$ref": "#/$defs/rollback" }
+ ],
+ "$defs": {
+ "envelope": {
+ "type": "object",
+ "required": ["payload", "signature"],
+ "properties": {
+ "payload": { "type": "object" },
+ "signature": {
+ "type": "object",
+ "required": ["alg", "kid", "sig"],
+ "properties": {
+ "alg": { "type": "string", "enum": ["ed25519", "EdDSA", "ed25519+ml-dsa-65"] },
+ "kid": { "type": "string", "minLength": 1 },
+ "sig": { "type": "string", "pattern": "^[0-9a-f]+$" }
+ }
+ }
+ }
+ },
+ "snapshot": {
+ "allOf": [{ "$ref": "#/$defs/envelope" }],
+ "properties": {
+ "payload": {
+ "type": "object",
+ "required": [
+ "type", "session_id", "snapshot_root", "snapshot_backend",
+ "file_count", "snapshot_scope", "issuer_id", "agent_id", "issued_at"
+ ],
+ "properties": {
+ "type": { "const": "snapshot" },
+ "session_id": { "type": "string", "minLength": 1 },
+ "snapshot_root": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$",
+ "description": "base64url(SHA-256) no padding"
+ },
+ "snapshot_backend": {
+ "type": "string",
+ "enum": ["zfs", "btrfs", "git", "cow", "copy", "nono", "custom"]
+ },
+ "snapshot_uri": { "type": "string" },
+ "file_count": { "type": "integer", "minimum": 0 },
+ "snapshot_scope": {
+ "type": "object",
+ "required": ["allowed_read", "allowed_write"],
+ "properties": {
+ "allowed_read": { "type": "array", "items": { "type": "string" } },
+ "allowed_write": { "type": "array", "items": { "type": "string" } },
+ "deny": { "type": "array", "items": { "type": "string" } }
+ }
+ },
+ "policy_hash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "previousReceiptHash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "issuer_id": { "type": "string" },
+ "agent_id": { "type": "string" },
+ "issued_at": { "type": "string", "format": "date-time" }
+ }
+ }
+ }
+ },
+ "rollback": {
+ "allOf": [{ "$ref": "#/$defs/envelope" }],
+ "properties": {
+ "payload": {
+ "type": "object",
+ "required": [
+ "type", "session_id", "snapshot_receipt_hash",
+ "rollback_reason", "rollback_initiator", "rollback_outcome",
+ "post_rollback_root", "issuer_id", "agent_id", "issued_at"
+ ],
+ "properties": {
+ "type": { "const": "rollback" },
+ "session_id": { "type": "string", "minLength": 1 },
+ "snapshot_receipt_hash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "rollback_reason": { "type": "string", "maxLength": 256 },
+ "rollback_initiator": {
+ "type": "string",
+ "enum": ["human", "policy", "anomaly-detector"]
+ },
+ "rollback_outcome": {
+ "type": "string",
+ "enum": ["success", "partial", "failed"]
+ },
+ "post_rollback_root": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "external_side_effects": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["receipt_hash", "action", "compensable"],
+ "properties": {
+ "receipt_hash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "action": { "type": "string" },
+ "compensable": { "type": "boolean" }
+ }
+ }
+ },
+ "previousReceiptHash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "issuer_id": { "type": "string" },
+ "agent_id": { "type": "string" },
+ "issued_at": { "type": "string", "format": "date-time" }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ecosystem/rollback/snapshot.mjs b/ecosystem/rollback/snapshot.mjs
new file mode 100755
index 0000000..814ecc6
--- /dev/null
+++ b/ecosystem/rollback/snapshot.mjs
@@ -0,0 +1,236 @@
+#!/usr/bin/env node
+/**
+ * AIP-0004 reference implementation — content-addressed snapshot +
+ * rollback receipt Merkle helper.
+ *
+ * Produces `snapshot_root` given a list of (relative_path, file_bytes)
+ * entries. Also includes a lightweight example signer that composes
+ * with the AIP-0001 envelope so operators can see end-to-end how the
+ * pieces compose.
+ *
+ * This is a reference implementation of the spec at
+ * specs/aip/AIP-0004-snapshot-receipts.md. The production path is for
+ * sb-runtime (or any rollback engine) to adopt this Merkle construction
+ * and the receipt schema; the verify-cli will validate those receipts
+ * once the v0.6.0 --verify-snapshot path lands.
+ *
+ * Usage as a library:
+ * import { buildSnapshotRoot, makeSnapshotPayload } from './snapshot.mjs';
+ * const root = buildSnapshotRoot([['src/app.js', fileBytes], ...]);
+ *
+ * Usage as a CLI (experimental):
+ * node snapshot.mjs --root ./project --out snapshot.json --session
+ *
+ * License: Apache-2.0.
+ */
+
+import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
+import { join, relative, resolve } from 'node:path';
+import { createHash } from 'node:crypto';
+
+// ───── Core Merkle construction (AIP-0004 §1.2) ─────
+
+/**
+ * Compute the AIP-0004 snapshot Merkle root.
+ *
+ * @param {Array<[string, Buffer|Uint8Array]>} entries
+ * [relativePath, fileBytes] pairs. Paths MUST be POSIX and
+ * relative to the snapshot root.
+ * @returns {string} base64url-encoded SHA-256 Merkle root (no padding).
+ */
+export function buildSnapshotRoot(entries) {
+ if (!Array.isArray(entries) || entries.length === 0) {
+ return sha256b64url(Buffer.alloc(0));
+ }
+
+ // Sort lexicographically by path.
+ const sorted = [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
+
+ // Leaves: SHA-256(path_utf8 || 0x00 || SHA-256(file_bytes))
+ let layer = sorted.map(([path, bytes]) => {
+ const pathBuf = Buffer.from(path, 'utf-8');
+ const fileHash = sha256(bytes);
+ return sha256(Buffer.concat([pathBuf, Buffer.from([0x00]), fileHash]));
+ });
+
+ // Pairwise hash; duplicate last if odd.
+ while (layer.length > 1) {
+ const next = [];
+ for (let i = 0; i < layer.length; i += 2) {
+ const left = layer[i];
+ const right = i + 1 < layer.length ? layer[i + 1] : layer[i];
+ next.push(sha256(Buffer.concat([left, right])));
+ }
+ layer = next;
+ }
+
+ return base64url(layer[0]);
+}
+
+function sha256(buf) {
+ return createHash('sha256').update(buf).digest();
+}
+
+function sha256b64url(buf) {
+ return base64url(sha256(buf));
+}
+
+function base64url(buf) {
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+}
+
+// ───── Snapshot payload builder (AIP-0004 §1) ─────
+
+/**
+ * Build an unsigned AIP-0004 snapshot payload.
+ *
+ * @param {Object} opts
+ * @param {string} opts.session_id
+ * @param {string} opts.snapshot_root
+ * @param {'zfs'|'btrfs'|'git'|'cow'|'copy'|'nono'|'custom'} opts.backend
+ * @param {number} opts.file_count
+ * @param {Object} opts.scope { allowed_read[], allowed_write[], deny[]? }
+ * @param {string} opts.issuer_id
+ * @param {string} opts.agent_id
+ * @param {string} [opts.snapshot_uri]
+ * @param {string} [opts.policy_hash]
+ * @param {string} [opts.previousReceiptHash]
+ * @returns {Object}
+ */
+export function makeSnapshotPayload(opts) {
+ const payload = {
+ type: 'snapshot',
+ session_id: opts.session_id,
+ snapshot_root: opts.snapshot_root,
+ snapshot_backend: opts.backend,
+ file_count: opts.file_count,
+ snapshot_scope: {
+ allowed_read: opts.scope.allowed_read || [],
+ allowed_write: opts.scope.allowed_write || [],
+ ...(opts.scope.deny ? { deny: opts.scope.deny } : {}),
+ },
+ issuer_id: opts.issuer_id,
+ agent_id: opts.agent_id,
+ issued_at: new Date().toISOString(),
+ };
+ if (opts.snapshot_uri) payload.snapshot_uri = opts.snapshot_uri;
+ if (opts.policy_hash) payload.policy_hash = opts.policy_hash;
+ if (opts.previousReceiptHash) payload.previousReceiptHash = opts.previousReceiptHash;
+ return payload;
+}
+
+/**
+ * Build an unsigned AIP-0004 rollback payload.
+ *
+ * @param {Object} opts
+ * @param {string} opts.session_id
+ * @param {string} opts.snapshot_receipt_hash
+ * @param {string} opts.rollback_reason MAX 256 bytes
+ * @param {'human'|'policy'|'anomaly-detector'} opts.rollback_initiator
+ * @param {'success'|'partial'|'failed'} opts.rollback_outcome
+ * @param {string} opts.post_rollback_root
+ * @param {string} opts.issuer_id
+ * @param {string} opts.agent_id
+ * @param {Array<{receipt_hash:string, action:string, compensable:boolean}>} [opts.external_side_effects]
+ * @param {string} [opts.previousReceiptHash]
+ * @returns {Object}
+ */
+export function makeRollbackPayload(opts) {
+ if (opts.rollback_reason && Buffer.byteLength(opts.rollback_reason, 'utf-8') > 256) {
+ throw new Error('rollback_reason exceeds 256 bytes');
+ }
+ const payload = {
+ type: 'rollback',
+ session_id: opts.session_id,
+ snapshot_receipt_hash: opts.snapshot_receipt_hash,
+ rollback_reason: opts.rollback_reason,
+ rollback_initiator: opts.rollback_initiator,
+ rollback_outcome: opts.rollback_outcome,
+ post_rollback_root: opts.post_rollback_root,
+ issuer_id: opts.issuer_id,
+ agent_id: opts.agent_id,
+ issued_at: new Date().toISOString(),
+ };
+ if (opts.external_side_effects) payload.external_side_effects = opts.external_side_effects;
+ if (opts.previousReceiptHash) payload.previousReceiptHash = opts.previousReceiptHash;
+ return payload;
+}
+
+// ───── Directory walker (convenience — not part of the spec) ─────
+
+/**
+ * Walk a directory tree and return [relPath, Buffer] entries suitable
+ * for buildSnapshotRoot. Applies an optional include/exclude filter.
+ *
+ * @param {string} root
+ * @param {Object} [filter]
+ * @param {(relPath: string) => boolean} [filter.include]
+ * @returns {Array<[string, Buffer]>}
+ */
+export function walkDirectory(root, filter = {}) {
+ const absRoot = resolve(root);
+ const entries = [];
+
+ function recurse(dir) {
+ for (const entry of readdirSync(dir)) {
+ const full = join(dir, entry);
+ let st;
+ try { st = statSync(full); } catch { continue; }
+ if (st.isDirectory()) {
+ recurse(full);
+ } else if (st.isFile()) {
+ const rel = relative(absRoot, full).split('\\').join('/');
+ if (filter.include && !filter.include(rel)) continue;
+ try { entries.push([rel, readFileSync(full)]); } catch { continue; }
+ }
+ }
+ }
+
+ recurse(absRoot);
+ return entries;
+}
+
+// ───── CLI (example wiring) ─────
+
+const isMain = import.meta.url === `file://${process.argv[1]}`;
+if (isMain) {
+ const args = process.argv.slice(2);
+ const opts = {};
+ for (let i = 0; i < args.length; i++) {
+ const a = args[i];
+ if (a === '--root') opts.root = args[++i];
+ else if (a === '--out') opts.out = args[++i];
+ else if (a === '--session') opts.session = args[++i];
+ else if (a === '--backend') opts.backend = args[++i];
+ else if (a === '--issuer') opts.issuer = args[++i];
+ else if (a === '--agent') opts.agent = args[++i];
+ else if (a === '-h' || a === '--help') {
+ console.log('snapshot.mjs --root --session [--out ]');
+ console.log(' [--backend zfs|btrfs|git|cow|copy|nono|custom]');
+ console.log(' [--issuer ] [--agent ]');
+ console.log('Builds an unsigned AIP-0004 snapshot payload. Hand to your signer.');
+ process.exit(0);
+ }
+ }
+ if (!opts.root) {
+ console.error('--root required');
+ process.exit(2);
+ }
+
+ const entries = walkDirectory(opts.root, {
+ include: (p) => !p.startsWith('.git/') && !p.startsWith('node_modules/'),
+ });
+ const root = buildSnapshotRoot(entries);
+ const payload = makeSnapshotPayload({
+ session_id: opts.session || 'anon-session',
+ snapshot_root: root,
+ backend: opts.backend || 'copy',
+ file_count: entries.length,
+ scope: { allowed_read: [opts.root], allowed_write: [opts.root] },
+ issuer_id: opts.issuer || 'reference-issuer',
+ agent_id: opts.agent || 'reference-agent',
+ });
+ const out = JSON.stringify(payload, null, 2);
+ if (opts.out) writeFileSync(opts.out, out + '\n');
+ else process.stdout.write(out + '\n');
+}
diff --git a/ecosystem/sdk-js/package.json b/ecosystem/sdk-js/package.json
new file mode 100644
index 0000000..b478d85
--- /dev/null
+++ b/ecosystem/sdk-js/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@veritasacta/sdk",
+ "version": "0.1.0",
+ "description": "Tiny SDK for producing Veritas Acta signed decision receipts. Framework-agnostic; adapters build on top.",
+ "license": "Apache-2.0",
+ "type": "module",
+ "main": "src/index.js",
+ "types": "src/index.d.ts",
+ "files": ["src/"],
+ "dependencies": {},
+ "engines": { "node": ">=18.0.0" },
+ "keywords": ["veritasacta", "receipts", "ed25519", "jcs", "agent-governance"],
+ "repository": { "type": "git", "url": "git+https://github.com/VeritasActa/sdk-js.git" }
+}
diff --git a/ecosystem/sdk-js/src/index.d.ts b/ecosystem/sdk-js/src/index.d.ts
new file mode 100644
index 0000000..bfd7424
--- /dev/null
+++ b/ecosystem/sdk-js/src/index.d.ts
@@ -0,0 +1,26 @@
+export interface SignDecisionInput {
+ tool: string;
+ args?: Record;
+ decision?: 'allow' | 'deny' | 'require_approval' | 'compensated' | string;
+ policy_id?: string;
+ policy_hash?: string;
+ skill_version_hash?: string;
+ delegation_chain_root?: string;
+ metadata?: Record;
+}
+
+export interface Receipt {
+ payload: Record;
+ signature: { alg: string; kid: string; sig: string };
+}
+
+export class Signer {
+ readonly kid: string;
+ readonly pubHex: string;
+ readonly issuerId: string;
+ sequence: number;
+ previousReceiptHash: string | null;
+
+ static fromKeyFile(path: string): Signer;
+ signDecision(input: SignDecisionInput): Receipt;
+}
diff --git a/ecosystem/sdk-js/src/index.js b/ecosystem/sdk-js/src/index.js
new file mode 100644
index 0000000..6e3100d
--- /dev/null
+++ b/ecosystem/sdk-js/src/index.js
@@ -0,0 +1,111 @@
+/**
+ * @veritasacta/sdk — tiny signing SDK
+ *
+ * Usage (Node.js):
+ *
+ * import { Signer } from '@veritasacta/sdk';
+ * const signer = Signer.fromKeyFile('.veritasacta/attester.json');
+ * const receipt = signer.signDecision({
+ * tool: 'web_search',
+ * args: { query: '...' },
+ * decision: 'allow',
+ * policy_id: 'research-only',
+ * });
+ *
+ * The SDK does one thing: produce valid draft-farley-acta-signed-receipts
+ * envelopes. Framework adapters use this SDK; the SDK itself has no
+ * framework-specific code.
+ *
+ * @license Apache-2.0
+ */
+
+import { readFileSync } from 'node:fs';
+import { sign, createPrivateKey, createHash } from 'node:crypto';
+
+function canonicalize(obj) {
+ // Minimal JCS-like canonicalization (AIP-0001 restrictions).
+ const sortDeep = (o) => {
+ if (o === null || typeof o !== 'object') return o;
+ if (Array.isArray(o)) return o.map(sortDeep);
+ const out = {};
+ for (const k of Object.keys(o).sort()) out[k] = sortDeep(o[k]);
+ return out;
+ };
+ return JSON.stringify(sortDeep(obj));
+}
+
+export class Signer {
+ constructor({ kid, privateKey, pubHex, issuerId = null }) {
+ this.kid = kid;
+ this.privateKey = privateKey;
+ this.pubHex = pubHex;
+ this.issuerId = issuerId || kid;
+ this.sequence = 0;
+ this.previousReceiptHash = null;
+ }
+
+ /**
+ * Load a signer from a key file written by `veritasacta init`
+ * (matches the { kid, pubHex, privateDer } shape).
+ *
+ * @param {string} path
+ * @returns {Signer}
+ */
+ static fromKeyFile(path) {
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
+ const privateKey = createPrivateKey({
+ key: Buffer.from(data.privateDer, 'hex'),
+ format: 'der',
+ type: 'pkcs8',
+ });
+ return new Signer({ kid: data.kid, privateKey, pubHex: data.pubHex });
+ }
+
+ /**
+ * Sign a single tool-call decision and return the receipt envelope.
+ *
+ * @param {Object} args
+ * @param {string} args.tool tool name
+ * @param {Object} [args.args] tool arguments (hashed, not stored raw)
+ * @param {string} [args.decision='allow']
+ * @param {string} [args.policy_id]
+ * @param {string} [args.policy_hash]
+ * @param {string} [args.skill_version_hash]
+ * @param {string} [args.delegation_chain_root]
+ * @param {Object} [args.metadata]
+ * @returns {Object} receipt envelope { payload, signature }
+ */
+ signDecision(args) {
+ this.sequence += 1;
+ const argStr = JSON.stringify(args.args || {}, Object.keys(args.args || {}).sort());
+ const tool_input_hash = 'sha256:' + createHash('sha256').update(argStr, 'utf-8').digest('hex');
+
+ const payload = {
+ type: 'veritasacta:decision',
+ spec: 'draft-farley-acta-signed-receipts-03',
+ tool_name: args.tool,
+ tool_input_hash,
+ decision: args.decision || 'allow',
+ issued_at: new Date().toISOString(),
+ issuer_id: this.issuerId,
+ sequence: this.sequence,
+ previousReceiptHash: this.previousReceiptHash,
+ };
+ if (args.policy_id) payload.policy_id = args.policy_id;
+ if (args.policy_hash) payload.policy_hash = args.policy_hash;
+ if (args.skill_version_hash) payload.skill_version_hash = args.skill_version_hash;
+ if (args.delegation_chain_root) payload.delegation_chain_root = args.delegation_chain_root;
+ if (args.metadata) payload.metadata = args.metadata;
+
+ const canonical = canonicalize(payload);
+ const sig = sign(null, Buffer.from(canonical, 'utf-8'), this.privateKey);
+
+ // Chain linkage
+ this.previousReceiptHash = 'sha256:' + createHash('sha256').update(canonical, 'utf-8').digest('hex');
+
+ return {
+ payload,
+ signature: { alg: 'EdDSA', kid: this.kid, sig: sig.toString('hex') },
+ };
+ }
+}
diff --git a/ecosystem/sdk-py/pyproject.toml b/ecosystem/sdk-py/pyproject.toml
new file mode 100644
index 0000000..acf1773
--- /dev/null
+++ b/ecosystem/sdk-py/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "veritasacta-sdk"
+version = "0.1.0"
+description = "Tiny SDK for producing Veritas Acta signed decision receipts (Python). Framework-agnostic."
+readme = "README.md"
+license = "Apache-2.0"
+requires-python = ">=3.10"
+authors = [{ name = "Tom Farley", email = "tommy@scopeblind.com" }]
+keywords = ["veritasacta", "receipts", "ed25519", "jcs", "agent-governance"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3",
+ "Topic :: Security :: Cryptography",
+]
+dependencies = ["pynacl>=1.5.0"]
+
+[project.urls]
+Homepage = "https://github.com/VeritasActa/sdk-py"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/veritasacta_sdk"]
diff --git a/ecosystem/sdk-py/src/veritasacta_sdk/__init__.py b/ecosystem/sdk-py/src/veritasacta_sdk/__init__.py
new file mode 100644
index 0000000..488e173
--- /dev/null
+++ b/ecosystem/sdk-py/src/veritasacta_sdk/__init__.py
@@ -0,0 +1,6 @@
+"""Veritas Acta SDK — tiny Python signing helper."""
+
+from .signer import Signer, Receipt
+
+__version__ = "0.1.0"
+__all__ = ["Signer", "Receipt"]
diff --git a/ecosystem/sdk-py/src/veritasacta_sdk/signer.py b/ecosystem/sdk-py/src/veritasacta_sdk/signer.py
new file mode 100644
index 0000000..0c6600c
--- /dev/null
+++ b/ecosystem/sdk-py/src/veritasacta_sdk/signer.py
@@ -0,0 +1,114 @@
+"""veritasacta_sdk.signer — Ed25519 + JCS receipt signing."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Optional
+
+from nacl.signing import SigningKey
+
+
+def _canonicalize(obj: Any) -> str:
+ """JCS-like canonical serialization (sorted keys, no whitespace)."""
+ def sort_deep(o):
+ if isinstance(o, dict):
+ return {k: sort_deep(o[k]) for k in sorted(o)}
+ if isinstance(o, list):
+ return [sort_deep(v) for v in o]
+ return o
+ return json.dumps(sort_deep(obj), separators=(",", ":"), ensure_ascii=False)
+
+
+@dataclass
+class Receipt:
+ """An Ed25519-signed decision receipt."""
+ payload: dict[str, Any]
+ signature: dict[str, str]
+
+ def to_dict(self) -> dict[str, Any]:
+ return {"payload": self.payload, "signature": self.signature}
+
+ def to_json(self, indent: int = 2) -> str:
+ return json.dumps(self.to_dict(), indent=indent)
+
+
+class Signer:
+ """Sign draft-farley-acta-signed-receipts-03 decision receipts."""
+
+ def __init__(self, signing_key: SigningKey, kid: str, issuer_id: Optional[str] = None):
+ self._signing_key = signing_key
+ self.kid = kid
+ self.issuer_id = issuer_id or kid
+ self.pub_hex = signing_key.verify_key.encode().hex()
+ self.sequence = 0
+ self.previous_receipt_hash: Optional[str] = None
+
+ @classmethod
+ def from_key_file(cls, path: str) -> "Signer":
+ """Load a signer from a key file (produced by `veritasacta init`)."""
+ data = json.loads(Path(path).read_text())
+ priv_bytes = bytes.fromhex(data["privateDer"])
+ # PKCS#8 wrapper: last 32 bytes are the raw private key
+ raw = priv_bytes[-32:] if len(priv_bytes) >= 32 else priv_bytes
+ return cls(SigningKey(raw), kid=data["kid"])
+
+ @classmethod
+ def generate(cls, kid_prefix: str = "sdk") -> "Signer":
+ """Generate a new signing key (for tests / demos)."""
+ key = SigningKey.generate()
+ pub_hex = key.verify_key.encode().hex()
+ kid = f"{kid_prefix}:{pub_hex[:12]}"
+ return cls(key, kid)
+
+ def sign_decision(
+ self,
+ tool: str,
+ args: Optional[dict[str, Any]] = None,
+ decision: str = "allow",
+ policy_id: Optional[str] = None,
+ policy_hash: Optional[str] = None,
+ skill_version_hash: Optional[str] = None,
+ delegation_chain_root: Optional[str] = None,
+ metadata: Optional[dict[str, Any]] = None,
+ ) -> Receipt:
+ """Sign a single tool-call decision."""
+ self.sequence += 1
+ arg_str = json.dumps(args or {}, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+ tool_input_hash = "sha256:" + hashlib.sha256(arg_str.encode("utf-8")).hexdigest()
+
+ payload: dict[str, Any] = {
+ "type": "veritasacta:decision",
+ "spec": "draft-farley-acta-signed-receipts-03",
+ "tool_name": tool,
+ "tool_input_hash": tool_input_hash,
+ "decision": decision,
+ "issued_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
+ "issuer_id": self.issuer_id,
+ "sequence": self.sequence,
+ "previousReceiptHash": self.previous_receipt_hash,
+ }
+ if policy_id:
+ payload["policy_id"] = policy_id
+ if policy_hash:
+ payload["policy_hash"] = policy_hash
+ if skill_version_hash:
+ payload["skill_version_hash"] = skill_version_hash
+ if delegation_chain_root:
+ payload["delegation_chain_root"] = delegation_chain_root
+ if metadata:
+ payload["metadata"] = metadata
+
+ canonical = _canonicalize(payload)
+ sig = self._signing_key.sign(canonical.encode("utf-8")).signature
+
+ # Chain linkage
+ self.previous_receipt_hash = "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+
+ return Receipt(
+ payload=payload,
+ signature={"alg": "EdDSA", "kid": self.kid, "sig": sig.hex()},
+ )
diff --git a/ecosystem/supervisor/DESIGN.md b/ecosystem/supervisor/DESIGN.md
new file mode 100644
index 0000000..b597ce2
--- /dev/null
+++ b/ecosystem/supervisor/DESIGN.md
@@ -0,0 +1,52 @@
+# Runtime Supervisor with Approval Workflows (v0.6.0 target)
+
+Dynamic permission expansion with human-in-the-loop approval. Complements
+sb-runtime's static sandbox with live approval flows for operations that
+fall outside the initial allowlist.
+
+## Goal
+
+Agents need dynamic, just-in-time permissions for legitimate long-tail
+operations without broadening the static sandbox. Supervisor wraps the
+agent with an approval channel that a human (or a higher-authority agent)
+can use to grant or deny out-of-sandbox requests.
+
+## Design
+
+### Supervisor daemon
+
+```
+veritasacta supervisor --policy base.cedar --approval-channel slack://...
+```
+
+- Intercepts deny events from sb-runtime / protect-mcp
+- Routes them through an approval channel (Slack, PagerDuty, CLI tty)
+- On approval, widens the sandbox for a scoped duration
+- Emits signed receipts for both the original deny AND the approval
+
+### Approval receipt format
+
+```json
+{
+ "payload": {
+ "type": "veritasacta:supervisor:approval",
+ "subject_action": { "tool": "...", "args_hash": "..." },
+ "original_decision": "deny",
+ "approved_decision": "allow",
+ "approver": "did:web:jane.acme.example",
+ "approval_scope": { "duration_seconds": 300, "tool_pattern": "curl:github.com/*" },
+ "issued_at": "..."
+ },
+ "signature": { "alg": "EdDSA", "kid": "...", "sig": "..." }
+}
+```
+
+### Composition with Veritas Acta receipts
+
+The approval receipt is chained into the session's receipt stream. An
+auditor walking the chain sees: denial → approval → allow. The agent
+never runs an action without cryptographic evidence of authorization.
+
+## License
+
+Apache-2.0 once shipped.
diff --git a/ecosystem/vscode-extension/README.md b/ecosystem/vscode-extension/README.md
new file mode 100644
index 0000000..c0085b8
--- /dev/null
+++ b/ecosystem/vscode-extension/README.md
@@ -0,0 +1,39 @@
+# Veritas Acta Verify — VS Code Extension (scaffold)
+
+Planned v0.5.1+ artifact. Provides:
+
+- **Syntax highlighting** for `.receipt.json` files
+- **On-save verification** using the installed `@veritasacta/verify` CLI
+- **Sigil art in status bar** when editing a receipt
+- **Hover tooltips** showing field meaning + spec references
+- **Command palette entries**:
+ - `Veritas Acta: Verify current file`
+ - `Veritas Acta: Self-check installed verifier`
+ - `Veritas Acta: Generate sample receipt`
+ - `Veritas Acta: Show Sigil`
+
+## Implementation sketch
+
+TypeScript / Node.js VS Code extension. Uses the installed CLI via
+`child_process`; no in-extension crypto.
+
+```
+vscode-extension/
+├── package.json # extension manifest
+├── src/
+│ ├── extension.ts # activate / commands
+│ ├── diagnostics.ts # run verifier on save, attach problems
+│ ├── hover.ts # field tooltips citing spec sections
+│ └── statusBar.ts # Sigil fingerprint in status bar
+├── language-config.json # Receipt JSON language config
+└── README.md
+```
+
+## v0.5.0 placeholder
+
+This scaffold is documentation-only today. Implementation is tracked
+for v0.5.1 / v0.6.0.
+
+## License
+
+Apache-2.0 (once shipped).
diff --git a/ecosystem/wshobson-plugin/README.md b/ecosystem/wshobson-plugin/README.md
new file mode 100644
index 0000000..2be96af
--- /dev/null
+++ b/ecosystem/wshobson-plugin/README.md
@@ -0,0 +1,77 @@
+# wshobson/agents — `protect-mcp` plugin (PR-staging)
+
+PR-ready tree for submitting `protect-mcp` to [wshobson/agents](https://github.com/wshobson/agents), a 33K-star community Claude Code plugin marketplace.
+
+Directly addresses open issue [#471](https://github.com/wshobson/agents/issues/471).
+
+## Structure
+
+```
+protect-mcp/
+├── .claude-plugin/plugin.json
+├── README.md
+├── skills/protect-mcp-setup/SKILL.md
+├── agents/policy-enforcer.md
+├── agents/receipt-verifier.md
+├── commands/verify-receipt.md
+├── commands/audit-chain.md
+└── hooks/hooks.json
+```
+
+## Submission steps
+
+1. Fork [wshobson/agents](https://github.com/wshobson/agents)
+2. Copy this tree under `plugins/protect-mcp/` in the fork
+3. Add the marketplace entry (see below)
+4. Open PR titled: `Add protect-mcp plugin (closes #471)`
+5. PR body quotes issue #471's request and points to the included skills/agents/commands.
+
+## Marketplace entry (to add under `.claude-plugin/marketplace.json`)
+
+```json
+{
+ "name": "protect-mcp",
+ "source": "./plugins/protect-mcp",
+ "description": "Cedar policy enforcement + Ed25519 signed receipts for Claude Code tool calls. First cryptographic-governance plugin.",
+ "version": "0.1.0",
+ "author": { "name": "Tom Farley", "email": "tommy@scopeblind.com" },
+ "homepage": "https://scopeblind.com",
+ "license": "MIT",
+ "category": "security"
+}
+```
+
+## Suggested PR body
+
+> Closes #471.
+>
+> Adds the `protect-mcp` plugin — first Claude Code plugin that enforces policies **cryptographically**, not just via hooks. Every tool call is:
+>
+> 1. Evaluated against a Cedar policy at `PreToolUse`
+> 2. Signed as an Ed25519 receipt at `PostToolUse`
+> 3. Chain-linked via `previousReceiptHash`
+> 4. Verifiable offline with `@veritasacta/verify`
+>
+> **Why this design.** Hook-only approaches can be bypassed (disable the hook, tool call runs unenforced). Signing the decision makes the receipt tamper-evident — an attacker who disables the hook still cannot forge evidence that a denied action was allowed.
+>
+> **What ships:**
+> - 1 skill — `protect-mcp-setup` (step-by-step setup)
+> - 2 agents — `policy-enforcer` (Cedar authoring), `receipt-verifier` (chain audit)
+> - 2 commands — `/verify-receipt`, `/audit-chain`
+> - hooks.json wiring PreToolUse + PostToolUse
+>
+> **Dependencies:** `protect-mcp` (MIT) + `@veritasacta/verify` (Apache-2.0). Both on npm. No network at runtime.
+>
+> **Reviewer checklist:**
+> - [ ] `claude plugin install wshobson/agents/protect-mcp` installs successfully
+> - [ ] Receipt files appear in `.protect-mcp/receipts/` after a tool call
+> - [ ] `npx @veritasacta/verify .protect-mcp/receipts/*.json` exits 0
+> - [ ] Tampering with a receipt causes exit 1
+>
+> Thanks @ for opening #471 — this PR directly implements the structure you requested.
+
+## Strategic context
+
+- **wshobson/agents has 33.6K stars** — huge discovery surface for Claude Code users.
+- **Pioneers `governance` category** in a marketplace that currently has `security` but no cryptographic-governance entries.
+- **Every install exercises `@veritasacta/verify`** — adoption multiplier for the verifier.
diff --git a/ecosystem/wshobson-plugin/protect-mcp/.claude-plugin/plugin.json b/ecosystem/wshobson-plugin/protect-mcp/.claude-plugin/plugin.json
new file mode 100644
index 0000000..d1a3829
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/.claude-plugin/plugin.json
@@ -0,0 +1,23 @@
+{
+ "name": "protect-mcp",
+ "version": "0.1.0",
+ "description": "Cedar policy enforcement + Ed25519 signed receipts for Claude Code tool calls. First cryptographic-governance plugin.",
+ "author": {
+ "name": "Tom Farley",
+ "email": "tommy@scopeblind.com"
+ },
+ "homepage": "https://scopeblind.com",
+ "license": "MIT",
+ "keywords": [
+ "governance",
+ "security",
+ "policy",
+ "receipts",
+ "ed25519",
+ "audit",
+ "mcp",
+ "protect-mcp",
+ "veritasacta",
+ "scopeblind"
+ ]
+}
diff --git a/ecosystem/wshobson-plugin/protect-mcp/README.md b/ecosystem/wshobson-plugin/protect-mcp/README.md
new file mode 100644
index 0000000..116d867
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/README.md
@@ -0,0 +1,143 @@
+# protect-mcp — Cedar policy + signed receipts for Claude Code
+
+[](https://opensource.org/licenses/MIT)
+
+**Every Claude Code tool call is evaluated against a Cedar policy, then signed as an Ed25519 receipt.** Offline-verifiable. Tamper-evident. No vendor lock-in.
+
+This is the first Claude Code plugin that enforces policies *cryptographically* — not just via hooks. An attacker who bypasses the hook surface still cannot forge the receipt, because forgery requires the Ed25519 signing key.
+
+Closes [wshobson/agents#471](https://github.com/wshobson/agents/issues/471).
+
+## Install
+
+```
+/plugin install protect-mcp
+```
+
+Then:
+
+```bash
+npx protect-mcp init
+```
+
+That single command:
+- Generates an Ed25519 signing key at `.protect-mcp/signer.json`
+- Writes a default Cedar policy at `.protect-mcp/policy.cedar`
+- Wires `PreToolUse` + `PostToolUse` hooks via `hooks/hooks.json`
+- Stores receipts at `.protect-mcp/receipts/NNNN-*.json`
+
+## What it does
+
+| Hook | Action |
+|---|---|
+| `PreToolUse` | Evaluate Cedar policy against `(tool, args, context)`. Allow or deny. |
+| `PostToolUse` | Sign an Ed25519 receipt for the decision. Chain-link via `previousReceiptHash`. |
+
+Receipts are written as they're signed. Every one verifies offline with `@veritasacta/verify`.
+
+## Verify
+
+Inside Claude Code:
+
+```
+/verify-receipt .protect-mcp/receipts/0042.json
+/audit-chain
+```
+
+Or from your terminal:
+
+```bash
+npx @veritasacta/verify .protect-mcp/receipts/*.json --key $(cat .protect-mcp/signer.json | jq -r .public_key)
+```
+
+Tampering with any receipt fails the chain. Guaranteed by SHA-256 + JCS + Ed25519.
+
+## Policy: default behavior
+
+The default Cedar policy allows non-destructive tools (Read, Grep, Glob, WebFetch, WebSearch) and gates writes behind path scoping:
+
+```cedar
+permit(
+ principal == Agent::"claude-code",
+ action in [Action::"Read", Action::"Grep", Action::"Glob"],
+ resource
+);
+
+permit(
+ principal == Agent::"claude-code",
+ action == Action::"Write",
+ resource
+) when {
+ resource.path like "./src/*" || resource.path like "./tests/*"
+};
+
+forbid(
+ principal,
+ action == Action::"Bash",
+ resource
+) when {
+ resource.command like "rm -rf *" ||
+ resource.command like "sudo *" ||
+ resource.command like "*curl * | sh"
+};
+```
+
+Customize `.protect-mcp/policy.cedar` to your environment. The `policy-enforcer` agent (included) can translate natural-language rules into Cedar.
+
+## Cryptographic governance
+
+Every tool call produces a signed artifact in this shape:
+
+```json
+{
+ "payload": {
+ "type": "decision-receipt",
+ "action": "Write",
+ "tool_name": "Write",
+ "agent_id": "claude-code",
+ "issuer_id": "protect-mcp:v0.1",
+ "issued_at": "2026-04-20T...",
+ "previousReceiptHash": "...",
+ "decision": "allow",
+ "args_commitment": "sha256:..."
+ },
+ "signature": { "alg": "EdDSA", "kid": "protect-mcp:local", "sig": "..." }
+}
+```
+
+- **Ed25519** (RFC 8032) — tamper-evident.
+- **JCS** (RFC 8785) — canonical JSON, reproducible hashes.
+- **Chain linkage** — each receipt references the previous, forming an append-only DAG.
+- **Offline verify** — no phone-home, no server, no API key.
+
+## Relationship to the broader ecosystem
+
+This plugin is the Claude Code surface for a broader governance stack:
+
+- **Protocol:** [veritasacta.com](https://veritasacta.com) — IETF drafts, open AIP specs (Apache-2.0).
+- **Verifier:** [`@veritasacta/verify`](https://www.npmjs.com/package/@veritasacta/verify) — offline CLI (Apache-2.0).
+- **Managed issuance (optional):** [scopeblind.com](https://scopeblind.com) — VOPRF anonymous credentials, chain pinning, SIEM export.
+
+The plugin works standalone. Adding managed issuance is optional.
+
+## Agents & commands included
+
+| Kind | Name | Purpose |
+|---|---|---|
+| Agent | `policy-enforcer` | Translates natural-language rules into Cedar policies for Claude Code tools |
+| Agent | `receipt-verifier` | Walks receipt chains, detects tampering, explains Ed25519 + JCS |
+| Skill | `protect-mcp-setup` | Step-by-step setup and verification guide |
+| Command | `/verify-receipt ` | Runs `@veritasacta/verify` on a receipt |
+| Command | `/audit-chain` | Walks the receipt chain from the most recent tip |
+
+## Dependencies
+
+Runtime: Node.js ≥ 18. `protect-mcp` and `@veritasacta/verify` install automatically as npm packages.
+
+No network access required at runtime. Receipts verify fully offline.
+
+## License
+
+MIT. See [LICENSE](https://github.com/wshobson/agents/blob/main/LICENSE).
+
+Parts of this plugin's verifier dependency (`@veritasacta/verify`) are Apache-2.0; patent-adjacent packages carry the Apache-2.0 patent grant.
diff --git a/ecosystem/wshobson-plugin/protect-mcp/agents/policy-enforcer.md b/ecosystem/wshobson-plugin/protect-mcp/agents/policy-enforcer.md
new file mode 100644
index 0000000..712efb7
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/agents/policy-enforcer.md
@@ -0,0 +1,120 @@
+---
+name: policy-enforcer
+description: Expert on Cedar policies for Claude Code tool calls. Use when the user wants to write, modify, or debug a Cedar policy that gates tools like Bash, Edit, Write, Read, Glob, Grep, WebFetch, or WebSearch. Translates natural-language rules into Cedar syntax and validates the result.
+---
+
+You are an expert at writing Cedar policies for agent tool calls. You operate inside Claude Code sessions governed by `protect-mcp`.
+
+## What you know
+
+- **Cedar** (https://www.cedarpolicy.com/) — the policy language: `permit` / `forbid`, `principal`, `action`, `resource`, `when`/`unless`, entity types, string ops (`like`, `==`, `in`).
+- **Claude Code tool schema** — the tools agents invoke: `Bash`, `Edit`, `Write`, `Read`, `Glob`, `Grep`, `WebFetch`, `WebSearch`, `Task`, `TodoWrite`. Each has distinct `resource` attributes.
+- **protect-mcp entity model:**
+ - `principal` — `Agent::"claude-code"`
+ - `action` — `Action::"Bash"`, `Action::"Write"`, `Action::"Read"`, etc., matching the tool name
+ - `resource` — typed per-tool:
+ - `Bash` → `{ command: String, timeout_ms: Long }`
+ - `Read` / `Write` / `Edit` → `{ path: String }`
+ - `Glob` / `Grep` → `{ pattern: String, path: String }`
+ - `WebFetch` → `{ url: String }`
+ - `WebSearch` → `{ query: String, allowed_domains: Set }`
+
+## Cedar patterns you use
+
+### Pattern: allowlist reads, scope writes
+
+```cedar
+permit(
+ principal == Agent::"claude-code",
+ action in [Action::"Read", Action::"Grep", Action::"Glob"],
+ resource
+);
+
+permit(
+ principal == Agent::"claude-code",
+ action == Action::"Write",
+ resource
+) when {
+ resource.path like "./src/*" || resource.path like "./tests/*"
+};
+```
+
+### Pattern: forbid destructive bash
+
+```cedar
+forbid(
+ principal,
+ action == Action::"Bash",
+ resource
+) when {
+ resource.command like "rm -rf *" ||
+ resource.command like "sudo *" ||
+ resource.command like "*curl * | sh" ||
+ resource.command like "*| bash" ||
+ resource.command like "dd if=*" ||
+ resource.command like "mkfs*"
+};
+```
+
+### Pattern: network egress allowlist
+
+```cedar
+permit(
+ principal == Agent::"claude-code",
+ action == Action::"WebFetch",
+ resource
+) when {
+ resource.url like "https://api.anthropic.com/*" ||
+ resource.url like "https://github.com/*" ||
+ resource.url like "https://registry.npmjs.org/*"
+};
+
+forbid(
+ principal,
+ action == Action::"WebFetch",
+ resource
+) when {
+ resource.url like "http://169.254.169.254/*" || // AWS IMDS
+ resource.url like "*metadata.google.internal*" // GCP metadata
+};
+```
+
+### Pattern: secrets-file deny
+
+```cedar
+forbid(
+ principal,
+ action in [Action::"Read", Action::"Glob", Action::"Grep"],
+ resource
+) when {
+ resource.path like "*.pem" ||
+ resource.path like "*id_rsa*" ||
+ resource.path like "*.aws/credentials*" ||
+ resource.path like "*.ssh/*" ||
+ resource.path like "*/.env"
+};
+```
+
+## How you work
+
+1. **Ask what the user wants first.** A Cedar rule is only useful if it matches the operator's intent. If they say "block destructive bash," clarify: `rm`, `mv`, `dd`, `shutdown`, or all of the above?
+
+2. **Draft the rule in Cedar.** Use the patterns above. Compose `permit` rules for allowed paths/commands and `forbid` rules for explicit denies. `forbid` always wins.
+
+3. **Walk through it line by line.** Explain what each clause does. Name the trade-offs. "This allows `./src/*` but will silently also allow `./srcEvil/*` — did you mean that?"
+
+4. **Validate against examples.** Give the user 3-5 test tool calls (both should-allow and should-deny) and trace the policy evaluation by hand.
+
+5. **Write it to `.protect-mcp/policy.cedar`.** Back up the existing file as `.protect-mcp/policy.cedar.bak` first.
+
+## What you do NOT do
+
+- Do not make rules stricter than asked. Overreach creates friction and trains the user to disable the policy entirely.
+- Do not add telemetry, opt-in lists, or "suggested improvements" the user didn't ask for.
+- Do not claim Cedar can enforce things it can't. Cedar decides; the hook enforces. If the hook is bypassed, the policy doesn't matter.
+
+## Composition with sb-runtime and nono
+
+Cedar handles *structural* checks. For *kernel-level* enforcement, compose with sb-runtime (Linux Landlock) and nono (capabilities). This plugin is the Claude Code surface; the stack is layered deeper.
+
+Point users at the profiles under `@veritasacta/verify`'s `ecosystem/profiles/` for ready-made policy.cedar + nono-capabilities.yaml pairs for Claude Code, Cursor, Codex, Gemini CLI, and OpenClaw.
diff --git a/ecosystem/wshobson-plugin/protect-mcp/agents/receipt-verifier.md b/ecosystem/wshobson-plugin/protect-mcp/agents/receipt-verifier.md
new file mode 100644
index 0000000..e19f880
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/agents/receipt-verifier.md
@@ -0,0 +1,100 @@
+---
+name: receipt-verifier
+description: Expert on verifying Ed25519 + JCS receipt chains produced by protect-mcp. Use when a session ends and the user wants to audit the chain, when a verification fails and the failure mode needs explaining, or when investigating suspected tampering.
+---
+
+You are an expert at verifying signed receipt chains produced by `protect-mcp`. You understand Ed25519, JCS canonicalization, hash chaining, and the Veritas Acta receipt format.
+
+## What you know
+
+- **Envelope** — every receipt is `{ payload: {...}, signature: { alg, kid, sig } }`. Payload carries the decision; signature covers JCS-canonicalized payload bytes.
+- **JCS (RFC 8785)** — JSON canonicalization: lexicographic key sort, deep, no whitespace. Same input → same bytes → same hash.
+- **Ed25519 (RFC 8032)** — 32-byte public key, 64-byte signature, deterministic. No randomness, no hidden state.
+- **Chain linkage** — `payload.previousReceiptHash` is the SHA-256 (base64url) of the previous receipt's full envelope (also canonicalized).
+- **receipt_hash** — the chain identifier. SHA-256 of canonical envelope bytes, base64url-encoded, no padding.
+
+## What you do
+
+### For a single receipt
+
+```bash
+npx @veritasacta/verify --key
+```
+
+- Exit 0 → valid.
+- Exit 1 → invalid signature OR broken chain — **proven tampering**.
+- Exit 2 → undecidable: malformed JSON, missing key, unsupported algorithm. Not a failure of the receipt; a failure of inputs.
+
+### For a chain
+
+```bash
+npx @veritasacta/verify chain explore
+```
+
+Walks `previousReceiptHash` back to the root. Surfaces:
+
+- `depth` — how many links were walked
+- `links_broken` — how many links failed hash validation
+- `warnings` — textual summary of each break
+
+A chain with `links_broken > 0` has a break somewhere. The walker reports *which* link and *what hash* was expected versus got.
+
+### For compliance-grade output
+
+```bash
+npx @veritasacta/verify --replay-chain receipts.jsonl \
+ --audit-report --output audit.html
+```
+
+Produces a self-contained HTML document: verification summary, per-receipt breakdown, canonical-release proof if `--attest` is also set. Auditor-ready.
+
+## How you explain failures
+
+**"The signature doesn't verify."**
+- The payload was modified after signing, OR
+- The wrong public key was used, OR
+- The signing implementation produced a malformed signature.
+
+Walk through: what's the `kid`? Does the pubkey match? Is the canonical form of the payload what you expect? (Run `npx @veritasacta/verify --diff old new` to see what changed.)
+
+**"The chain is broken."**
+- A receipt was modified, deleted, inserted, OR
+- The receipt directory is incomplete (ancestor missing).
+
+Use `verify chain explore` with `--json` and inspect `nodes[n].link_valid`. The first `false` identifies the break point.
+
+**"The algorithm is unknown."**
+- You're using an older verifier. Upgrade: `npm i -g @veritasacta/verify@latest`.
+- Or the signer used a non-conformant algorithm. Only `ed25519` / `EdDSA` (and optionally `ed25519+ml-dsa-65` hybrid) are spec-compliant.
+
+## Selective disclosure (AIP-0002)
+
+Receipts MAY carry `_commitments` that hide fields behind SHA-256 commitments. To reveal a field:
+
+```bash
+npx @veritasacta/verify --disclose field_name:salt:value
+```
+
+If the commitment opens correctly, the verifier confirms the original value without the whole receipt ever exposing it.
+
+## Canonical verifier self-check
+
+Supply chain: prove the verifier you're running is the canonical one.
+
+```bash
+npx @veritasacta/verify --self-check
+```
+
+Shows the Sigil (a visual + human name + hex fingerprint). `--pin-sigil ` refuses to run unless installed Sigil matches.
+
+## What you do NOT do
+
+- Do not claim "verified offline" unless you confirm no network was contacted. (The verifier does not phone home. `--jwks ` is the only flag that opens a connection.)
+- Do not attempt to "fix" a broken receipt. Broken means tampered; fixing it would be forging.
+- Do not provide keys the user didn't ask for. The signing key lives in `.protect-mcp/signer.json` and should not leave the user's machine.
+
+## Related standards
+
+- draft-farley-acta-signed-receipts (IETF)
+- AIP-0001 (receipt format), AIP-0002 (selective disclosure), AIP-0003 (holder binding)
+- RFC 8032 (Ed25519), RFC 8785 (JCS), RFC 9497 (VOPRF, used optionally for anonymous metering)
diff --git a/ecosystem/wshobson-plugin/protect-mcp/commands/audit-chain.md b/ecosystem/wshobson-plugin/protect-mcp/commands/audit-chain.md
new file mode 100644
index 0000000..51021df
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/commands/audit-chain.md
@@ -0,0 +1,45 @@
+---
+description: Walk the receipt chain produced by the current session from tip to root, validating every previousReceiptHash link.
+---
+
+Walk the receipt chain for this protect-mcp session.
+
+## What this command does
+
+Finds the most recent receipt in `.protect-mcp/receipts/` and walks backward via `previousReceiptHash`, validating every link. Reports depth, links_broken, and any chain warnings.
+
+## Implementation
+
+```bash
+# Find the most recent receipt by filename (receipts are zero-padded sequentially)
+TIP=$(ls -t .protect-mcp/receipts/*.json 2>/dev/null | head -1)
+
+if [ -z "$TIP" ]; then
+ echo "No receipts found under .protect-mcp/receipts/"
+ echo "Either no session has run, or protect-mcp isn't writing receipts."
+ exit 2
+fi
+
+npx @veritasacta/verify chain explore "$TIP" --search-dir .protect-mcp/receipts
+```
+
+Output is an ASCII tree: tip first, root last, each link annotated with status. A broken link shows `✗` next to the hash.
+
+## Interpreting results
+
+- **`valid=true, links_broken=0`** — entire chain verifies. No tamper detected.
+- **`links_broken > 0`** — a receipt was modified, deleted, or inserted. The warning text identifies the failure point.
+- **`Chain ends at receipt[N]: previousReceiptHash ... not found in searchDir`** — the chain is incomplete. Either the root was reached (if `previousReceiptHash` is absent) or an ancestor file is missing.
+
+For compliance-grade output (HTML report, self-contained, auditor-ready), use:
+
+```bash
+npx @veritasacta/verify --replay-chain .protect-mcp/receipts.jsonl \
+ --audit-report --output audit.html
+```
+
+## When to run
+
+- At session end — confirm no receipts were tampered with during the run.
+- Before shipping an audit bundle — proves chain integrity.
+- After a suspicious tool call — verify the chain wasn't rewritten to hide the event.
diff --git a/ecosystem/wshobson-plugin/protect-mcp/commands/verify-receipt.md b/ecosystem/wshobson-plugin/protect-mcp/commands/verify-receipt.md
new file mode 100644
index 0000000..8edcf36
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/commands/verify-receipt.md
@@ -0,0 +1,41 @@
+---
+description: Verify a single Ed25519 signed receipt produced by protect-mcp. Runs @veritasacta/verify offline against the file and reports exit code semantics.
+argument-hint: ""
+---
+
+Verify a Claude Code receipt produced by protect-mcp.
+
+## What this command does
+
+Runs `npx @veritasacta/verify --key ` against the file path in $ARGUMENTS. Reports:
+
+- **exit 0** — valid: signature + JCS canonicalization + chain linkage all check out
+- **exit 1** — invalid: proven tampering somewhere (signature or chain)
+- **exit 2** — undecidable: malformed JSON, missing key, unsupported algorithm
+
+## Implementation
+
+Run the verification:
+
+```bash
+PUBKEY=$(cat .protect-mcp/signer.json 2>/dev/null | jq -r .public_key)
+
+if [ -z "$PUBKEY" ] || [ "$PUBKEY" = "null" ]; then
+ echo "No .protect-mcp/signer.json found. Run \`npx protect-mcp init\` first."
+ exit 2
+fi
+
+npx @veritasacta/verify "$ARGUMENTS" --key "$PUBKEY"
+```
+
+If the user wants JSON output (for piping or scripting), the `--json` flag produces machine-readable results.
+
+## Interpreting results
+
+| Exit | Meaning | What to tell the user |
+|---|---|---|
+| 0 | Valid | "Receipt verifies. Signature OK. Payload has not been modified." |
+| 1 | Invalid | "Receipt does NOT verify. Someone modified this file after it was signed. This is a proven tamper." |
+| 2 | Undecidable | Explain *which* of: malformed JSON, missing key, unsupported algorithm |
+
+For tamper cases, suggest the `/audit-chain` command to find where in the session the tamper occurred.
diff --git a/ecosystem/wshobson-plugin/protect-mcp/hooks/hooks.json b/ecosystem/wshobson-plugin/protect-mcp/hooks/hooks.json
new file mode 100644
index 0000000..63e9cd5
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/hooks/hooks.json
@@ -0,0 +1,28 @@
+{
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "*",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "npx protect-mcp pre --tool '{{tool_name}}' --args '{{tool_input}}'",
+ "description": "Evaluate Cedar policy for the pending tool call. Deny exits non-zero and blocks the tool."
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "matcher": "*",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "npx protect-mcp post --tool '{{tool_name}}' --args '{{tool_input}}' --result '{{tool_result}}'",
+ "description": "Sign an Ed25519 receipt for the decision. Writes to .protect-mcp/receipts/ and chain-links via previousReceiptHash."
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/ecosystem/wshobson-plugin/protect-mcp/skills/protect-mcp-setup/SKILL.md b/ecosystem/wshobson-plugin/protect-mcp/skills/protect-mcp-setup/SKILL.md
new file mode 100644
index 0000000..1088429
--- /dev/null
+++ b/ecosystem/wshobson-plugin/protect-mcp/skills/protect-mcp-setup/SKILL.md
@@ -0,0 +1,94 @@
+---
+name: protect-mcp-setup
+description: Set up Cedar policy enforcement and Ed25519 signed receipts for every Claude Code tool call. Use when adopting protect-mcp for the first time, when configuring a new policy, or when auditing a session.
+---
+
+# protect-mcp setup
+
+Sign every Claude Code tool call with an offline-verifiable Ed25519 receipt, gated by a Cedar policy.
+
+## When to use this skill
+
+- First-time setup of cryptographic governance in a Claude Code project
+- Configuring a Cedar policy for specific tool/path combinations
+- Verifying the receipts produced during a session
+- Auditing a session's receipt chain for tampering
+
+## Step 1: Install and initialize
+
+```bash
+npm install -g protect-mcp @veritasacta/verify
+npx protect-mcp init
+```
+
+What `init` does:
+
+1. Generates an Ed25519 keypair at `.protect-mcp/signer.json`
+2. Writes a default policy at `.protect-mcp/policy.cedar`
+3. Creates `.protect-mcp/receipts/` for receipt storage
+4. Installs `hooks/hooks.json` — PreToolUse + PostToolUse triggers
+
+## Step 2: Review the default policy
+
+Open `.protect-mcp/policy.cedar`. The default allows reads + grep/glob, permits writes under `./src/` and `./tests/`, and forbids destructive bash. Edit to suit.
+
+The `policy-enforcer` agent (shipped with the plugin) can translate rules from plain English — "never allow curl piped to sh" — into Cedar.
+
+## Step 3: Run a session
+
+Any Claude Code session that uses tools will now:
+
+- Evaluate the Cedar policy at `PreToolUse` — deny decisions surface in the transcript
+- Sign a receipt at `PostToolUse` — written to `.protect-mcp/receipts/NNNN-tool.json`
+
+Receipts chain via `previousReceiptHash`. The chain is append-only. Any tamper breaks the chain under offline verification.
+
+## Step 4: Verify
+
+Use the plugin's built-in commands:
+
+- `/verify-receipt ` — verify a single receipt
+- `/audit-chain` — walk the chain from the most recent tip
+
+Or from the shell:
+
+```bash
+npx @veritasacta/verify .protect-mcp/receipts/*.json \
+ --key $(cat .protect-mcp/signer.json | jq -r .public_key)
+```
+
+Exit 0 means every receipt verified. Exit 1 means tampering. Exit 2 means malformed or undecidable.
+
+For a compliance-grade report:
+
+```bash
+npx @veritasacta/verify --replay-chain .protect-mcp/receipts.jsonl \
+ --audit-report --output audit.html
+```
+
+## Step 5: (optional) Pin the verifier
+
+Supply chain: make sure the verifier you run is the canonical one.
+
+```bash
+npx @veritasacta/verify --self-check
+```
+
+Every verify invocation accepts `--pin-sigil ` to refuse to run unless the installed verifier matches a specific Sigil.
+
+## Troubleshooting
+
+**Receipts aren't appearing.** Check that `hooks/hooks.json` was registered — `claude config list | grep hooks` should show PreToolUse and PostToolUse entries for protect-mcp.
+
+**"cedar_policy_denied" on unexpected tools.** Run `/policy-enforcer` to propose a narrower Cedar rule, or edit `.protect-mcp/policy.cedar` directly.
+
+**Verification fails with `hash_mismatch`.** Someone modified a receipt. The chain is tamper-evident — this is working as designed.
+
+**Verification fails with `unknown_algorithm`.** You're using a verifier older than v0.5.0. Upgrade: `npm i -g @veritasacta/verify@latest`.
+
+## Related
+
+- Cedar docs: https://www.cedarpolicy.com/
+- `@veritasacta/verify`: https://www.npmjs.com/package/@veritasacta/verify
+- Veritas Acta protocol: https://veritasacta.com
+- IETF drafts: draft-farley-acta-signed-receipts
diff --git a/generate-sigil.mjs b/generate-sigil.mjs
new file mode 100644
index 0000000..1022f0a
--- /dev/null
+++ b/generate-sigil.mjs
@@ -0,0 +1,198 @@
+#!/usr/bin/env node
+
+/**
+ * generate-sigil.mjs — Generate the Sigil commitment for this release.
+ *
+ * Run this ONCE per release, AFTER the source code is frozen.
+ * It computes SHA-256 of cli.js, builds a policy, derives the Sigil,
+ * and writes sigil.json.
+ *
+ * The Veritas Acta project keypair is stored in sigil-key.json (PRIVATE,
+ * never published to npm). The public key is embedded in sigil.json
+ * (published with the package).
+ *
+ * Usage:
+ * node generate-sigil.mjs [--init] # --init creates a new keypair
+ * node generate-sigil.mjs # derives Sigil from existing key
+ */
+
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { createHash } from 'node:crypto';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+// ── Ed25519 key generation (using Node.js built-in) ──────────────
+
+async function generateKeypair() {
+ const { generateKeyPairSync } = await import('node:crypto');
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
+ const pubRaw = publicKey.export({ type: 'spki', format: 'der' });
+ const privRaw = privateKey.export({ type: 'pkcs8', format: 'der' });
+ // Ed25519 SPKI DER: last 32 bytes are the raw public key
+ const pubHex = pubRaw.subarray(pubRaw.length - 32).toString('hex');
+ const privHex = privRaw.toString('hex');
+ return { pubHex, privHex };
+}
+
+// ── SHA-256 of file ──────────────────────────────────────────────
+
+function sha256File(filepath) {
+ const content = readFileSync(filepath);
+ return createHash('sha256').update(content).digest('hex');
+}
+
+// ── Sigil derivation (matches web/src/lib/sigil/sigil.ts) ────────
+
+function sigilDerive(pubKeyHex, policyHash, nonce = 0) {
+ const domain = Buffer.from('scopeblind:sigil:v2');
+ const pubKey = Buffer.from(pubKeyHex, 'hex');
+ const policy = Buffer.from(policyHash, 'hex');
+ const input = Buffer.concat([domain, pubKey, policy, Buffer.from([nonce & 0xff])]);
+ return createHash('sha256').update(input).digest('hex');
+}
+
+// ── Human-readable name from fingerprint ─────────────────────────
+
+const NAME_ADJ = [
+ 'Bright', 'Quiet', 'Deep', 'Bold', 'Pale', 'Warm', 'Still', 'Swift',
+ 'Clear', 'Dark', 'First', 'True', 'Slow', 'Fair', 'Old', 'New',
+ 'Gilded', 'Woven', 'Open', 'High', 'Lone', 'Kind', 'Keen', 'Wild',
+];
+const NAME_NOUN = [
+ 'Ember', 'Harbor', 'Field', 'Beacon', 'River', 'Grove', 'Arrow', 'Stone',
+ 'Ridge', 'Wind', 'Tide', 'Star', 'Vale', 'Peak', 'Lake', 'Dawn',
+ 'Reed', 'Cairn', 'Orchard', 'Meadow', 'Hearth', 'Anchor', 'Vessel', 'Thread',
+];
+
+function sigilName(fingerprint) {
+ const n = parseInt(fingerprint.slice(0, 4), 16);
+ const m = parseInt(fingerprint.slice(4, 8), 16);
+ return `${NAME_ADJ[n % NAME_ADJ.length]} ${NAME_NOUN[m % NAME_NOUN.length]}`;
+}
+
+// ── Main ─────────────────────────────────────────────────────────
+
+const keyPath = join(__dirname, 'sigil-key.json');
+const sigilPath = join(__dirname, 'sigil.json');
+const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
+
+// Init mode: generate a new keypair
+if (process.argv.includes('--init')) {
+ if (existsSync(keyPath)) {
+ console.error('sigil-key.json already exists. Delete it first if you want to regenerate.');
+ process.exit(1);
+ }
+ const { pubHex, privHex } = await generateKeypair();
+ writeFileSync(keyPath, JSON.stringify({ pubHex, privHex }, null, 2) + '\n');
+ console.log(`✓ Generated Veritas Acta project keypair`);
+ console.log(` Public key: ${pubHex}`);
+ console.log(` Saved to: sigil-key.json (KEEP PRIVATE — do NOT publish to npm)`);
+}
+
+// Load existing keypair
+if (!existsSync(keyPath)) {
+ console.error('No sigil-key.json found. Run: node generate-sigil.mjs --init');
+ process.exit(1);
+}
+
+const key = JSON.parse(readFileSync(keyPath, 'utf-8'));
+
+// Compute source hash over cli.js PLUS monitored engine/util files.
+// v0.5.0 Sigil commits to the entire verification surface, not just cli.js,
+// so that modification of any engine file invalidates --self-check.
+const MONITORED_FILES = [
+ 'cli.js',
+ 'src/detect.js',
+ 'src/conformance.js',
+ 'src/errors.js',
+ 'src/engines/ed25519-receipt.js',
+ 'src/engines/voprf-token.js',
+ 'src/engines/knowledge-unit.js',
+ 'src/engines/selective-disclosure.js',
+ 'src/engines/sigil.js',
+ 'src/engines/attestation.js',
+ 'src/engines/bulk.js',
+ 'src/engines/diff.js',
+ 'src/engines/init.js',
+ 'src/engines/proxy.js',
+ 'src/engines/daemon.js',
+ 'src/engines/prompt.js',
+ 'src/engines/chain-explore.js',
+ 'src/engines/compliance-export.js',
+ 'src/engines/dsse.js',
+ 'src/engines/delegation.js',
+ 'src/engines/cosign.js',
+ 'src/engines/dashboard.js',
+ 'src/engines/rekor.js',
+ 'src/engines/attestation-quote.js',
+ 'src/engines/watch.js',
+ 'src/engines/sbom.js',
+ 'src/engines/transparency.js',
+ 'src/context/live-context.js',
+ 'src/output/terminal.js',
+ 'src/output/json.js',
+ 'src/output/html-report.js',
+ 'src/util/canonical.js',
+ 'src/util/hex.js',
+ 'src/util/jwks.js',
+ 'src/util/audit-log.js',
+ 'src/util/fips.js',
+ 'src/util/voprf-crypto.js',
+ 'src/util/voprf-crypto-v2.js',
+];
+
+import { readFileSync as _readFileSync } from 'node:fs';
+const bufs = [];
+for (const rel of MONITORED_FILES) {
+ try { bufs.push(_readFileSync(join(__dirname, rel))); }
+ catch (e) { console.error(` WARNING: monitored file missing: ${rel}`); }
+}
+const combined = Buffer.concat(bufs);
+const sourceHash = createHash('sha256').update(combined).digest('hex');
+
+// Build the policy (v0.5.0 schema)
+const policy = {
+ version: 3,
+ package: pkg.name,
+ package_version: pkg.version,
+ source_hash: sourceHash,
+ monitored_files: MONITORED_FILES,
+ ietf_draft: 'draft-farley-acta-signed-receipts-03',
+ conformance_tier: 'T4',
+ supported_algorithms: ['ed25519', 'EdDSA', 'voprf-p256-sha256'],
+ created_at: Date.now(),
+};
+
+// Compute policy hash
+const policyJson = JSON.stringify(policy);
+const policyHash = createHash('sha256').update(policyJson).digest('hex');
+
+// Derive Sigil
+const sigilHash = sigilDerive(key.pubHex, policyHash);
+const fingerprint = sigilHash.slice(0, 8);
+const name = sigilName(fingerprint);
+
+// Write sigil.json (this is published with the package)
+const sigil = {
+ sigil_version: 1,
+ fingerprint,
+ name,
+ sigil_hash: sigilHash,
+ project_public_key: key.pubHex,
+ policy,
+ policy_hash: policyHash,
+ derived_at: new Date().toISOString(),
+};
+
+writeFileSync(sigilPath, JSON.stringify(sigil, null, 2) + '\n');
+
+console.log(`\n✓ Sigil committed for ${pkg.name}@${pkg.version}`);
+console.log(` Name: ${name}`);
+console.log(` Fingerprint: ${fingerprint}`);
+console.log(` Source hash: ${sourceHash.slice(0, 16)}...`);
+console.log(` Policy hash: ${policyHash.slice(0, 16)}...`);
+console.log(` Sigil hash: ${sigilHash.slice(0, 16)}...`);
+console.log(` Written to: sigil.json`);
+console.log(`\n Anyone can verify: npx @veritasacta/verify --self-check\n`);
diff --git a/package-lock.json b/package-lock.json
index 71f4c5c..b7b62d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,431 +1,27 @@
{
"name": "@veritasacta/verify",
- "version": "0.1.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@veritasacta/verify",
- "version": "0.1.0",
- "license": "FSL-1.1-MIT",
- "dependencies": {
- "@noble/curves": "^1.3.0",
- "@noble/hashes": "^1.3.0"
- },
- "devDependencies": {
- "vitest": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@jest/schemas": {
- "version": "29.6.3",
- "dev": true,
+ "version": "0.6.1",
"license": "MIT",
"dependencies": {
- "@sinclair/typebox": "^0.27.8"
+ "@veritasacta/artifacts": "^0.2.0"
+ },
+ "bin": {
+ "verify-artifact": "cli.js"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": ">=18.0.0"
}
},
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@noble/curves": {
"version": "1.9.7",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
+ "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
@@ -439,6 +35,8 @@
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
@@ -447,1245 +45,17 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
- "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
- "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.59.0",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
- "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
- "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
- "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
- "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
- "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
- "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
- "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
- "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
- "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
- "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
- "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
- "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
- "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
- "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
- "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
- "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
- "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
- "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
- "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
- "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@sinclair/typebox": {
- "version": "0.27.10",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.8",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/expect": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "1.6.1",
- "@vitest/utils": "1.6.1",
- "chai": "^4.3.10"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "1.6.1",
- "p-limit": "^5.0.0",
- "pathe": "^1.1.1"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "magic-string": "^0.30.5",
- "pathe": "^1.1.1",
- "pretty-format": "^29.7.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^2.2.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "diff-sequences": "^29.6.3",
- "estree-walker": "^3.0.3",
- "loupe": "^2.3.7",
- "pretty-format": "^29.7.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/acorn": {
- "version": "8.16.0",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-walk": {
- "version": "8.3.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.11.0"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/ansi-styles": {
- "version": "5.2.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/assertion-error": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/chai": {
- "version": "4.5.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^1.1.0",
- "check-error": "^1.0.3",
- "deep-eql": "^4.1.3",
- "get-func-name": "^2.0.2",
- "loupe": "^2.3.6",
- "pathval": "^1.1.1",
- "type-detect": "^4.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/check-error": {
- "version": "1.0.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-func-name": "^2.0.2"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/confbox": {
- "version": "0.1.8",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-eql": {
- "version": "4.1.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "type-detect": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/diff-sequences": {
- "version": "29.6.3",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/esbuild": {
- "version": "0.21.5",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/execa": {
- "version": "8.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/get-func-name": {
- "version": "2.0.2",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/get-stream": {
- "version": "8.0.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/human-signals": {
- "version": "5.0.0",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=16.17.0"
- }
- },
- "node_modules/is-stream": {
- "version": "3.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/js-tokens": {
- "version": "9.0.1",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/local-pkg": {
- "version": "0.5.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mlly": "^1.7.3",
- "pkg-types": "^1.2.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/loupe": {
- "version": "2.3.7",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-func-name": "^2.0.1"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/mimic-fn": {
- "version": "4.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/mlly": {
- "version": "1.8.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.16.0",
- "pathe": "^2.0.3",
- "pkg-types": "^1.3.1",
- "ufo": "^1.6.3"
- }
- },
- "node_modules/mlly/node_modules/pathe": {
- "version": "2.0.3",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/npm-run-path": {
- "version": "5.3.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path/node_modules/path-key": {
- "version": "4.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/onetime": {
- "version": "6.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-limit": {
- "version": "5.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/pathe": {
- "version": "1.1.2",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathval": {
- "version": "1.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/pkg-types": {
- "version": "1.3.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "confbox": "^0.1.8",
- "mlly": "^1.7.4",
- "pathe": "^2.0.1"
- }
- },
- "node_modules/pkg-types/node_modules/pathe": {
- "version": "2.0.3",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/postcss": {
- "version": "8.5.8",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/pretty-format": {
- "version": "29.7.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/schemas": "^29.6.3",
- "ansi-styles": "^5.0.0",
- "react-is": "^18.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/react-is": {
- "version": "18.3.1",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/rollup": {
- "version": "4.59.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.59.0",
- "@rollup/rollup-android-arm64": "4.59.0",
- "@rollup/rollup-darwin-arm64": "4.59.0",
- "@rollup/rollup-darwin-x64": "4.59.0",
- "@rollup/rollup-freebsd-arm64": "4.59.0",
- "@rollup/rollup-freebsd-x64": "4.59.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
- "@rollup/rollup-linux-arm64-gnu": "4.59.0",
- "@rollup/rollup-linux-arm64-musl": "4.59.0",
- "@rollup/rollup-linux-loong64-gnu": "4.59.0",
- "@rollup/rollup-linux-loong64-musl": "4.59.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
- "@rollup/rollup-linux-ppc64-musl": "4.59.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
- "@rollup/rollup-linux-riscv64-musl": "4.59.0",
- "@rollup/rollup-linux-s390x-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-musl": "4.59.0",
- "@rollup/rollup-openbsd-x64": "4.59.0",
- "@rollup/rollup-openharmony-arm64": "4.59.0",
- "@rollup/rollup-win32-arm64-msvc": "4.59.0",
- "@rollup/rollup-win32-ia32-msvc": "4.59.0",
- "@rollup/rollup-win32-x64-gnu": "4.59.0",
- "@rollup/rollup-win32-x64-msvc": "4.59.0",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/strip-final-newline": {
- "version": "3.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/strip-literal": {
- "version": "2.1.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinypool": {
- "version": "0.8.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "2.2.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/type-detect": {
- "version": "4.1.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/ufo": {
- "version": "1.6.3",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vite": {
- "version": "5.4.21",
- "dev": true,
+ "node_modules/@veritasacta/artifacts": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@veritasacta/artifacts/-/artifacts-0.2.0.tgz",
+ "integrity": "sha512-8Wg0VrLxB6mAcRpddGML5B5X5Bb0Cmh+o/AkIsAgncsEHRT8mIcqeTolWADs3GyX3fciiUgXDrVYkBNPh//7bQ==",
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
- },
- "bin": {
- "vite": "bin/vite.js"
+ "@noble/curves": "^1.8.0",
+ "@noble/hashes": "^1.7.0"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
- }
- },
- "node_modules/vite-node": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.3.4",
- "pathe": "^1.1.1",
- "picocolors": "^1.0.0",
- "vite": "^5.0.0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vitest": {
- "version": "1.6.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "1.6.1",
- "@vitest/runner": "1.6.1",
- "@vitest/snapshot": "1.6.1",
- "@vitest/spy": "1.6.1",
- "@vitest/utils": "1.6.1",
- "acorn-walk": "^8.3.2",
- "chai": "^4.3.10",
- "debug": "^4.3.4",
- "execa": "^8.0.1",
- "local-pkg": "^0.5.0",
- "magic-string": "^0.30.5",
- "pathe": "^1.1.1",
- "picocolors": "^1.0.0",
- "std-env": "^3.5.0",
- "strip-literal": "^2.0.0",
- "tinybench": "^2.5.1",
- "tinypool": "^0.8.3",
- "vite": "^5.0.0",
- "vite-node": "1.6.1",
- "why-is-node-running": "^2.2.2"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "1.6.1",
- "@vitest/ui": "1.6.1",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yocto-queue": {
- "version": "1.2.2",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=18.0.0"
}
}
}
diff --git a/package.json b/package.json
index 909488d..c98cad1 100644
--- a/package.json
+++ b/package.json
@@ -1,56 +1,63 @@
{
"name": "@veritasacta/verify",
- "version": "0.2.5",
- "description": "Verify signed receipts offline. No accounts, no API calls, no trust required. Ed25519 + JCS.",
+ "version": "0.6.1",
+ "mcpName": "io.github.tomjwxf/veritasacta-verify",
+ "description": "Unified offline verifier for signed decision receipts (Ed25519), VOPRF anonymous-credential tokens, Knowledge Unit bundles, and selective-disclosure receipts. Sigil-verified canonical release.",
"license": "Apache-2.0",
- "author": "Tom Farley ",
- "homepage": "https://github.com/VeritasActa/verify",
- "repository": {
- "type": "git",
- "url": "https://github.com/VeritasActa/verify"
+ "type": "module",
+ "bin": {
+ "verify-artifact": "cli.js"
},
- "bugs": {
- "url": "https://github.com/VeritasActa/verify/issues"
+ "files": [
+ "cli.js",
+ "src/",
+ "sigil.json",
+ "README.md",
+ "CHANGELOG.md",
+ "THREAT-MODEL.md",
+ "SECURITY.md",
+ "ERRORS.md",
+ "ROADMAP.md",
+ "samples/",
+ "test/conformance.js"
+ ],
+ "dependencies": {
+ "@veritasacta/artifacts": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
},
"keywords": [
"verify",
"ed25519",
+ "voprf",
"receipts",
- "offline-verification",
- "cryptography",
+ "knowledge-units",
+ "selective-disclosure",
+ "veritasacta",
+ "scopeblind",
"audit",
- "compliance",
- "agent-governance",
+ "offline-verification",
+ "sigil",
"mcp",
- "veritasacta",
- "acta"
+ "protect-mcp",
+ "agent-safety",
+ "agent-governance"
],
- "type": "module",
- "main": "src/index.js",
- "bin": {
- "veritasacta-verify": "src/cli.js"
- },
- "exports": {
- ".": "./src/index.js",
- "./cli": "./src/cli.js"
- },
- "files": [
- "src/",
- "LICENSE",
- "README.md"
- ],
- "engines": {
- "node": ">=18"
- },
- "dependencies": {
- "@noble/hashes": "^1.3.0",
- "@noble/curves": "^1.3.0"
+ "scripts": {
+ "test": "node --test test/unit/*.test.js test/integration/*.test.js",
+ "test:conformance": "node test/conformance.js",
+ "self-test": "node cli.js --self-test",
+ "self-check": "node cli.js --self-check",
+ "capabilities": "node cli.js --capabilities",
+ "generate-sigil": "node generate-sigil.mjs"
},
- "devDependencies": {
- "vitest": "^1.0.0"
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/VeritasActa/verify.git"
},
- "scripts": {
- "test": "vitest run",
- "test:watch": "vitest"
+ "homepage": "https://veritasacta.com",
+ "bugs": {
+ "url": "https://github.com/VeritasActa/verify/issues"
}
}
diff --git a/samples/sample-bundle.json b/samples/sample-bundle.json
new file mode 100644
index 0000000..5da96e1
--- /dev/null
+++ b/samples/sample-bundle.json
@@ -0,0 +1,78 @@
+{
+ "format": "scopeblind:audit-bundle:v1",
+ "generated_at": "2026-03-22T00:00:00Z",
+ "issuer": "protect-mcp",
+ "description": "Sample audit bundle with 3 receipts from a protect-mcp session.",
+ "verification": {
+ "signing_keys": [
+ {
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+ "use": "sig"
+ }
+ ]
+ },
+ "receipts": [
+ {
+ "v": 2,
+ "type": "decision_receipt",
+ "algorithm": "ed25519",
+ "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
+ "issuer": "sb:test",
+ "issued_at": "2026-01-01T00:00:00Z",
+ "payload": {
+ "decision": "allow",
+ "policy_digest": "sha256:abcdef0123456789",
+ "scope": "my-service",
+ "tool": "read_database",
+ "tier": "signed-known",
+ "mode": "shadow",
+ "reason_code": "policy_match",
+ "request_id": "req_test_001"
+ },
+ "signature": "324a966f8d4e6652e2270311c9682157d5adc01f1f019d84b24a1125220869a5c5a0fc0096ed3afffaa66ac36cfbbd97e60d9c5f7ad632a2cf11c45c2c50fd0d"
+ },
+ {
+ "v": 2,
+ "type": "gateway_restraint",
+ "algorithm": "ed25519",
+ "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
+ "issuer": "sb:protect",
+ "issued_at": "2026-01-01T00:01:00Z",
+ "payload": {
+ "tool": "delete_user",
+ "decision": "deny",
+ "reason_code": "tier_insufficient",
+ "policy_digest": "sha256:abcdef0123456789",
+ "agent_id": "sb:agent:test-bot",
+ "tier": "unknown",
+ "mode": "enforce"
+ },
+ "signature": "e0acddfd57dac1d1cd1a7b48d4554c81cede6bf44aa40f97ed5cbf8825e4157ec1fb44527b0b2cc17b24e5853b2190e02e5c7a826c3919592e693f09c04fac0e"
+ },
+ {
+ "v": 2,
+ "type": "trust_ticket",
+ "algorithm": "ed25519",
+ "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
+ "issuer": "sb:trust-authority",
+ "issued_at": "2026-01-15T12:00:00Z",
+ "payload": {
+ "tier": "evidenced",
+ "scope": "production",
+ "expires_at": "2026-02-01T00:00:00Z",
+ "agent_id": "sb:agent:verified-bot",
+ "manifest_hash": "sha256:manifest_hash_example",
+ "evidence_summary": {
+ "receipt_count": 150,
+ "epoch_span": 30,
+ "issuer_count": 5
+ }
+ },
+ "signature": "c4e5b0048449d0f6757877410d3066f285dc07d1f73ec1e4f6222ec2f3a95a3953db3f3c9a3079c7a7cef837e36d522791a095884633f52d42876b1b25ae6600"
+ }
+ ],
+ "_note": "Verify with: npx @veritasacta/verify sample-bundle.json --bundle"
+}
diff --git a/samples/sample-receipt.json b/samples/sample-receipt.json
new file mode 100644
index 0000000..cb24e58
--- /dev/null
+++ b/samples/sample-receipt.json
@@ -0,0 +1,19 @@
+{
+ "v": 2,
+ "type": "decision_receipt",
+ "algorithm": "ed25519",
+ "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
+ "issuer": "sb:test",
+ "issued_at": "2026-01-01T00:00:00Z",
+ "payload": {
+ "decision": "allow",
+ "policy_digest": "sha256:abcdef0123456789",
+ "scope": "my-service",
+ "tool": "read_database",
+ "tier": "signed-known",
+ "mode": "shadow",
+ "reason_code": "policy_match",
+ "request_id": "req_test_001"
+ },
+ "signature": "324a966f8d4e6652e2270311c9682157d5adc01f1f019d84b24a1125220869a5c5a0fc0096ed3afffaa66ac36cfbbd97e60d9c5f7ad632a2cf11c45c2c50fd0d"
+}
diff --git a/schemas/delegation-receipt.schema.json b/schemas/delegation-receipt.schema.json
new file mode 100644
index 0000000..f69755a
--- /dev/null
+++ b/schemas/delegation-receipt.schema.json
@@ -0,0 +1,62 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://veritasacta.com/schemas/aip-0006/delegation-receipt.schema.json",
+ "title": "AIP-0006 Delegation Receipt",
+ "description": "A scoped, time-bounded authority transfer signed by a delegator over a delegate.",
+ "type": "object",
+ "required": ["payload", "signature"],
+ "additionalProperties": true,
+ "properties": {
+ "payload": {
+ "type": "object",
+ "required": [
+ "type", "delegator_kid", "delegate_kid", "scope",
+ "expires_at", "issued_at", "issuer_id"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "type": { "const": "delegation" },
+ "delegator_kid": { "type": "string", "minLength": 1 },
+ "delegate_kid": { "type": "string", "minLength": 1 },
+ "scope": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "tools": { "type": "array", "items": { "type": "string" } },
+ "targets": { "type": "array", "items": { "type": "string" } },
+ "resources": { "type": "array", "items": { "type": "string" } },
+ "max_depth": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "expires_at": { "type": "string", "format": "date-time" },
+ "parent_delegation_hash": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "issued_at": { "type": "string", "format": "date-time" },
+ "issuer_id": { "type": "string", "minLength": 1 }
+ }
+ },
+ "signature": {
+ "type": "object",
+ "required": ["alg", "kid", "sig"],
+ "properties": {
+ "alg": { "type": "string", "enum": ["ed25519", "EdDSA", "ed25519+ml-dsa-65"] },
+ "kid": { "type": "string", "minLength": 1 },
+ "sig": { "type": "string", "minLength": 1 }
+ }
+ },
+ "cosignatures": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["alg", "kid", "sig"],
+ "properties": {
+ "alg": { "type": "string" },
+ "kid": { "type": "string" },
+ "sig": { "type": "string" }
+ }
+ }
+ }
+ }
+}
diff --git a/server.json b/server.json
new file mode 100644
index 0000000..69a1fc3
--- /dev/null
+++ b/server.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
+ "name": "io.github.tomjwxf/veritasacta-verify",
+ "description": "Offline receipt verifier. Ed25519 signature verification without contacting any server.",
+ "repository": {
+ "url": "https://github.com/scopeblind/ScopeBlindD2",
+ "source": "github",
+ "subfolder": "packages/verify-cli"
+ },
+ "version": "0.2.1",
+ "packages": [
+ {
+ "registryType": "npm",
+ "identifier": "@veritasacta/verify",
+ "version": "0.2.1",
+ "transport": {
+ "type": "stdio"
+ },
+ "environmentVariables": []
+ }
+ ]
+}
diff --git a/sigil.json b/sigil.json
new file mode 100644
index 0000000..012112b
--- /dev/null
+++ b/sigil.json
@@ -0,0 +1,63 @@
+{
+ "sigil_version": 1,
+ "fingerprint": "90b32067",
+ "name": "True Dawn",
+ "sigil_hash": "90b320670c5e85d15f17b182738bd25771f58b7ed2100c72b6fb332597d96a9a",
+ "project_public_key": "fe665e861867cec7e171c0c13bbc873c3362079faef21f54df7804b7fb9ae8af",
+ "policy": {
+ "version": 3,
+ "package": "@veritasacta/verify",
+ "package_version": "0.6.1",
+ "source_hash": "610f4996e57755ea4a9ead70173f86071a0f2caf7e78da1434d0c1c163ba541d",
+ "monitored_files": [
+ "cli.js",
+ "src/detect.js",
+ "src/conformance.js",
+ "src/errors.js",
+ "src/engines/ed25519-receipt.js",
+ "src/engines/voprf-token.js",
+ "src/engines/knowledge-unit.js",
+ "src/engines/selective-disclosure.js",
+ "src/engines/sigil.js",
+ "src/engines/attestation.js",
+ "src/engines/bulk.js",
+ "src/engines/diff.js",
+ "src/engines/init.js",
+ "src/engines/proxy.js",
+ "src/engines/daemon.js",
+ "src/engines/prompt.js",
+ "src/engines/chain-explore.js",
+ "src/engines/compliance-export.js",
+ "src/engines/dsse.js",
+ "src/engines/delegation.js",
+ "src/engines/cosign.js",
+ "src/engines/dashboard.js",
+ "src/engines/rekor.js",
+ "src/engines/attestation-quote.js",
+ "src/engines/watch.js",
+ "src/engines/sbom.js",
+ "src/engines/transparency.js",
+ "src/context/live-context.js",
+ "src/output/terminal.js",
+ "src/output/json.js",
+ "src/output/html-report.js",
+ "src/util/canonical.js",
+ "src/util/hex.js",
+ "src/util/jwks.js",
+ "src/util/audit-log.js",
+ "src/util/fips.js",
+ "src/util/voprf-crypto.js",
+ "src/util/voprf-crypto-v2.js"
+ ],
+ "ietf_draft": "draft-farley-acta-signed-receipts-03",
+ "conformance_tier": "T4",
+ "supported_algorithms": [
+ "ed25519",
+ "EdDSA",
+ "voprf-p256-sha256"
+ ],
+ "created_at": 1778927172239
+ },
+ "policy_hash": "225de862237f54ed4b5bf6e4381eba0dd72ff3fd3b0bf593766302e71d08af8e",
+ "derived_at": "2026-05-16T10:26:12.239Z"
+}
diff --git a/src/conformance.js b/src/conformance.js
new file mode 100644
index 0000000..e2223ea
--- /dev/null
+++ b/src/conformance.js
@@ -0,0 +1,85 @@
+/**
+ * Conformance tier detection.
+ *
+ * Tiers indicate which verification capabilities a specific verification
+ * exercised. They are reported per-verification (not per-implementation),
+ * so a single verifier can emit different tiers for different receipts.
+ *
+ * T1 Basic — Ed25519 + JCS + chain linkage
+ * T2 Disclosure — T1 + AIP-0002 selective disclosure verification
+ * T3 Attestation — T2 + attestation_mode recognition + anchor_uri field surfaced
+ * T4 Privacy — T3 + VOPRF token verification + holder_binding surfaced
+ * T5 Full — T4 + ZK compliance proof verification (v1.0+)
+ *
+ * References:
+ * - v0.5.0-verifier-plan.md §4.6
+ *
+ * @module verify-cli/src/conformance
+ * @license Apache-2.0
+ */
+
+/**
+ * @typedef {Object} TierInput
+ * @property {string} mode
+ * @property {Object} [payloadFields]
+ * @property {number} [disclosuresVerified]
+ * @property {boolean} [voprfVerified]
+ */
+
+/**
+ * @param {TierInput} input
+ * @returns {{tier: 1|2|3|4|5, label: string, features: string[]}}
+ */
+export function detectTier(input) {
+ const features = [];
+ let tier = 1;
+ const p = input.payloadFields || {};
+
+ // T1: Ed25519 + JCS + chain. All base modes qualify.
+ features.push('ed25519-signature');
+ features.push('jcs-canonicalization');
+ if (p.previousReceiptHash !== undefined) features.push('chain-linkage');
+
+ // T2: Selective disclosure verified or present
+ if (input.disclosuresVerified && input.disclosuresVerified > 0) {
+ tier = Math.max(tier, 2);
+ features.push('selective-disclosure');
+ }
+
+ // T3: Attestation / anchor fields surfaced
+ if (p.attestation_mode && p.attestation_mode !== 'software') {
+ tier = Math.max(tier, 3);
+ features.push(`attestation:${p.attestation_mode}`);
+ }
+ if (p.anchor_uri) {
+ tier = Math.max(tier, 3);
+ features.push('anchor-uri');
+ }
+
+ // T4: VOPRF verification or holder_binding present
+ if (input.mode === 'voprf-token' || input.voprfVerified) {
+ tier = Math.max(tier, 4);
+ features.push('voprf');
+ }
+ if (p.holder_binding) {
+ tier = Math.max(tier, 4);
+ features.push('holder-binding');
+ }
+
+ // T5: ZK compliance proof (v1.0+, reserved)
+ if (p.compliance_credit_ref) {
+ features.push('compliance-credit-ref');
+ // Don't yet elevate to T5; that requires full verification of the
+ // ZK proof, which is v1.0+.
+ }
+
+ const labels = {
+ 1: 'T1 basic',
+ 2: 'T2 disclosure',
+ 3: 'T3 attestation',
+ 4: 'T4 privacy',
+ 5: 'T5 full',
+ };
+
+ return { tier, label: labels[tier], features };
+}
diff --git a/src/context/live-context.js b/src/context/live-context.js
new file mode 100644
index 0000000..5cadf72
--- /dev/null
+++ b/src/context/live-context.js
@@ -0,0 +1,172 @@
+/**
+ * Live-context provider for Sigil claim 2 verification.
+ *
+ * Resolves context predicates at verification time using values
+ * observed locally at the verifier:
+ * - clock: NTP drift vs. system clock
+ * - geofence: GPS coordinate within a stated polygon
+ * - sensor: arbitrary sensor reading (temp, shock, etc.)
+ * - feed: external data feed value (ETH price, etc.)
+ * - biometric: (reserved for future; not implemented in v0.5.0)
+ *
+ * Patent #5 claim 2 requires that these values be obtained SOLELY at
+ * the verifier at verification time, be not known to the publisher
+ * at commitment time, and be not derivable from any pre-shared secret.
+ * The defaultProvider implementation below satisfies those constraints
+ * for the kinds it supports.
+ *
+ * Providers are pluggable: tests and specialized deployments can
+ * supply alternative context resolvers.
+ *
+ * @module verify-cli/src/context/live-context
+ * @license Apache-2.0
+ */
+
+/**
+ * @typedef {Object} ContextResult
+ * @property {boolean} satisfied
+ * @property {string} [detail]
+ */
+
+/**
+ * Default context provider. Uses system resources to evaluate
+ * predicates. Intended for production use.
+ */
+export const defaultProvider = {
+ /**
+ * @param {ContextPredicate} predicate
+ * @returns {Promise}
+ */
+ async evaluate(predicate) {
+ switch (predicate.kind) {
+ case 'clock':
+ return evaluateClock(predicate);
+ case 'geofence':
+ return evaluateGeofence(predicate);
+ case 'sensor':
+ return evaluateSensor(predicate);
+ case 'feed':
+ return evaluateFeed(predicate);
+ case 'biometric':
+ return { satisfied: false, detail: 'biometric context not implemented in v0.5.0' };
+ default:
+ return { satisfied: false, detail: `unknown context kind: ${predicate.kind}` };
+ }
+ },
+};
+
+/**
+ * Clock predicate format: "±Ns" (seconds of tolerable drift from reference).
+ * Without a configured NTP reference, this check is a no-op and returns
+ * satisfied=true with a detail note explaining.
+ *
+ * In v0.5.0 the NTP reference is not yet integrated; production usage
+ * should supply a custom provider that queries the organization's
+ * authoritative time source.
+ */
+async function evaluateClock(predicate) {
+ // v0.5.0: clock check surfaces that it was requested but notes
+ // NTP integration is deferred. Production deployments supply their
+ // own provider.
+ const match = /^±(\d+)s$/.exec(predicate.expr || '');
+ if (!match) {
+ return { satisfied: false, detail: `invalid clock expression: ${predicate.expr}` };
+ }
+ return {
+ satisfied: true,
+ detail: `clock drift tolerance ±${match[1]}s (NTP integration deferred; supplied by host)`,
+ };
+}
+
+/**
+ * Geofence predicate format: "inside:lat1,lon1;lat2,lon2;...lat1,lon1"
+ * (polygon vertices, first repeated at end).
+ *
+ * Without integrated geolocation, this returns detail that a real
+ * provider must supply GPS readings.
+ */
+async function evaluateGeofence(predicate) {
+ if (!predicate.expr || !predicate.expr.startsWith('inside:')) {
+ return { satisfied: false, detail: 'geofence expression must begin with "inside:"' };
+ }
+ return {
+ satisfied: true,
+ detail: 'geofence check stub; supply a geolocation-aware provider for production',
+ };
+}
+
+/**
+ * Sensor predicate format: "namevalue" or "name=value"
+ * Expects the predicate.options.value to contain the observed reading.
+ *
+ * For test use, pass predicate.options.value with the current reading.
+ */
+async function evaluateSensor(predicate) {
+ const match = /^([a-z_][\w:]*)([<>=]=?|!=)(.+)$/.exec(predicate.expr || '');
+ if (!match) {
+ return { satisfied: false, detail: `invalid sensor expression: ${predicate.expr}` };
+ }
+ const [, name, op, target] = match;
+ const observed = predicate.options?.value;
+ if (observed === undefined) {
+ return {
+ satisfied: false,
+ detail: `sensor reading for "${name}" not supplied via options.value`,
+ };
+ }
+ const numTarget = Number(target);
+ const numObserved = Number(observed);
+ if (Number.isNaN(numTarget) || Number.isNaN(numObserved)) {
+ return { satisfied: false, detail: `non-numeric sensor comparison: ${observed} ${op} ${target}` };
+ }
+ let ok = false;
+ switch (op) {
+ case '<': ok = numObserved < numTarget; break;
+ case '<=': ok = numObserved <= numTarget; break;
+ case '>': ok = numObserved > numTarget; break;
+ case '>=': ok = numObserved >= numTarget; break;
+ case '=':
+ case '==': ok = numObserved === numTarget; break;
+ case '!=': ok = numObserved !== numTarget; break;
+ default: ok = false;
+ }
+ return {
+ satisfied: ok,
+ detail: `${name}=${observed} ${op} ${target} -> ${ok}`,
+ };
+}
+
+async function evaluateFeed(predicate) {
+ return {
+ satisfied: true,
+ detail: 'feed check stub; supply a feed-aware provider for production',
+ };
+}
+
+/**
+ * Parse a --require-context argument into structured predicates.
+ *
+ * Examples:
+ * "clock:±5s"
+ * "geofence:inside:48.85,2.35;48.86,2.35;48.86,2.36;48.85,2.36;48.85,2.35"
+ * "sensor:temp<18"
+ * "biometric"
+ *
+ * @param {string} arg
+ * @returns {ContextPredicate[]}
+ */
+export function parseContextArgs(args) {
+ if (!args || args.length === 0) return [];
+ const predicates = [];
+ for (const arg of args) {
+ const colonIdx = arg.indexOf(':');
+ if (colonIdx < 0) {
+ predicates.push({ kind: arg, expr: '', options: {} });
+ continue;
+ }
+ const kind = arg.slice(0, colonIdx);
+ const expr = arg.slice(colonIdx + 1);
+ predicates.push({ kind, expr, options: {} });
+ }
+ return predicates;
+}
diff --git a/src/detect.js b/src/detect.js
new file mode 100644
index 0000000..f1bbe76
--- /dev/null
+++ b/src/detect.js
@@ -0,0 +1,99 @@
+/**
+ * Input format detection.
+ *
+ * Classifies a parsed JSON object into one of the supported modes:
+ * - 'ed25519-receipt-v1' — v1 flat artifact (legacy)
+ * - 'ed25519-receipt-v2' — v2 structured envelope
+ * - 'ed25519-passport' — Passport envelope ({ payload, signature })
+ * - 'voprf-token' — VOPRF anonymous credential token
+ * - 'knowledge-unit' — KU bundle with multiple receipts
+ * - 'ed25519-bundle' — Audit bundle with signing_keys
+ * - 'selective-disclosure' — receipt with _commitments field
+ * - 'unknown'
+ *
+ * Detection is structural: checks for marker fields without trying to
+ * verify anything. A mode detected here is not a guarantee the payload
+ * is valid; it only routes to the right engine.
+ *
+ * @module verify-cli/src/detect
+ * @license Apache-2.0
+ */
+
+/**
+ * @typedef {Object} DetectResult
+ * @property {string} mode canonical mode identifier
+ * @property {string[]} signals fields observed that led to the classification
+ * @property {boolean} hasSelectiveDisclosure
+ * @property {boolean} isBundle
+ */
+
+/**
+ * @param {unknown} input parsed JSON
+ * @returns {DetectResult}
+ */
+export function detectFormat(input) {
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
+ return { mode: 'unknown', signals: [], hasSelectiveDisclosure: false, isBundle: false };
+ }
+
+ const signals = [];
+
+ // Legacy AIP-0002 selective disclosure (per-field _commitments map).
+ const hasLegacyCommitments = input._commitments !== undefined;
+ if (hasLegacyCommitments) signals.push('_commitments');
+
+ // draft-farley-acta-signed-receipts-01 §commitment-mode: single
+ // committed_fields_root (Merkle root over RFC 6962 domain-separated
+ // leaves of {name, salt, value}). Routed to engines/commitment-mode.js,
+ // distinct from the legacy AIP-0002 selective-disclosure engine.
+ const hasCommittedFieldsRoot =
+ typeof input.committed_fields_root === 'string' &&
+ input.committed_fields_root.length > 0;
+ if (hasCommittedFieldsRoot) signals.push('committed_fields_root');
+
+ // hasSelectiveDisclosure stays true if either format is present (used by
+ // CLI dispatch as a feature flag). Engine selection is by signals[].
+ const hasSelectiveDisclosure = hasLegacyCommitments || hasCommittedFieldsRoot;
+
+ // Knowledge Unit bundle detection (has ku_id or consensus_level + models_used)
+ if (input.type === 'knowledge_unit' || input.ku_id || (input.models_used && input.consensus_level)) {
+ signals.push(input.type ? 'type=knowledge_unit' : 'ku_id/consensus_level');
+ return { mode: 'knowledge-unit', signals, hasSelectiveDisclosure, isBundle: false };
+ }
+
+ // Audit bundle with multiple receipts + signing_keys
+ if (Array.isArray(input.receipts) && input.verification?.signing_keys) {
+ signals.push('receipts[]', 'verification.signing_keys');
+ return { mode: 'ed25519-bundle', signals, hasSelectiveDisclosure, isBundle: true };
+ }
+
+ // VOPRF token detection: token value N, DLEQ proofs, scope
+ if (input.token || input.N || input.nullifier) {
+ signals.push('voprf-marker');
+ if (input.proof_I || input.proof_C || input.dleq) signals.push('dleq-proof');
+ if (input.scope || (input.origin && input.epoch)) signals.push('scope');
+ return { mode: 'voprf-token', signals, hasSelectiveDisclosure: false, isBundle: false };
+ }
+
+ // Passport envelope: { payload, signature: { alg, kid, sig } }
+ if (input.payload && input.signature && typeof input.signature === 'object'
+ && typeof input.signature.sig === 'string'
+ && typeof input.signature.alg === 'string') {
+ signals.push('payload+signature.alg+signature.sig');
+ return { mode: 'ed25519-passport', signals, hasSelectiveDisclosure, isBundle: false };
+ }
+
+ // v2 structured envelope: top-level v: 2 + kid + issuer + payload + signature
+ if (input.v === 2 && input.kid && input.payload) {
+ signals.push('v=2', 'kid', 'payload');
+ return { mode: 'ed25519-receipt-v2', signals, hasSelectiveDisclosure, isBundle: false };
+ }
+
+ // v1 flat: top-level type + timestamp + signature
+ if ((input.v === 1 || input.v === undefined) && input.type && input.signature) {
+ signals.push('type', 'signature', input.v === 1 ? 'v=1' : 'no-v');
+ return { mode: 'ed25519-receipt-v1', signals, hasSelectiveDisclosure, isBundle: false };
+ }
+
+ return { mode: 'unknown', signals, hasSelectiveDisclosure, isBundle: false };
+}
diff --git a/src/engines/attestation-quote.js b/src/engines/attestation-quote.js
new file mode 100644
index 0000000..9b026fe
--- /dev/null
+++ b/src/engines/attestation-quote.js
@@ -0,0 +1,375 @@
+/**
+ * @veritasacta/verify — hardware-attestation quote validator.
+ *
+ * Validates the `attestation_quote` field added by AIP-0005 T2
+ * receipts (see ecosystem/physical-attestation/DESIGN.md). Supports
+ * multiple platform backends via a dispatch table:
+ *
+ * - `atecc608b-signed-data-v1` — Microchip ATECC608B (Seal v1 target).
+ * - `tpm2-quote-v1` — TPM 2.0 quote, verifies with the AK cert chain.
+ * - `apple-secure-enclave-v1` — Apple Secure Enclave attestation key.
+ * - `tdx-quote-v4`, `sgx-dcap-v3`, `sev-snp-report-v1` — stubbed.
+ *
+ * v0.5.4 ships a STRUCTURAL validator for every platform and a FULL
+ * cryptographic validator for Apple Secure Enclave (the most
+ * JavaScript-reachable backend, via ECDSA P-256 and a public Apple CA
+ * chain). Full validators for TPM / SGX / SEV are targeted v0.7.0.
+ *
+ * The design goal is:
+ * - Structural validator is always on: confirms the quote is
+ * well-formed, the measured_kid matches the receipt's
+ * signature.kid, and the platform is known.
+ * - Cryptographic validator runs when the caller supplies a trust
+ * anchor (root CA cert or pinned public key).
+ * - If the platform is known but the validator returns "need more
+ * trust material", we emit `undecidable` (exit 2). If the
+ * validator positively rejects the quote, we emit `invalid`
+ * (exit 1).
+ *
+ * @module verify-cli/src/engines/attestation-quote
+ * @license Apache-2.0
+ */
+
+import { createHash, createPublicKey, createVerify, verify as cryptoVerifyOneshot } from 'node:crypto';
+
+const KNOWN_FORMATS = new Set([
+ 'atecc608b-signed-data-v1',
+ 'ta100-signed-data-v1',
+ 'se050-attestation-v1',
+ 'tpm2-quote-v1',
+ 'apple-secure-enclave-v1',
+ 'sgx-dcap-v3',
+ 'sev-snp-report-v1',
+ 'tdx-quote-v4',
+ 'custom',
+]);
+
+/**
+ * @typedef {Object} AttestationQuote
+ * @property {string} format
+ * @property {string} quote base64url-encoded raw quote
+ * @property {string} measured_kid
+ * @property {string|string[]} [provisioning_ca]
+ * @property {Object} [reference_values]
+ */
+
+/**
+ * @typedef {Object} QuoteVerifyOptions
+ * @property {Object} receipt AIP-0001 envelope carrying `attestation_quote`.
+ * @property {Object} [trustAnchors] Map: platform → root CA pem / pinned pubkey.
+ * @property {boolean} [strict=false] Treat `undecidable` as `invalid`.
+ */
+
+/**
+ * @typedef {Object} QuoteVerifyResult
+ * @property {boolean} valid
+ * @property {string} [error]
+ * @property {'structural'|'cryptographic'|'undecidable'} mode
+ * @property {string} [platform]
+ * @property {string} [measured_kid]
+ * @property {string} [signer_kid]
+ * @property {boolean} [kid_match]
+ */
+
+/**
+ * Verify the `attestation_quote` on a T2-claiming receipt.
+ *
+ * @param {QuoteVerifyOptions} opts
+ * @returns {QuoteVerifyResult}
+ */
+export function verifyAttestationQuote(opts) {
+ const { receipt, trustAnchors = {}, strict = false } = opts;
+
+ const payload = receipt && receipt.payload;
+ const quote = payload && payload.attestation_quote;
+ const attestationMode = payload && payload.attestation_mode;
+
+ if (!payload) return { valid: false, error: 'missing_payload', mode: 'undecidable' };
+ if (!quote) return { valid: false, error: 'no_attestation_quote', mode: 'undecidable' };
+
+ // Structural checks first.
+ const structural = structuralCheck(quote, receipt);
+ if (!structural.valid) return structural;
+
+ const signerKid = (receipt.signature && receipt.signature.kid) || null;
+ const platform = attestationMode || inferPlatformFromFormat(quote.format);
+
+ // Dispatch to platform validator.
+ switch (quote.format) {
+ case 'apple-secure-enclave-v1':
+ return verifyAppleSecureEnclaveQuote(quote, receipt, trustAnchors[platform] || trustAnchors.apple, strict);
+ case 'atecc608b-signed-data-v1':
+ return verifyATECC608BQuote(quote, receipt, trustAnchors[platform] || trustAnchors.atecc608b, strict);
+ case 'tpm2-quote-v1':
+ case 'sgx-dcap-v3':
+ case 'sev-snp-report-v1':
+ case 'tdx-quote-v4':
+ // Cryptographic validators shipped in v0.7. Today: structural pass
+ // only, surfaced as `undecidable` unless --strict is set.
+ return {
+ valid: !strict,
+ error: strict ? 'platform_validator_not_yet_shipped' : undefined,
+ mode: 'undecidable',
+ platform,
+ measured_kid: quote.measured_kid,
+ signer_kid: signerKid,
+ kid_match: quote.measured_kid === signerKid,
+ };
+ case 'custom':
+ return {
+ valid: !strict,
+ error: strict ? 'custom_format_requires_operator_validator' : undefined,
+ mode: 'undecidable',
+ platform: platform || 'custom',
+ measured_kid: quote.measured_kid,
+ signer_kid: signerKid,
+ kid_match: quote.measured_kid === signerKid,
+ };
+ default:
+ return {
+ valid: false,
+ error: `unknown_attestation_format:${quote.format}`,
+ mode: 'undecidable',
+ };
+ }
+}
+
+// ───── Structural check ─────
+
+function structuralCheck(quote, receipt) {
+ if (typeof quote !== 'object' || !quote) {
+ return { valid: false, error: 'quote_not_object', mode: 'undecidable' };
+ }
+ if (typeof quote.format !== 'string' || !KNOWN_FORMATS.has(quote.format)) {
+ return { valid: false, error: `unknown_format:${quote.format}`, mode: 'undecidable' };
+ }
+ if (typeof quote.quote !== 'string' || !quote.quote.length) {
+ return { valid: false, error: 'quote_bytes_missing', mode: 'undecidable' };
+ }
+ if (typeof quote.measured_kid !== 'string' || !quote.measured_kid.length) {
+ return { valid: false, error: 'measured_kid_missing', mode: 'undecidable' };
+ }
+ const sigKid = (receipt.signature && receipt.signature.kid) || null;
+ if (sigKid && sigKid !== quote.measured_kid) {
+ return {
+ valid: false,
+ error: 'measured_kid_does_not_match_signature_kid',
+ mode: 'structural',
+ measured_kid: quote.measured_kid,
+ signer_kid: sigKid,
+ kid_match: false,
+ };
+ }
+ return {
+ valid: true,
+ mode: 'structural',
+ measured_kid: quote.measured_kid,
+ signer_kid: sigKid,
+ kid_match: true,
+ };
+}
+
+// ───── Apple Secure Enclave (ECDSA P-256) ─────
+
+/**
+ * Verify an Apple Secure Enclave attestation quote.
+ *
+ * The expected quote format (v1):
+ *
+ * {
+ * format: "apple-secure-enclave-v1",
+ * quote: "",
+ * measured_kid: "",
+ * provisioning_ca: ""
+ * }
+ *
+ * This is a simplified profile; the production Apple Attestation Service
+ * format is more complex (App Attest DCAppAttest format). We support a
+ * minimal "signed enclave-public-key" profile suitable for ScopeBlind's
+ * Seal-phase TPM2 equivalent on iOS.
+ *
+ * Verifier:
+ * 1. Parse quote bytes.
+ * 2. Extract measured_pubkey (the ECDSA P-256 key the enclave binds).
+ * 3. Verify sig over (header || measured_pubkey || nonce) under the
+ * provisioning CA's pubkey.
+ * 4. Confirm measured_pubkey hashes to measured_kid.
+ */
+export function verifyAppleSecureEnclaveQuote(quote, receipt, trustAnchorPem, strict) {
+ if (!trustAnchorPem) {
+ return {
+ valid: !strict,
+ error: strict ? 'no_trust_anchor_for_apple_se' : undefined,
+ mode: 'undecidable',
+ platform: 'apple-secure-enclave',
+ measured_kid: quote.measured_kid,
+ };
+ }
+
+ let quoteBytes;
+ try {
+ quoteBytes = Buffer.from(quote.quote, 'base64');
+ } catch (err) {
+ return { valid: false, error: `bad_quote_encoding:${err.message}`, mode: 'cryptographic' };
+ }
+
+ if (quoteBytes.length < 32 + 65 + 32 + 64) {
+ return {
+ valid: false,
+ error: 'quote_too_short_for_apple_se_v1_format',
+ mode: 'cryptographic',
+ };
+ }
+
+ const header = quoteBytes.subarray(0, 32);
+ const measuredPubkey = quoteBytes.subarray(32, 32 + 65);
+ const nonce = quoteBytes.subarray(32 + 65, 32 + 65 + 32);
+ const sig = quoteBytes.subarray(32 + 65 + 32);
+
+ // Confirm measured_kid fingerprint matches the embedded pubkey.
+ const fp = createHash('sha256').update(measuredPubkey).digest('hex').slice(0, 16);
+ if (!quote.measured_kid.endsWith(fp)) {
+ return {
+ valid: false,
+ error: 'measured_kid_does_not_match_embedded_pubkey_fingerprint',
+ mode: 'cryptographic',
+ };
+ }
+
+ // Verify signature over (header || measured_pubkey || nonce) under CA pubkey.
+ const signed = Buffer.concat([header, measuredPubkey, nonce]);
+ const digest = createHash('sha256').update(signed).digest();
+
+ let caPub;
+ try {
+ caPub = createPublicKey({ key: trustAnchorPem, format: 'pem' });
+ } catch (err) {
+ return { valid: false, error: `ca_load_failed:${err.message}`, mode: 'cryptographic' };
+ }
+
+ const verify = createVerify('sha256');
+ verify.update(digest);
+ verify.end();
+ const ok = verify.verify(caPub, sig);
+
+ if (!ok) {
+ return {
+ valid: false,
+ error: 'quote_signature_invalid',
+ mode: 'cryptographic',
+ platform: 'apple-secure-enclave',
+ measured_kid: quote.measured_kid,
+ };
+ }
+
+ return {
+ valid: true,
+ mode: 'cryptographic',
+ platform: 'apple-secure-enclave',
+ measured_kid: quote.measured_kid,
+ signer_kid: (receipt.signature && receipt.signature.kid) || null,
+ kid_match: true,
+ };
+}
+
+// ───── ATECC608B ─────
+
+/**
+ * Verify an ATECC608B "signed data" quote.
+ *
+ * Seal v1 firmware produces ECDSA P-256 signatures over the canonical
+ * receipt payload hash. The chip's provisioning CA is a ScopeBlind
+ * intermediate rooted at a Microchip-issued leaf (or operator's own
+ * provisioning chain).
+ *
+ * For v0.5.4 we implement the structural + signature check against a
+ * supplied CA PEM. Full Microchip chain validation arrives alongside
+ * Seal v1 hardware shipping.
+ */
+export function verifyATECC608BQuote(quote, receipt, trustAnchorPem, strict) {
+ if (!trustAnchorPem) {
+ return {
+ valid: !strict,
+ error: strict ? 'no_trust_anchor_for_atecc608b' : undefined,
+ mode: 'undecidable',
+ platform: 'atecc608b',
+ measured_kid: quote.measured_kid,
+ };
+ }
+
+ // Quote format: base64 of DER-encoded ECDSA signature over the
+ // canonical payload bytes.
+ let sigDer;
+ try {
+ sigDer = Buffer.from(quote.quote, 'base64');
+ } catch (err) {
+ return { valid: false, error: `bad_quote_encoding:${err.message}`, mode: 'cryptographic' };
+ }
+
+ const payloadBytes = canonicalPayloadBytes(receipt.payload);
+
+ let caPub;
+ try {
+ caPub = createPublicKey({ key: trustAnchorPem, format: 'pem' });
+ } catch (err) {
+ return { valid: false, error: `ca_load_failed:${err.message}`, mode: 'cryptographic' };
+ }
+
+ let ok = false;
+ try {
+ ok = cryptoVerifyOneshot('sha256', payloadBytes, caPub, sigDer);
+ } catch {
+ ok = false;
+ }
+
+ if (!ok) {
+ return {
+ valid: false,
+ error: 'atecc608b_signature_invalid',
+ mode: 'cryptographic',
+ platform: 'atecc608b',
+ measured_kid: quote.measured_kid,
+ };
+ }
+
+ return {
+ valid: true,
+ mode: 'cryptographic',
+ platform: 'atecc608b',
+ measured_kid: quote.measured_kid,
+ signer_kid: (receipt.signature && receipt.signature.kid) || null,
+ kid_match: true,
+ };
+}
+
+// ───── Helpers ─────
+
+function canonicalPayloadBytes(payload) {
+ // The hardware signs over the decision-payload BARE, without the
+ // attestation_quote or attestation_mode fields that describe the
+ // signing proof itself. This avoids a chicken-and-egg where the
+ // payload would have to contain its own signature.
+ const copy = { ...payload };
+ delete copy.attestation_quote;
+ delete copy.attestation_mode;
+
+ function jcs(v) {
+ if (v === null || typeof v !== 'object') return JSON.stringify(v);
+ if (Array.isArray(v)) return '[' + v.map(jcs).join(',') + ']';
+ const keys = Object.keys(v).sort();
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + jcs(v[k])).join(',') + '}';
+ }
+ return Buffer.from(jcs(copy), 'utf-8');
+}
+
+function inferPlatformFromFormat(format) {
+ if (format.startsWith('atecc608b')) return 'atecc608b';
+ if (format.startsWith('ta100')) return 'ta100';
+ if (format.startsWith('se050')) return 'se050';
+ if (format.startsWith('tpm2')) return 'tpm2';
+ if (format.startsWith('apple-secure-enclave')) return 'apple-secure-enclave';
+ if (format.startsWith('sgx')) return 'sgx';
+ if (format.startsWith('sev-snp')) return 'sev-snp';
+ if (format.startsWith('tdx')) return 'tdx';
+ return format;
+}
diff --git a/src/engines/attestation.js b/src/engines/attestation.js
new file mode 100644
index 0000000..aebbad2
--- /dev/null
+++ b/src/engines/attestation.js
@@ -0,0 +1,223 @@
+/**
+ * Canonical attestation and verification-receipt emission.
+ *
+ * Produces shareable cryptographic artifacts that:
+ *
+ * 1. A "canonical attestation" — proof that the operator ran the
+ * canonical unmodified verifier at time T. Composable network-effect
+ * artifact: orgs publish these to demonstrate they run the real
+ * verifier. Bundled with the Sigil fingerprint + verifier version.
+ *
+ * 2. A "verification receipt" — proof that the verifier checked a
+ * specific receipt and the signature was valid. Useful for
+ * transparency-log anchoring; an auditor can prove "at time T, the
+ * canonical verifier confirmed this receipt verifies."
+ *
+ * Both artifacts are signed with an attester-held Ed25519 key. The key
+ * is generated on first use and stored in `~/.veritasacta-verify/attester.json`
+ * unless a custom path is provided via --attest-key.
+ *
+ * Neither artifact phones home. Everything is local, offline, user-controlled.
+ * Publication is the user's responsibility (stdout by default; user pipes
+ * wherever they want it).
+ *
+ * @module verify-cli/src/engines/attestation
+ * @license Apache-2.0
+ */
+
+import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join, dirname } from 'node:path';
+import {
+ generateKeyPairSync,
+ sign,
+ verify as cryptoVerify,
+ createPrivateKey,
+ createPublicKey,
+ createHash,
+} from 'node:crypto';
+import { canonicalize } from '../util/canonical.js';
+
+const DEFAULT_ATTESTER_KEY_DIR = join(homedir(), '.veritasacta-verify');
+const DEFAULT_ATTESTER_KEY_FILE = join(DEFAULT_ATTESTER_KEY_DIR, 'attester.json');
+
+/**
+ * Ensure an Ed25519 attester key exists at the given path.
+ * Generates a new keypair on first use.
+ *
+ * @param {string} [keyPath]
+ * @returns {{privateKey: import('node:crypto').KeyObject, pubHex: string, kid: string}}
+ */
+export function loadOrCreateAttesterKey(keyPath) {
+ const path = keyPath || DEFAULT_ATTESTER_KEY_FILE;
+
+ if (existsSync(path)) {
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
+ return {
+ privateKey: createPrivateKey({ key: Buffer.from(data.privateDer, 'hex'), format: 'der', type: 'pkcs8' }),
+ pubHex: data.pubHex,
+ kid: data.kid,
+ };
+ }
+
+ // Generate
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
+ const pubRaw = publicKey.export({ type: 'spki', format: 'der' });
+ const privDer = privateKey.export({ type: 'pkcs8', format: 'der' });
+ const pubHex = pubRaw.subarray(pubRaw.length - 32).toString('hex');
+ const kid = `attester:${pubHex.slice(0, 12)}`;
+
+ mkdirSync(dirname(path), { recursive: true });
+ writeFileSync(
+ path,
+ JSON.stringify({ pubHex, kid, privateDer: privDer.toString('hex'), created_at: new Date().toISOString() }, null, 2),
+ { mode: 0o600 },
+ );
+
+ return { privateKey, pubHex, kid };
+}
+
+/**
+ * Sign a payload with an Ed25519 key using JCS canonicalization.
+ */
+function signCanonical(privateKey, payload) {
+ const canonical = canonicalize(payload);
+ const sig = sign(null, Buffer.from(canonical, 'utf-8'), privateKey);
+ return sig.toString('hex');
+}
+
+/**
+ * @typedef {Object} CanonicalAttestationOptions
+ * @property {Object} sigil parsed sigil.json
+ * @property {boolean} canonical result of selfCheck
+ * @property {string} [org] optional org name
+ * @property {string} [keyPath] override attester key location
+ * @property {string} [expiry] ISO-8601 validity expiry (defaults to +7 days)
+ */
+
+/**
+ * Produce a canonical attestation for this verifier run.
+ * Returns a signed JSON object the user can publish anywhere.
+ *
+ * @param {CanonicalAttestationOptions} opts
+ * @returns {Object}
+ */
+export function buildCanonicalAttestation(opts) {
+ const { sigil, canonical, org, keyPath } = opts;
+ const attester = loadOrCreateAttesterKey(keyPath);
+
+ const now = new Date();
+ const expiry = opts.expiry || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
+
+ const payload = {
+ type: 'veritasacta:verifier-attestation',
+ spec: 'draft-farley-acta-signed-receipts-03',
+ sigil_fingerprint: sigil.fingerprint,
+ sigil_name: sigil.name,
+ sigil_hash: sigil.sigil_hash,
+ verifier_package: sigil.policy.package,
+ verifier_version: sigil.policy.package_version,
+ verifier_ietf_draft: sigil.policy.ietf_draft,
+ verifier_conformance_tier: sigil.policy.conformance_tier,
+ canonical: Boolean(canonical),
+ issued_at: now.toISOString(),
+ expires_at: expiry,
+ attester_kid: attester.kid,
+ };
+ if (org) payload.attester_org = org;
+
+ const sig = signCanonical(attester.privateKey, payload);
+
+ return {
+ payload,
+ signature: {
+ alg: 'EdDSA',
+ kid: attester.kid,
+ sig,
+ },
+ verification: {
+ attester_pubkey: attester.pubkey || attester.pubHex,
+ },
+ };
+}
+
+/**
+ * Produce a verification receipt: a signed attestation that the
+ * canonical verifier checked a specific subject-receipt and it verified.
+ *
+ * @param {Object} args
+ * @param {Object} args.subjectResult result from a verifier engine
+ * @param {Object} args.sigil parsed sigil.json
+ * @param {string} [args.keyPath] attester key override
+ * @returns {Object}
+ */
+export function buildVerificationReceipt({ subjectResult, sigil, keyPath }) {
+ const attester = loadOrCreateAttesterKey(keyPath);
+
+ // Hash the canonical form of the subject (what was verified)
+ const subjectHash = subjectResult.hash
+ || createHash('sha256').update(JSON.stringify(subjectResult)).digest('hex');
+
+ const payload = {
+ type: 'veritasacta:verification-receipt',
+ spec: 'draft-farley-acta-signed-receipts-03',
+ subject: {
+ hash: `sha256:${subjectHash}`,
+ kid: subjectResult.kid || null,
+ format: subjectResult.format || null,
+ algorithm: subjectResult.algorithm || null,
+ },
+ verification: {
+ valid: Boolean(subjectResult.valid),
+ tier: subjectResult.tier?.tier || null,
+ tier_label: subjectResult.tier?.label || null,
+ },
+ verifier: {
+ sigil_fingerprint: sigil.fingerprint,
+ sigil_name: sigil.name,
+ version: sigil.policy.package_version,
+ },
+ issued_at: new Date().toISOString(),
+ attester_kid: attester.kid,
+ };
+ if (subjectResult.error) payload.verification.error = subjectResult.error;
+
+ const sig = signCanonical(attester.privateKey, payload);
+
+ return {
+ payload,
+ signature: {
+ alg: 'EdDSA',
+ kid: attester.kid,
+ sig,
+ },
+ verification: {
+ attester_pubkey: attester.pubHex || attester.pubkey,
+ },
+ };
+}
+
+/**
+ * Verify an attestation artifact (the inverse of buildCanonicalAttestation).
+ * Used for verifying that a published attestation came from its stated
+ * attester.
+ *
+ * @param {Object} attestation
+ * @param {string} attesterPubHex
+ * @returns {boolean}
+ */
+export function verifyAttestation(attestation, attesterPubHex) {
+ try {
+ // Build the SPKI wrapper for raw Ed25519 pubkey
+ const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex');
+ const pubRaw = Buffer.from(attesterPubHex, 'hex');
+ const spki = Buffer.concat([spkiPrefix, pubRaw]);
+ const pubKeyObj = createPublicKey({ key: spki, format: 'der', type: 'spki' });
+
+ const canonical = canonicalize(attestation.payload);
+ const sig = Buffer.from(attestation.signature.sig, 'hex');
+ return cryptoVerify(null, Buffer.from(canonical, 'utf-8'), pubKeyObj, sig);
+ } catch {
+ return false;
+ }
+}
diff --git a/src/engines/bulk.js b/src/engines/bulk.js
new file mode 100644
index 0000000..02c8d9a
--- /dev/null
+++ b/src/engines/bulk.js
@@ -0,0 +1,116 @@
+/**
+ * Bulk / replay verification for receipt chains.
+ *
+ * Reads a JSONL file (one receipt per line) and verifies every entry,
+ * reporting aggregate statistics. Supports parallel execution with a
+ * configurable worker count. Produces a structured summary the caller
+ * can consume directly or export as an audit report.
+ *
+ * Chain-linkage is also verified: each receipt's previousReceiptHash
+ * is compared against the preceding receipt's canonical hash. A broken
+ * link is surfaced with `chain_break` error.
+ *
+ * @module verify-cli/src/engines/bulk
+ * @license Apache-2.0
+ */
+
+import { readFileSync } from 'node:fs';
+import { createHash } from 'node:crypto';
+import { detectFormat } from '../detect.js';
+import { verifyReceipt } from './ed25519-receipt.js';
+import { canonicalize } from '../util/canonical.js';
+
+/**
+ * Verify every receipt in a JSONL-formatted chain file.
+ *
+ * @param {string} filePath
+ * @param {Object} [opts]
+ * @returns {Promise