From ea1401f5465495455a94d5d15b42c3ded6114b07 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sun, 19 Jul 2026 21:50:38 +0100 Subject: [PATCH 1/2] web: the roster is the only source of witness keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both surfaces verified statements against issuer keys carried in the same untrusted payload — any server or node could mint a key and verify itself. Now the admissions key is pinned in the app bundle at build time; the roster document re-checks under that pinned key AND its bytes must hash to the rid its attestation signed; entry keys reach the pages only from a roster that passed both. The node console judges a build statement solely by the roster key for the node's claimed did (unlisted node: withheld, fail closed), ignoring any issuer_key the node serves. READMEs stop naming a CLI command that does not exist. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 13 --- .claude/skills/burndown/SKILL.md | 2 +- .claude/skills/cycle/SKILL.md | 2 +- .claude/skills/review/SKILL.md | 2 +- .gitignore | 1 + .recurve/ADJUDICATE.md | 77 +- .recurve/PRD.md | 679 ++++++------------ .recurve/README.md | 10 +- .recurve/REVIEW.md | 2 +- .recurve/RUN-AUTO.md | 31 +- .recurve/RUN.md | 93 ++- .recurve/TROUBLESHOOTING.md | 4 +- .recurve/claims/network/GAPS.md | 554 -------------- .recurve/claims/network/bin/.gitignore | 6 - .../claims/network/cycles/BOOT-1/outcome.md | 80 --- .../claims/network/cycles/BOOT-2/outcome.md | 93 --- .../claims/network/cycles/BOOT-3/outcome.md | 116 --- .../claims/network/cycles/WIT-N1/outcome.md | 113 --- .../claims/network/cycles/WIT-N2/outcome.md | 108 --- .../claims/network/cycles/WIT-N3/outcome.md | 110 --- .../claims/network/cycles/WIT-N4/outcome.md | 119 --- .../claims/network/cycles/WIT-N5/outcome.md | 114 --- .recurve/claims/network/gaps.draft.yaml | 434 ----------- .recurve/claims/network/gaps.yaml | 366 ---------- .recurve/claims/network/harness/.gitignore | 1 - .../harness/compose/docker-compose.yml | 62 -- .recurve/claims/network/harness/down.sh | 12 - .../harness/ensure-build-attestation.sh | 74 -- .../claims/network/harness/ensure-image.sh | 43 -- .recurve/claims/network/harness/env.sh | 75 -- .recurve/claims/network/harness/kill-node.sh | 32 - .recurve/claims/network/harness/ksn_emit.py | 46 -- .recurve/claims/network/harness/ksn_oracle.py | 67 -- .recurve/claims/network/harness/rebuild.sh | 33 - .recurve/claims/network/harness/up.sh | 49 -- .recurve/claims/network/harness/versions.lock | 19 - .recurve/claims/network/probes/_contract.sh | 18 - .recurve/claims/network/probes/boot-1.sh | 105 --- .../network/probes/boot-1.trap/README.md | 12 - .../cloned-identity/health/wit1.json | 1 - .../cloned-identity/health/wit2.json | 1 - .../cloned-identity/health/wit3.json | 1 - .recurve/claims/network/probes/boot-2.sh | 90 --- .../network/probes/boot-2.trap/README.md | 11 - .../node-in-default-tree/default-tree.txt | 9 - .recurve/claims/network/probes/boot-3.sh | 97 --- .../network/probes/boot-3.trap/README.md | 18 - .../broken-sibling/cannot-measure.sh | 12 - .../network/probes/fixtures/icp-event.json | 1 - .../network/probes/fixtures/keri-icp.json | 1 - .../probes/fixtures/receipt-bundle.json | 1 - .recurve/claims/network/probes/wit-n1.sh | 130 ---- .../network/probes/wit-n1.trap/README.md | 18 - .../probes/wit-n1.trap/occupied-port/up.code | 1 - .../probes/wit-n1.trap/occupied-port/up.out | 1 - .recurve/claims/network/probes/wit-n2.sh | 162 ----- .../network/probes/wit-n2.trap/README.md | 18 - .../wit-n2.trap/forged-signature/bundle.json | 13 - .recurve/claims/network/probes/wit-n3.sh | 140 ---- .../network/probes/wit-n3.trap/README.md | 19 - .../probes/wit-n3.trap/stale-ksn/ksn.json | 1 - .../probes/wit-n3.trap/stale-ksn/last_seen | 1 - .recurve/claims/network/probes/wit-n4.sh | 158 ---- .../network/probes/wit-n4.trap/README.md | 24 - .../wrong-digest/forged.auths.json | 27 - .recurve/claims/network/probes/wit-n5.sh | 170 ----- .../network/probes/wit-n5.trap/README.md | 22 - .../wit-n5.trap/jargon-leak/happy-path.out | 7 - .recurve/claims/web/gaps.draft.yaml | 189 ----- .recurve/recurve.toml | 50 +- .recurve/state/records.jsonl | 2 + .recurve/state/reports/web-burndown.md | 235 ++++++ .recurve/workflows/burndown-parallel.sh | 12 +- .recurve/workflows/burndown.js | 68 +- .recurve/workflows/burndown.sh | 91 ++- README.md | 61 ++ docs/ARCHITECTURE.md | 148 ++++ web/README.md | 75 ++ web/app/directory/page.tsx | 65 +- web/app/globals.css | 79 ++ web/app/layout.tsx | 16 +- web/app/lib/admissions-key.json | 4 + web/app/lib/roster.tsx | 134 ++++ web/app/node/page.tsx | 69 +- web/app/page.tsx | 62 +- web/app/tokens.css | 2 +- web/fixtures/generate.py | 132 ++++ web/public/demo-node/build | 7 + web/public/demo-node/health | 4 + web/public/directory.json | 4 + 89 files changed, 1567 insertions(+), 4861 deletions(-) delete mode 100644 .recurve/claims/network/GAPS.md delete mode 100644 .recurve/claims/network/bin/.gitignore delete mode 100644 .recurve/claims/network/cycles/BOOT-1/outcome.md delete mode 100644 .recurve/claims/network/cycles/BOOT-2/outcome.md delete mode 100644 .recurve/claims/network/cycles/BOOT-3/outcome.md delete mode 100644 .recurve/claims/network/cycles/WIT-N1/outcome.md delete mode 100644 .recurve/claims/network/cycles/WIT-N2/outcome.md delete mode 100644 .recurve/claims/network/cycles/WIT-N3/outcome.md delete mode 100644 .recurve/claims/network/cycles/WIT-N4/outcome.md delete mode 100644 .recurve/claims/network/cycles/WIT-N5/outcome.md delete mode 100644 .recurve/claims/network/gaps.draft.yaml delete mode 100644 .recurve/claims/network/gaps.yaml delete mode 100644 .recurve/claims/network/harness/.gitignore delete mode 100644 .recurve/claims/network/harness/compose/docker-compose.yml delete mode 100755 .recurve/claims/network/harness/down.sh delete mode 100755 .recurve/claims/network/harness/ensure-build-attestation.sh delete mode 100755 .recurve/claims/network/harness/ensure-image.sh delete mode 100755 .recurve/claims/network/harness/env.sh delete mode 100755 .recurve/claims/network/harness/kill-node.sh delete mode 100755 .recurve/claims/network/harness/ksn_emit.py delete mode 100755 .recurve/claims/network/harness/ksn_oracle.py delete mode 100755 .recurve/claims/network/harness/rebuild.sh delete mode 100755 .recurve/claims/network/harness/up.sh delete mode 100644 .recurve/claims/network/harness/versions.lock delete mode 100644 .recurve/claims/network/probes/_contract.sh delete mode 100755 .recurve/claims/network/probes/boot-1.sh delete mode 100644 .recurve/claims/network/probes/boot-1.trap/README.md delete mode 100644 .recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit1.json delete mode 100644 .recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit2.json delete mode 100644 .recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit3.json delete mode 100755 .recurve/claims/network/probes/boot-2.sh delete mode 100644 .recurve/claims/network/probes/boot-2.trap/README.md delete mode 100644 .recurve/claims/network/probes/boot-2.trap/node-in-default-tree/default-tree.txt delete mode 100755 .recurve/claims/network/probes/boot-3.sh delete mode 100644 .recurve/claims/network/probes/boot-3.trap/README.md delete mode 100755 .recurve/claims/network/probes/boot-3.trap/broken-sibling/cannot-measure.sh delete mode 100644 .recurve/claims/network/probes/fixtures/icp-event.json delete mode 100644 .recurve/claims/network/probes/fixtures/keri-icp.json delete mode 100644 .recurve/claims/network/probes/fixtures/receipt-bundle.json delete mode 100755 .recurve/claims/network/probes/wit-n1.sh delete mode 100644 .recurve/claims/network/probes/wit-n1.trap/README.md delete mode 100644 .recurve/claims/network/probes/wit-n1.trap/occupied-port/up.code delete mode 100644 .recurve/claims/network/probes/wit-n1.trap/occupied-port/up.out delete mode 100755 .recurve/claims/network/probes/wit-n2.sh delete mode 100644 .recurve/claims/network/probes/wit-n2.trap/README.md delete mode 100644 .recurve/claims/network/probes/wit-n2.trap/forged-signature/bundle.json delete mode 100755 .recurve/claims/network/probes/wit-n3.sh delete mode 100644 .recurve/claims/network/probes/wit-n3.trap/README.md delete mode 100644 .recurve/claims/network/probes/wit-n3.trap/stale-ksn/ksn.json delete mode 100644 .recurve/claims/network/probes/wit-n3.trap/stale-ksn/last_seen delete mode 100755 .recurve/claims/network/probes/wit-n4.sh delete mode 100644 .recurve/claims/network/probes/wit-n4.trap/README.md delete mode 100644 .recurve/claims/network/probes/wit-n4.trap/wrong-digest/forged.auths.json delete mode 100755 .recurve/claims/network/probes/wit-n5.sh delete mode 100644 .recurve/claims/network/probes/wit-n5.trap/README.md delete mode 100644 .recurve/claims/network/probes/wit-n5.trap/jargon-leak/happy-path.out delete mode 100644 .recurve/claims/web/gaps.draft.yaml create mode 100644 README.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 web/README.md create mode 100644 web/app/lib/admissions-key.json create mode 100644 web/app/lib/roster.tsx create mode 100644 web/fixtures/generate.py create mode 100644 web/public/demo-node/build create mode 100644 web/public/demo-node/health create mode 100644 web/public/directory.json diff --git a/.claude/skills/burndown/SKILL.md b/.claude/skills/burndown/SKILL.md index ac900ac..fece22d 100644 --- a/.claude/skills/burndown/SKILL.md +++ b/.claude/skills/burndown/SKILL.md @@ -1,6 +1,6 @@ --- name: burndown -description: Launch the unattended claims burndown for auths-network — sequential fresh agents close RED gaps under the fleet gate until done, capped, or parked out +description: Launch the unattended claims burndown for auths-network-web — sequential fresh agents close RED gaps under the fleet gate until done, capped, or parked out --- # Burndown launcher diff --git a/.claude/skills/cycle/SKILL.md b/.claude/skills/cycle/SKILL.md index 024b62e..c418370 100644 --- a/.claude/skills/cycle/SKILL.md +++ b/.claude/skills/cycle/SKILL.md @@ -1,6 +1,6 @@ --- name: cycle -description: Run exactly ONE improvement cycle for auths-network — triage the highest-value RED gap, sculpt, gate, promote, snapshot, stop +description: Run exactly ONE improvement cycle for auths-network-web — triage the highest-value RED gap, sculpt, gate, promote, snapshot, stop --- # Single cycle diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md index 79ddbc5..bfe1457 100644 --- a/.claude/skills/review/SKILL.md +++ b/.claude/skills/review/SKILL.md @@ -1,6 +1,6 @@ --- name: adversarial-review -description: Run the adversarial review protocol on a review-gated (security-tradeoff) gap for auths-network — try to BREAK the change, never to confirm it +description: Run the adversarial review protocol on a review-gated (security-tradeoff) gap for auths-network-web — try to BREAK the change, never to confirm it --- # Adversarial review diff --git a/.gitignore b/.gitignore index a2fe502..1ff740f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # recurve run state (parked gaps, records, receipts) .recurve/state/ +.recurve/ \ No newline at end of file diff --git a/.recurve/ADJUDICATE.md b/.recurve/ADJUDICATE.md index 598840a..5ab9fff 100644 --- a/.recurve/ADJUDICATE.md +++ b/.recurve/ADJUDICATE.md @@ -8,69 +8,16 @@ > point where hostile spec text can be kept out of the loop's > instruction stream. -## ADJUDICATE-1 — where the witness node + operator CLI live +## FORK-1: "looks incapable of lying: simple" — define it concretely + +- Spec (PRD.md): "This is trust infrastructure, so it must also look incapable of lying: simple," +- Why it forks: the spec says 'simple' — what exactly, with a number or an enumerable behavior? +- DECIDED (2026-06-14): "Simple / incapable of lying" is NOT a standalone claim or a + separate metric — it is **defined operationally as the conjunction of the quantified + design-gate claims**: FE-2 (tokens-only), FE-3 (one accent, neutral palette), FE-4 + (<=2 typefaces, body >=16px/1.5), FE-5 (8px spacing + max-width), FE-6 (AA both + schemes), FE-8 (functional motion only), FE-9 (LCP/CLS budgets), FE-10 (no + hacker/crypto tropes), FE-11 (plain microcopy). The page is "simple/incapable of + lying" exactly when all of those are GREEN. The vague sentence gets no probe of its + own; it is the AND of the design gate, so an agent cannot rationalize a definition. -PRD Open Question 1. Originally framed as "a separate witness binary vs a -platform subcommand." **Re-decided 2026-06-13** after recognizing a -witness node is *core trust infrastructure* (witnesses are central to the -KERI model), not a peripheral consumer like the demos — so it belongs in the -platform workspace, and a cargo feature (not a separate binary) is how auths -already keeps optional heavy components out of the lean default build -(precedent: `auths-rp`'s `git-sync` git2 feature; the fips/cnsa/bulk crypto -provider features). - -**DECIDED (2026-06-13, supersedes the standalone-binary decision):** -- **Witness node + threshold verification** → a **feature-gated Rust crate in - the platform workspace**, `auths/crates/auths-witness-node`, behind a - `witness-node` cargo feature. The lean default `auths` build never pulls it; - operators install a feature-enabled build (`cargo install auths --features - witness-node`, shipped as an `auths-witness` release binary). It composes - the platform's public crate APIs (`auths-witness`, `auths-keri`, - `auths-verifier`) — it does not re-implement protocol (WIT-B1). -- **Operator CLI** → **unified into the main `auths` CLI** as `auths witness …` - subcommands (NOT a separate binary). The subcommand *surface* is - always compiled in (thin clap definitions, no heavy deps); the *handler* is - feature-split — with the feature it runs the node, without it returns a - helpful error pointing operators at the witness-enabled build. Lean default - AND unified UX; the false dichotomy ("separate binary for leanness") is - resolved. -- **IaC** (OpenTofu `.tf`) → embedded with the node crate - (`auths-witness-node/deploy/`), shipped with the feature build. -- **Dashboard + public directory** (Next.js/TS) → the web tier - (auths-site / a web repo), never the Rust workspace. - -**What this means for this suite:** `auths-network/.recurve/` is no longer a -product repo — it is a **conformance/integration suite, structurally like -`interop/`**, whose probes drive building the `auths-witness-node` crate + -`auths witness` commands in the workspace and gate them against demos + -interop. Single tree, no cross-repo commits. Future -neutral-governance extraction to a separate governed repo remains possible — -extracting a crate later is easy; start coupled. - -## ADJUDICATE-2 — directory admission policy v1 - -PRD Open Question 2. Who may join the public directory at v1, checked -how, and who signs admissions until governance exists? Sketch on the -table: verified operator org identity + reachable endpoints + ToS -signature, admissions signed by the auths org identity (disclosed as -interim, replaced by governance later). Gates: WIT-D2 (`register` is -blocked until this is decided). - -**DECIDED (2026-06-13):** v1 admission requires a verified operator org identity + reachable endpoints + a signed ToS acknowledgment, with admissions counter-signed by the auths org identity as the explicitly-disclosed interim authority — to be replaced by neutral governance before the directory opens permissionlessly; the interim status must be visible in the directory itself. - -## ADJUDICATE-3 — classification of the WIT-T block - -Per this suite's default-closed convention, the threshold-verification -claims (WIT-T1..T5) start as `security-tradeoff` (review-gated — the -unattended loop will NOT work them). Counter-argument from the house -precedent: these claims ADD fail-closed checks rather than loosening -any, which in the demos/interop convention is ordinary green-gate work; -the only true loosening (accepting pre-revocation history) lives in -lost-the-laptop LTL-1/LTL-2 and keeps its own review protocol -regardless. Decide: keep WIT-T1..T5 review-gated (slower, maximally -cautious) or downgrade them to their natural classes -(missing-surface/wire-mismatch) so the loop can burn them down, with -WIT-T5's trap as the permanent guard. Gates: whether the autonomous -burndown can touch the suite's headline block at all. - -**DECIDED (2026-06-13):** Split — WIT-T1..T4 downgraded to `missing-surface` so the loop builds the threshold *mechanism* autonomously (they ADD fail-closed checks: a bug fails loud as over-strict, never silently permissive — the safe direction), while WIT-T5 stays `security-tradeoff` so a human confirms the headline outcome (the forged-ordering fixture genuinely dies) and it remains a permanent forgery trap. The one true loosening — accepting pre-revocation history — is NOT here; it lives in auths-demos LTL-1/LTL-2 and keeps its own review protocol regardless. **Sanity-check me on this one** — it is the load-bearing security call; if you'd rather a human design the whole threshold block, revert WIT-T1..T4 to `security-tradeoff`. diff --git a/.recurve/PRD.md b/.recurve/PRD.md index 2832462..871fcb5 100644 --- a/.recurve/PRD.md +++ b/.recurve/PRD.md @@ -1,502 +1,271 @@ -# PRD: auths-network — the witness network - -> **⚠ ARCHITECTURE UPDATE (2026-06-13) — supersedes the standalone-repo framing below.** -> The *what* in this PRD is unchanged (one-command witness standup, M-of-N -> threshold, public directory, the WIT-T mechanism that unblocks LTL-1/LTL-2). -> Only the *where* changed (see ADJUDICATE-1, re-decided): -> - **Witness node + threshold** → a feature-gated crate **`auths/crates/auths-witness-node`** (behind a `witness-node` cargo feature), NOT a standalone repo. Lean default build; operators install the feature-enabled `auths` / `auths-witness` build. -> - **Operator CLI** → **`auths witness …` subcommands in the main `auths` CLI** (feature-gated handler; the lean build returns a helpful "install the witness build" error). There is no separate `anet` binary — the command is `auths witness …`, full stop. -> - **IaC** (`.tf`) → embedded with the node crate (`…/deploy/`). **Dashboard + directory UI** → the web tier (auths-site / web repo), never the Rust workspace. -> - **This `.recurve/` suite** is now a conformance/integration suite (like `interop/`) that drives the in-workspace crate — single tree, no cross-repo commits. The "Why a separate repo" note in §1 and the repo-boundary framing in §7 are superseded by ADJUDICATE-1. - -> **One line:** make it one command to stand up a witness node, make the -> network of witnesses publicly discoverable, and make verifiers able to -> demand M-of-N independent receipts — so that key-event ordering becomes -> unforgeable without collusion, and the agentic-trust flywheel can start -> turning. +# PRD: auths-network web — the witness network's public face + +> **One line:** the public, trustworthy face of the witness network — a public +> **directory** of witnesses and a read-only **operator console** — that *renders +> verifiable proofs of the network's state, never assertions*, under a simple, +> Apple-grade design language. > -> **Build method:** recurve (claims-driven, probe-gated). This PRD is written -> to be *claimified*: every requirement names an observable and its -> adversarial twin, so the build IS the burndown. Cycles build the -> `auths-witness-node` crate + `auths witness …` commands IN `../auths` and -> sculpt the platform as needed (exactly as `auths-demos` did); this `.recurve/` -> dir holds only the suite (claims, probes, harness). Dashboard → web tier. +> **Scope — frontend only.** The witness node, its protocol, key-state, receipts, +> threshold verification, and standup are already built and live in the platform +> (`../auths`). They are **out of scope here.** This project is a web app that +> *fetches already-signed data, verifies it client-side through the published +> verifier, and renders it.* It originates no trust and computes no verdicts. > -> **Scope decisions (adjudicated 2026-06-12):** v1 = node + directory + -> threshold verification, dashboard read-only (operator console + public -> directory). Standup targets local Docker Compose AND `--cloud` via embedded -> OpenTofu. First network = 1 auths-operated node + 2 independent partner -> nodes (real 2-of-3 from day one). +> **Build method:** recurve (claims-driven, probe-gated). This PRD is written to be +> *claimified*: every requirement names an observable and its adversarial twin, so +> the build IS the burndown. --- ## 1. Introduction / Overview -Today the auths trust root is **staged**: a single registry, no independent -witnesses. Three consequences, all already named in the ledgers: - -- Revocation *ordering* is signer-stamped and forgeable (`lost-the-laptop` - LTL-1/LTL-2, review-gated) — a thief can claim "signed before revocation." -- Duplicity (an identity showing two histories) is detectable in code but - fail-open on key paths (`verify-the-world` V1, review-gated). -- Every enterprise conversation eventually hits "who runs the - infrastructure?" (GTM lever L4 — the unglamorous backbone). - -The witness network fixes all three with one mechanism: **independent -operators receipt key events; verifiers require receipts at a threshold the -identity controller designates (e.g. 2-of-3); forged ordering then requires -collusion of M witnesses.** Because each identity designates its *own* -witness set in its event log, the network is permissionless-by-design and -incrementally adoptable — no global consensus, no chain. A witness is closer -to an NTP server than a blockchain validator. - -The flywheel this unlocks: more cross-org agent traffic → more value in -running your own witness (stop trusting a competitor's view of key state) → -more independent witnesses → stronger ordering/duplicity guarantees → -higher-value agent transactions become safe → more traffic. The product's -job is to make each turn of that wheel one command cheaper. - -**Why an in-workspace crate (not a separate repo):** a witness node is core -trust infrastructure, not a peripheral consumer — it belongs in the platform -workspace as the feature-gated `auths-witness-node` crate (ADJUDICATE-1). -This `.recurve/` suite mirrors `interop/`: it holds the claims, probes, and -harness that *drive* building that crate, and federates into the same gate as -demos + interop (shared tree → one gate, one loop at a time, lockfile rule). +The witness network needs a public face that a stranger can trust at a glance and an +operator can run infrastructure against — without either of them having to take the +page's word for anything. Two surfaces: -## 2. Goals +- A **public directory** of witnesses (operator, jurisdiction, uptime, receipt + volume) — the network's growth chart, screenshottable and linkable. +- A read-only **operator console** for a single node — health, receipts, peers, + version — run like real infrastructure. -- **G1 — One-command witness.** A stranger with Docker (or a cloud account) - goes from nothing to a receipting, directory-registered witness node in - one command and ≤10 minutes, with zero protocol vocabulary required. -- **G2 — Real 2-of-3 by launch.** Three live nodes (auths + 2 independent - operators), and at least one identity verifying at a 2-of-3 threshold in - production use. -- **G3 — Threshold verification in the platform.** `auths-verifier` can - require M-of-N receipts from a designated witness set, and a forged - ordering FAILS without collusion of M witnesses — probed, with traps. -- **G4 — The directory as a verifiable artifact.** The public witness - directory is itself signed and offline-verifiable (we eat our own - dogfood; the directory is not a trusted web page). -- **G5 — Feed the platform.** Every platform gap discovered lands in a - ledger with a RED probe and gets burned down by the recurve loop — and - this work unblocks the parked review-gated gaps (LTL-1, LTL-2, V1) by - providing the corroboration mechanism they require. -- **G6 — Operable, honestly.** Operator console + public directory expose - real health/receipt metrics; a node that lies about uptime is caught by - probes, not marketing. +The non-negotiable: **the page renders proofs, not claims.** Every figure that +implies trust is backed by a client-side verification or a signed, reproducible +prober result. The directory is a *signed artifact*, not a trusted web page; a +tampered entry renders as a visible verification failure, not as data. Verification +itself is never re-implemented here — it runs through the published `@auths-dev/verify` +package, the same verdict path the rest of the platform uses. -## 3. User Stories +This is trust infrastructure, so it must also *look* incapable of lying: simple, +neutral, refined — the design language in §6. -### US-001: Operator stands up a local witness in one command -**Description:** As an infrastructure operator at a partner org, I want -`auths witness up` to take me from a blank VPS to a running, healthy witness node so -that joining the network costs minutes, not a project. +## 2. Goals -**Acceptance Criteria:** -- [ ] `auths witness up` on a machine with Docker brings up the witness node + - monitor sidecar via embedded Compose; exits 0 with a printed health URL -- [ ] `auths witness status` reports: node identity, health, receipts issued, peers, - KSN endpoint reachability -- [ ] Node identity is generated at first boot (enclave/KMS-backed where - available, file-key fallback with a stern warning) -- [ ] Total wall-clock command→healthy ≤ 10 min on a clean VPS (probed, - scripted cold-start — TTV discipline) -- [ ] No KERI vocabulary in any output of the happy path (leak-gate probe, - same grep discipline as TTV-1 in `roadmap/aspirational_claims/`) -- [ ] Adversarial: `auths witness up` on a box where the port is taken / Docker - absent fails with a one-line actionable error, not a stack trace - -### US-002: Operator provisions to a cloud in one command -**Description:** As an operator without spare hardware, I want -`auths witness up --cloud aws|gcp|hetzner|fly` to provision and start the node via -embedded IaC so the cloud path is the same single command. +- **G1 — The directory is a verifiable artifact.** The directory dataset is a signed + artifact fetched and verified in the browser; the page renders only what verifies. + A tampered entry shows as a failure, visibly — never as data. +- **G2 — A console that cannot lie.** The operator console reflects real node health; + a dead node shows DOWN within 60s and is never stale-green. +- **G3 — Proofs, not assertions.** Every trust-bearing element on both surfaces is + backed by a client-side verification (via the published verifier) or a signed + prober result — never a hardcoded "verified" badge. +- **G4 — Simple, powerful, trustworthy.** The surfaces meet the §6 design language: + Apple-grade restraint, neutral/trustworthy, accessible (WCAG AA in both schemes), + fast (LCP/CLS budgets), with zero hacker/crypto aesthetic. +- **G5 — One verdict vocabulary.** Verification verdicts render through one shared + component (verified / failed / unreceipted) across both surfaces, with failure the + most prominent thing on the page when present. -**Acceptance Criteria:** -- [ ] Embedded OpenTofu modules per provider; `auths witness up --cloud

` plans, - applies, boots, health-checks — one command, idempotent on re-run -- [ ] `auths witness down --cloud

` tears down cleanly (probed: re-run `up` after - `down` succeeds; no orphaned billable resources — verified via - provider inventory diff in the harness) -- [ ] State stored locally by default (documented), with `--state` escape - hatch; no auths-operated state service in the loop -- [ ] Adversarial: invalid credentials fail BEFORE any resource is created - -### US-003: Witness receipts events and serves key state -**Description:** As an identity controller, I want a witness I designate to -receipt my key events and serve signed key-state notices so verifiers can -corroborate ordering without trusting me. +## 3. User Stories -**Acceptance Criteria:** -- [ ] Node ingests a designated identity's events and returns receipts - (non-transferable witness key, standard `B`-code — requires interop - IOP-L3b, referenced not duplicated) -- [ ] Node serves KERI-conformant KSN at a stable endpoint (requires - IOP-L3c; cross-verified against the keripy oracle in `interop/peers/`) -- [ ] Receipts verify offline on a stranger's machine from the receipt + - the witness's published identity alone -- [ ] Adversarial: a receipt with a bit-flipped signature is rejected; a - KSN for a stale state is detected as stale by a verifier holding a - newer receipt - -### US-004: Org designates its witness set and threshold -**Description:** As an org admin, I want to designate my witness set and -threshold (e.g. 2-of-3: us, partner A, partner B) in my org identity so -that my key-event ordering is corroborated by operators I chose. +### US-1: Anyone browses the public witness directory +**Description:** As anyone evaluating the network, I want a public directory of +witnesses — operator, jurisdiction, uptime, receipt volume — so I can choose a +diverse witness set and watch the network grow. **Acceptance Criteria:** -- [ ] `auths org witness set --threshold M` writes the designation - into the org KEL (platform sculpt; CLI surface in `../auths`) -- [ ] Designation is itself a signed, anchored event (history shows when - the witness set changed, forever) -- [ ] Rotating the witness set is one command and takes effect at a - provable log position -- [ ] Adversarial: a witness NOT in the designated set producing receipts - does not satisfy the threshold - -### US-005: Verifier enforces M-of-N receipts -**Description:** As a relying party, I want verification to fail closed -when an identity's events lack threshold receipts so that forged ordering -requires collusion, not just a stolen key. +- [ ] The directory lists witnesses with: operator name + verified org identity, + region/jurisdiction, status, uptime %, receipts (7d), endpoint +- [ ] The directory dataset is a signed artifact fetched and verified client-side; + the page renders only entries that verify +- [ ] The interim governance status of admissions is visible in the directory itself +- [ ] Adversarial: a tampered directory entry renders as a verification failure, + visibly — not as data + +### US-2: An operator watches their node (read-only) +**Description:** As a node operator, I want a dashboard for MY node — health, receipts +issued, identities served, peer reachability, version — so I can run it like real +infrastructure. **Acceptance Criteria:** -- [ ] `auths-verifier` exposes a threshold policy: given a designated set + - M, verification of ordering-sensitive verdicts (SignedBefore / - SignedAfterRevocation) requires M valid receipts -- [ ] The LTL-2 forgery (signer-stamped low anchor-seq) FAILS under - threshold policy — this is the trap fixture, kept forever -- [ ] Verdict distinguishes "unreceipted" from "invalid" (operators can fix - the former; the latter is an attack) -- [ ] Overhead: threshold check adds ≤ 50ms p99 to verification on the - pinned rig (perf probe with hysteresis) -- [ ] Adversarial: M−1 receipts do not verify; receipts from one operator's - two nodes count once under the diversity rule (see FR-9) - -### US-006: Anyone browses the public witness directory -**Description:** As anyone evaluating the network, I want a public -directory of witnesses — operator, jurisdiction, uptime, receipt volume — -so I can choose a diverse witness set and see the network grow. +- [ ] The console reads the node's metrics/health endpoint and shows: health + timeline, receipts/day, identities served, request rate, last-seen-by-peers, + and the node's signed build attestation (which binary it runs) +- [ ] Read-only — no mutating actions +- [ ] Adversarial: the console against a dead node shows DOWN within 60s; never + stale-green -**Acceptance Criteria:** -- [ ] Directory page lists nodes with: operator name + verified org - identity, region/jurisdiction, status, uptime %, receipts (7d), - KSN endpoint -- [ ] The directory dataset is a **signed artifact in a git repo**, fetched - and verified client-side (WASM verifier) — the page renders only what - verifies; "trust me" pages are off-brand -- [ ] Registration: `auths witness register` opens a signed entry (PR-style) against - the directory repo; admission per governance policy (Open Question 2) -- [ ] Verify in browser using dev-browser skill -- [ ] Adversarial: a tampered directory entry renders as a verification - failure, visibly, not as data - -### US-007: Operator console (read-only v1) -**Description:** As a node operator, I want a dashboard for MY node — -health, receipts issued, identities served, peer reachability, version — -so I can run it like real infrastructure. +### US-3: The page proves what it shows +**Description:** As a security-minded visitor, I want every trust-bearing element to +be the result of a verification I could re-run, so I never have to trust the page. **Acceptance Criteria:** -- [ ] Console reads node metrics endpoint (Prometheus-compatible; complete - the `auths-monitor` daemon as the collector — it is currently a - 394-LoC framework with incomplete handlers) -- [ ] Views: health timeline, receipts/day, identities served, KSN request - rate, last-seen-by-peers -- [ ] Alert hooks: webhook on unhealthy > N minutes (no paging product in - v1 — webhook only) -- [ ] Verify in browser using dev-browser skill -- [ ] Adversarial: console against a dead node shows DOWN within 60s; never - stale-green +- [ ] Every verdict renders through the published `@auths-dev/verify` package — the + app re-implements no verification, threshold, or duplicity logic +- [ ] Verdicts render through one shared component with one vocabulary (verified / + failed / unreceipted); a failure is the most prominent element on the page +- [ ] Adversarial: any `verify`/`threshold`/duplicity logic in the app outside the + published package fails the boundary gate -### US-008: The recurve loop builds it -**Description:** As the platform team, I want every requirement here to -exist as a claim with a RED probe in this repo's suite so the autonomous -loop can burn it down and the platform inherits the hardening — exactly the -auths-demos pattern. +### US-4: The look is simple, refined, and consistent +**Description:** As anyone who lands on the site, I want it to feel like a simple, +powerful, trustworthy piece of well-designed technology — not a crypto dashboard. **Acceptance Criteria:** -- [ ] `auths-network/claims/` suite scaffolded (gaps.yaml + GAPS.md + - probes/ + harness/), federated into the shared gate with demos + - interop (one tree, one gate, lockfile — never concurrent loops) -- [ ] Bootstrap/scaffolding gaps filed first ("harness boots 3 local - witnesses", "probe can run at all") so cycle 1 never faces a wall of - BROKEN — greenfield rule from `recurve/plan.md` §7.B -- [ ] Every FR below maps to ≥1 ledger entry; `coverage --gate` green -- [ ] Platform-side gaps reference, never duplicate, interop entries - (IOP-L3b/L3c/L4a are prerequisites owned by `interop/`) -- [ ] The run that closes WIT-T* explicitly unblocks the review protocol - for LTL-1/LTL-2/V1 (they stay review-gated; the mechanism now exists) +- [ ] Design tokens are the single source of visual truth; components consume tokens + only — a hardcoded color or font fails the design gate +- [ ] WCAG 2.2 AA contrast holds in BOTH light and dark schemes, probed on rendered + pages +- [ ] The anti-trope gate is clean (no terminal/hacker aesthetic, no glitch/matrix + effects, monospace only for identifiers) +- [ ] Adversarial: a change introducing a hardcoded color, a third typeface, a + sub-AA pair, decorative animation, or an anti-trope element fails the gate ## 4. Functional Requirements -**Node & standup** -- FR-1: `auths witness up` stands up witness + monitor via embedded Docker Compose; - `auths witness up --cloud aws|gcp|hetzner|fly` provisions via embedded OpenTofu - modules. Both paths: one command, idempotent, health-checked exit. -- FR-2: `auths witness down`, `auths witness status`, `auths witness register`, `auths witness logs` complete - the operator verb set. No other verbs in v1. -- FR-3: The witness node is the hardened `auths-witness` server plus: KSN - serving (per IOP-L3c), receipt issuance with non-transferable keys (per - IOP-L3b), Prometheus metrics, and a signed version/build attestation - (the node proves what binary it runs — dogfood `artifact sign --ci`). -- FR-4: Node key custody: KMS/enclave where the platform provides it; - file-backed fallback requires `--accept-file-key` acknowledgment. +**The app** +- FR-1: One Next.js app in `web/`, two surfaces: `/directory` (public) and `/node` + (operator console). Read-only in v1. Adversarial: any control that mutates network + or node state is out of scope and its presence fails review. **Directory** -- FR-5: The directory is a git repo of signed entries; the dashboard - renders only entries that verify client-side (WASM). Hosting is static. -- FR-6: Entries carry: operator org identity (auths identity, verified), - endpoints, region/jurisdiction labels, admission timestamp. -- FR-7: Uptime/receipt stats are computed by an open prober (part of this - repo) whose results are themselves signed and published — observers can - re-run the prober and get the same answer. - -**Threshold verification (platform sculpt)** -- FR-8: KEL-designated witness sets + threshold M (US-004); verifier - enforcement (US-005) with distinct verdicts for unreceipted vs invalid. -- FR-9: Diversity rule shipped as the DEFAULT verifier policy: receipts - counting toward M must come from distinct operators (by directory - identity), with jurisdiction-diversity as an optional stricter mode. A - threshold met by one operator's three nodes is the CA oligopoly rebuilt — - default-closed against it. -- FR-10: Graceful degradation policy is explicit and conservative: if - fewer than M designated witnesses are reachable, ordering-sensitive - verdicts fail closed with `InsufficientReceipts`; non-ordering verdicts - (signature validity, capability checks) proceed and say so. No silent - downgrade. - -**Dashboard** -- FR-11: One Next.js app (reuse `auths-site` design system + the - `@auths-dev/verify` widget), two surfaces: `/directory` (public, US-006) - and `/node` (operator console, US-007). Read-only in v1 — no mutating - control-panel actions. - -**Recurve integration** -- FR-12: Suite layout, probe contract (GREEN 0 / RED 1 / BROKEN 2, traps, - freshness `reads:` keys), draft→baseline ceremony, and per-cycle - snapshots all follow the house pattern; claim blocks: WIT-N* (node), - WIT-I* (IaC), WIT-D* (directory), WIT-T* (threshold), WIT-O* (ops). -- FR-13: The harness can stand up a 3-witness local network (Compose) as - the probe fixture, with failure injection: kill 1 node (threshold still - met), kill 2 (fails closed), forged ordering (trap). +- FR-2: The directory dataset is a signed artifact the app fetches and verifies + client-side; the page renders only entries that verify. Adversarial: a tampered + entry renders as a verification failure, not as data. +- FR-3: Each entry shows operator org identity (verified), region/jurisdiction, + status, uptime %, receipts (7d), endpoint. Uptime/receipt figures come from a + signed, reproducible prober result — an outside observer re-running the prober + gets the same number. Adversarial: a stat not backed by a verifiable result is not + rendered as fact. +- FR-4: The directory is designed for "number going up" (nodes, operators, + jurisdictions, receipts/day) and stays calm at every viewport. + +**Operator console** +- FR-5: `/node` reads a node's metrics/health endpoint and shows the health timeline, + receipts/day, identities served, request rate, last-seen-by-peers, and the node's + signed build attestation. Adversarial: a dead node shows DOWN within 60s; never + stale-green. +- FR-6: Console density is delivered through progressive disclosure; the default view + stays calm. + +**Verification boundary** +- FR-7: ALL verification verdicts render through the published `@auths-dev/verify` + package — the app re-implements no verification, threshold, or duplicity logic. + Adversarial: any such logic in the app outside that package fails the boundary gate + (`grep -rE "verify|threshold" web/ | grep -v "@auths-dev/verify"` → empty). +- FR-8: Verdicts render through one shared component with one vocabulary (verified / + failed / unreceipted); a failure is the most prominent element on the page. + +**Design system** +- FR-9: Design tokens are the single source of visual truth (color, type, spacing, + radius, motion) in one file; components consume tokens only. Adversarial: a + hardcoded color or font in a component fails the design gate. +- FR-10: The full §6 design language is gated — each bullet in §6 is an observable + with an adversarial twin. ## 5. Non-Goals (Out of Scope for v1) -- **No org-facing control panel UI.** Witness-set designation is CLI-only - (US-004); the panel is v2 after the org console exists. -- **No permissionless directory admission.** Day one is auths + 2 partners; - the admission policy is governed (Open Question 2), not open. -- **No incentive/token layer, no billing.** Witness economics in v1 = - self-interest + partnership. Paid managed witnessing is a later SKU. -- **No global consensus, no chain, no gossip protocol.** Witnesses are - independent receipt servers; the threshold lives in the verifier. -- **No Kubernetes/Helm artifact** (Compose + OpenTofu only; Helm when a - platform-team operator asks). -- **No paging/alerting product** (webhook only). -- **No new cryptography.** Everything composes existing platform - primitives; if a claim seems to need novel crypto, it's mis-scoped — file - it for adjudication. +- **No node, protocol, or verification logic.** The app runs no witness node and + computes no verdicts / threshold / duplicity — it consumes the published verifier + and published data. That code lives in `../auths`. +- **No mutating control panel.** Read-only v1: no witness-set designation, no node + control, no registration UI (registration is CLI-side). +- **No borrowed visual identity.** The design system is defined fresh per §6 — NOT + auths-site, NOT any marketing site. +- **No incentive / billing / paging UI.** ## 6. Design Considerations -- **KERI-invisible carries over.** Operators see "witness", "receipt", - "key state", "threshold" — never KEL/KSN/CESR in the happy path. The - leak-gate grep is a standing probe (same rule that productized TTV-1). -- **Directory = front door.** It will be screenshotted, linked, and used as - the network's growth chart. Design for "number going up": nodes, - operators, jurisdictions, receipts/day. Reuse the `auths-site` "Ledger" - visual language. -- **The dashboard renders proofs, not assertions** — every green element in - both surfaces is backed by a client-side verification or a signed prober - result. This is the brand, enforced by probes. +- **Directory = front door.** It will be screenshotted, linked, and used as the + network's growth chart. Design for "number going up": nodes, operators, + jurisdictions, receipts/day — rendered in the §6 design language (its own clean + tokens, simple and Apple-like). +- **The page renders proofs, not assertions** — every trust-bearing element on both + surfaces is backed by a client-side verification or a signed prober result. This is + the brand, enforced by probes. +- **Plain copy.** Microcopy is plain and declarative — no protocol vocabulary, no + fear-based framing; safety is demonstrated by proofs rendering, never asserted by + adjectives. ### Design language — neutral, safe, inviting, powerful -This is trust infrastructure; the surfaces must look like something you'd -let your bank run. The register is Apple-grade restraint: form *is* -function here, because a page that renders cryptographic proofs has to look -incapable of lying. Calm, neutral, generous with space, fast — and **never -themed "crypto" or "hacker"**: no terminal cosplay, no spectacle. Power -reads through clarity and density-on-demand, not decoration. The four -words, mechanized: *neutral* = palette and voice; *safe* = accessibility -and fail-closed visuals; *inviting* = whitespace, type, and speed; -*powerful* = real data, progressively disclosed, proofs rendered inline. - -The qualities are deliberately quantified below — "simple" and "beautiful" -are not probeable; the following are: - -- Design tokens must be the single source of visual truth (color, type, - spacing, radius, motion) in one file; components must consume tokens only, - and a hardcoded color or font in a component is a gate failure (grep - probe, same discipline as the leak gate). -- The palette must be neutral-first: one near-white light surface, one calm - dark surface, ONE accent color, and semantic state colors reserved - exclusively for verification verdicts — color must carry meaning, never - decoration. -- The UI must never ship a green-on-black terminal aesthetic, glitch/ - scanline/matrix effects, ASCII-art banners, hex-dump decoration, or - monospace as body text — monospace is reserved for identifiers, receipts, - and code (standing anti-trope probe over stylesheets and components). -- Typography must use at most two typefaces with a documented modular - scale; body text must be ≥ 16px with line-height ≥ 1.5. -- Layout must breathe: spacing only from an 8px scale, a content max-width - on every reading surface, and operator-console density delivered through - progressive disclosure — the public directory stays calm at every - viewport. -- Every text/background pair must meet WCAG 2.2 AA contrast (≥ 4.5:1 body, - ≥ 3:1 large text) in BOTH color schemes, probed on rendered pages, not - in the token file alone. -- Keyboard navigation must reach every interactive element with a visible - focus state, and both surfaces must honor prefers-reduced-motion and - prefers-color-scheme. -- Motion must be functional only: state transitions in the 150–300ms range, - and zero looping or decorative animation anywhere. -- Speed is part of the design language: directory and console must hold - LCP ≤ 2.5s and CLS ≤ 0.1 on a mid-range device profile (headless-browser - probe in the harness; a beautiful page that janks is off-brand). -- Verification verdicts must render through one shared component with one - vocabulary (verified / failed / unreceipted) across both surfaces, and a - failure must be the most visually prominent element on the page when - present — calm, specific, unmissable, never alarmist. -- Microcopy must be plain and declarative: no exclamation marks, no - protocol vocabulary (KERI-invisible extends to copy), no fear-based - framing — safety is demonstrated by proofs rendering, never asserted by - adjectives. -- Adversarial twin for the whole section: a change introducing a hardcoded - color, a third typeface, a sub-AA pair, decorative animation, or any - anti-trope element must fail the design gate before it lands. - -Relationship to FR-11: this section governs and extends the reused -`auths-site` design system — where they conflict, this section wins and the -delta is upstreamed as tokens, not forked locally. +**The reference is a simple Apple product page — NOT auths-site, NOT any existing +marketing site.** This is trust infrastructure; the surfaces must look like something +you'd let your bank run, and like a single, refined, quietly powerful piece of +well-designed technology. The register is Apple-grade restraint: form *is* function +here, because a page that renders cryptographic proofs has to look incapable of lying. +Sleek, calm, neutral and trustworthy, generous with space, fast — and **never themed +"crypto" or "hacker"** (no terminal cosplay, no spectacle) and **never inheriting +auths-site's look**. Power reads through clarity and density-on-demand, not decoration. +The four words, mechanized: *neutral* = a neutral, trustworthy palette and voice; +*safe* = accessibility and fail-closed visuals; *inviting* = whitespace, type, and +speed; *powerful* = real data, progressively disclosed, proofs rendered inline. The +whole, in one line: **simple yet powerful, sleek, refined, trustworthy.** + +The qualities are deliberately quantified below — "simple" and "beautiful" are not +probeable; the following are: + +- Design tokens must be the single source of visual truth (color, type, spacing, + radius, motion) in one file; components must consume tokens only, and a hardcoded + color or font in a component is a gate failure (grep probe). +- The palette must be neutral-first: one near-white light surface, one calm dark + surface, ONE accent color, and semantic state colors reserved exclusively for + verification verdicts — color must carry meaning, never decoration. +- The UI must never ship a green-on-black terminal aesthetic, glitch/scanline/matrix + effects, ASCII-art banners, hex-dump decoration, or monospace as body text — + monospace is reserved for identifiers, receipts, and code (standing anti-trope probe + over stylesheets and components). +- Typography must use at most two typefaces with a documented modular scale; body text + must be ≥ 16px with line-height ≥ 1.5. +- Layout must breathe: spacing only from an 8px scale, a content max-width on every + reading surface, and operator-console density delivered through progressive + disclosure — the public directory stays calm at every viewport. +- Every text/background pair must meet WCAG 2.2 AA contrast (≥ 4.5:1 body, ≥ 3:1 large + text) in BOTH color schemes, probed on rendered pages, not in the token file alone. +- Keyboard navigation must reach every interactive element with a visible focus state, + and both surfaces must honor prefers-reduced-motion and prefers-color-scheme. +- Motion must be functional only: state transitions in the 150–300ms range, and zero + looping or decorative animation anywhere. +- Speed is part of the design language: directory and console must hold LCP ≤ 2.5s and + CLS ≤ 0.1 on a mid-range device profile (headless-browser probe; a beautiful page + that janks is off-brand). +- Verification verdicts must render through one shared component with one vocabulary + (verified / failed / unreceipted) across both surfaces, and a failure must be the + most visually prominent element on the page when present — calm, specific, + unmissable, never alarmist. +- Microcopy must be plain and declarative: no exclamation marks, no protocol + vocabulary, no fear-based framing — safety is demonstrated by proofs rendering, + never asserted by adjectives. +- Adversarial twin for the whole section: a change introducing a hardcoded color, a + third typeface, a sub-AA pair, decorative animation, or any anti-trope element must + fail the design gate before it lands. + +The design language is the SOLE source of the frontend's look. It does NOT inherit, +reuse, extend, or align with the auths-site design system or any other marketing site +— the frontend defines its own tokens from scratch to the rules above. (Generic, +identity-free primitives are fine — the `@auths-dev/verify` widget for verdicts, a +headless/unstyled component library — but never another site's visual identity.) ## 7. Technical Considerations -- **Reuse over build:** `auths-witness` (real, 101 LoC wrapper over shared - core), `auths-checkpoint-cosigner` (real, C2SP cosigs), `auths-transparency` - (RFC 6962), `auths-monitor` (complete it — currently framework-only), - `auths-site` design system, `@auths-dev/verify` widget, interop's keripy - oracle + `versions.lock` convention for KSN conformance probes. -- **Dependency order:** IOP-L3b → IOP-L3c (interop suite, platform sculpts) - → WIT-N receipts/KSN → WIT-T threshold → WIT-D directory stats. IaC - (WIT-I) and dashboard (WIT-O/D UI) parallelize after WIT-N. -- **Shared-tree discipline:** this suite's cycles sculpt `../auths`; the - burndown must run under the same lockfile/federated-gate regime as demos - + interop. Never two loops on the tree (recurve plan §11.13). -- **Partner reality:** 2 independent operators means real-world keys, - firewalls, and time zones. Budget a human onboarding runbook (RUNBOOK.md) - and treat partner onboarding as part of v1 acceptance, not aftermath. -- **Review-gated handling unchanged:** closing WIT-T provides the - corroboration mechanism LTL-1/LTL-2/V1 require, but their promotion still - goes through the adversarial review protocol — the loop must not - auto-promote them. - -### Repo boundary — `../auths` decides, this repo operates - -The seam is protocol vs. operation, not frontend vs. backend: -**`../auths` owns anything that must be correct for strangers; this repo -owns anything that must be convenient for operators.** Three tests make the -boundary mechanical: - -1. **Verifier test:** if a third party verifying a receipt offline needs the - code to be correct, it is platform (threshold policy, receipt/KSN - formats, diversity rule, directory *entry* format — all `../auths`). -2. **Second-network test:** code a different witness network would reuse - unchanged is platform (the witness binary, the verifier); what they would - replace is this repo (directory instance + governance, IaC, dashboard, - prober, runbooks). -3. **Cadence test:** protocol changes are slow and conformance-gated; - dashboards and OpenTofu modules change weekly. Different velocity, - different security posture — npm supply chain never enters the trust - kernel's review boundary. - -Consequences, made concrete: - -- The witness server, threshold verification, KEL witness designation, and - `auths-monitor` live in `../auths` (the "platform sculpts" column of - Appendix A). Threshold logic lives in `auths-verifier` specifically, so - WASM, FFI, and CLI verdicts are one implementation — the dashboard renders - proofs *through the published verifier*, never beside it. -Boundary discipline lives in WIT-B1–B4, reframed for the in-workspace model: -- **WIT-B1:** `auths-witness-node` composes the platform's PUBLIC crate APIs - (`auths-witness`, `auths-keri`, `auths-verifier`) — depending on them IS the - integration. It reimplements zero protocol: a message the platform doesn't - expose is a missing public API → add the surface, never inline the bytes. -- **WIT-B2:** the `witness-node` cargo feature is purely additive — no core - crate depends on the node crate, and a default `auths` build pulls none of - its heavy deps (the lean install stays lean). -- **WIT-B3:** the dashboard (web tier) verifies only through the published - `@auths-dev/verify` WASM package — never a forked verdict path. -- **WIT-B4:** standup deploys released, attested binaries, never source builds. - -Violation checks (the WIT-B probes): - -```bash -grep -rnE "fn .*\bsaid\b|parse.*cesr|decode.*receipt" auths/crates/auths-witness-node/src/ # hand-rolled protocol → RED (B1) -cargo tree -p auths-cli | grep -q auths-witness-node && echo RED # node in DEFAULT deps → RED (B2) -grep -rE "verify|threshold" web/.../dashboard/src/ | grep -v "@auths-dev/verify" # forked verdict → RED (B3) -grep -rE "cargo build|--path" deploy/ # source build in standup → RED (B4) -``` - -One tempting move, banned: implementing threshold checks in the dashboard -backend "to iterate faster" — that forks verification truth, and the fork is -the one in the screenshot. (The witness server itself correctly lives in the -platform now — `auths-witness` + `auths-witness-node` — inside the -conformance/audit boundary; that's the ADJUDICATE-1 decision, not a violation.) +- **Stack:** one Next.js app; static-first where possible. No backend that could fork + verification truth. +- **Data sources (read-only):** the signed directory artifact (a repo of signed + entries) and a node's published metrics/health endpoint. The app fetches, verifies + via `@auths-dev/verify`, and renders. It never originates trust. +- **The boundary, mechanized:** `grep -rE "verify|threshold" web/` (minus the + published-package import) finds no forked verdict path; verification truth has one + home, and it is not this app. +- **Reuse:** the `@auths-dev/verify` widget for verdicts; a headless/unstyled + component library is acceptable. The visual design system is defined fresh (§6), + never borrowed. +- **No new cryptography, no node logic.** If a claim seems to need either, it is + mis-scoped — it belongs in `../auths`, not here. ## 8. Success Metrics -- Operator TTV: command → healthy, registered node ≤ 10 min local / - ≤ 20 min cloud (scripted, probed, on every release). -- Network: 3 live nodes, ≥ 2 independent operators, ≥ 1 org verifying at - 2-of-3 in production by v1 close. -- Security: the LTL-2 forged-ordering trap fails under threshold policy — - permanently RED-guarded; review protocol for LTL-1/LTL-2/V1 unblocked. -- Verification overhead: ≤ 50ms p99 added by threshold checks (pinned rig). -- Recurve health: 100% of FRs covered by ledger entries; coverage gate - green; zero TODOs (discoveries become filed gaps). -- Honesty: directory uptime numbers reproducible by an external observer - re-running the open prober. +- Design gate green: tokens-only, AA in both schemes, anti-trope grep clean, + LCP ≤ 2.5s / CLS ≤ 0.1, reduced-motion + color-scheme honored. +- Directory: a tampered entry renders as a visible failure (probed). +- Console: a dead node shows DOWN within 60s; never stale-green (probed). +- Boundary: zero verification logic in the app outside the published verifier. ## 9. Open Questions -1. **CLI naming:** ✅ RESOLVED (ADJUDICATE-1) — unified `auths witness …` - subcommands in the main CLI, feature-gated (lean default build, helpful - error without the feature). No separate binary. -2. **Directory admission policy v1:** what are the criteria (verified org - identity + reachable endpoints + ToS signature?), and who signs - admissions until governance exists? Needs a human decision recorded as - an ADJUDICATE entry before `auths witness register` is built. -3. **Partner selection:** which two independent operators? (Design partner - + OSS foundation per the GTM lighthouse motion — names needed.) -4. **Witness ToS/liability:** what does a receipt legally attest? Needs - counsel input before partners sign; engineering proceeds regardless. -5. **Jurisdiction labels:** self-declared vs verified? (v1: self-declared - with display caveat; revisit when diversity-strict mode matters.) -6. **Does the prober live in this repo or as a third "observer" role?** - (v1: this repo; an independent observer network is the v2 version of - the same flywheel.) - ---- - -## Appendix A — Claim-block map (for the claimify pass) - -| Block | Claims (sketch) | Probes live in | Platform sculpts | -| --- | --- | --- | --- | -| WIT-N | one-command local standup; receipts verify offline; KSN conformant; build attestation | `claims/network/probes/` | `auths-witness`, KSN surfaces (after IOP-L3b/c) | -| WIT-I | cloud up/down idempotent; no orphan resources; creds fail-before-create | same | none (tooling-only) | -| WIT-D | directory signed + client-verified; tamper renders as failure; stats reproducible | same | none / minor | -| WIT-T | M-of-N enforced; LTL-2 trap fails; M−1 insufficient; diversity default; ≤50ms p99 | same | `auths-verifier` threshold policy, KEL witness designation, CLI | -| WIT-O | console never stale-green; DOWN ≤ 60s; metrics complete | same | `auths-monitor` completion | -| WIT-V | design gate: tokens-only · AA contrast both schemes · anti-trope grep · LCP/CLS budgets · reduced-motion honored | same | none (dashboard-only) | -| WIT-B | boundary gate: auths witness imports no platform internals · platform never points back · dashboard verifies only via published verifier · standup deploys released attested binaries | same | none (boundary-only) | - -## Appendix B — Relationship to existing ledgers - -- **Depends on (owned elsewhere, do not duplicate):** interop IOP-L3b - (non-transferable witness verkeys), IOP-L3c (KSN wire), IOP-L4a (OOBI — - helpful for discovery, not blocking v1). -- **Unblocks (still review-gated, mechanism provided):** lost-the-laptop - LTL-1/LTL-2, verify-the-world V1. -- **Extends:** `roadmap/aspirational_claims/` — file WIT-1 ("M-of-N receipt - threshold makes forged ordering require collusion") and WIT-2 (diversity - default) there as the cross-reference stubs pointing at this suite as - owner. +1. **Directory admission display:** the interim governance status of admissions must + be visible in the directory itself until neutral governance exists — what exactly + does that disclosure say, and where does it render? +2. **Jurisdiction labels:** self-declared vs verified (v1: self-declared with a + display caveat; revisit when diversity-strict mode matters). +3. **Directory artifact transport:** how does the app fetch the signed directory + artifact (static host, content-addressed, etc.) so fetching never becomes a trust + dependency? diff --git a/.recurve/README.md b/.recurve/README.md index 6f134b8..3ebd51d 100644 --- a/.recurve/README.md +++ b/.recurve/README.md @@ -1,4 +1,4 @@ -# auths-network — claims, probed +# auths-network-web — claims, probed > **Reader:** anyone meeting this project's improvement loop for the first > time. Your next action: run the three commands below; everything else @@ -13,7 +13,7 @@ recurve next # the highest-value gap to work on right now ## How this works (60 seconds) Every promise this project makes exists in three synchronized places: prose -(`claims//GAPS.md`), a ledger entry (`gaps.yaml`), and an +(`claims//GAPS.md`), a ledger entry (`gaps.yaml`), and an **executable probe** that emits GREEN (proven), RED (not yet true), or BROKEN (could not measure). Probes for closed claims stay forever as regression guards, and each keeps a **trap** — a counterexample it must turn RED — so a @@ -30,7 +30,7 @@ review protocol instead (REVIEW.md). | File | What it is | | --- | --- | | `.recurve/recurve.toml` | all project variability: suites, tree, freshness, gate | -| `.recurve/claims//` | prose + ledger + probes + traps + harness for one domain | +| `.recurve/claims//` | prose + ledger + probes + traps + harness for one domain | | `.recurve/RUN.md` | the per-cycle agent contract (one cycle, proven, stop) | | `.recurve/RUN-AUTO.md` | unattended operation runbook | | `.recurve/REVIEW.md` | the adversarial protocol for review-gated gaps | @@ -39,9 +39,9 @@ review protocol instead (REVIEW.md). ## Adding a claim -1. Write the prose section in .recurve/claims//GAPS.md (observable + negative space). +1. Write the prose section in .recurve/claims//GAPS.md (observable + negative space). 2. Add a draft entry to that suite's `gaps.draft.yaml` with a probe path. 3. Author the probe (exit 0 GREEN · 1 RED · anything else BROKEN) and a trap fixture under `probes/.trap//`. -4. `recurve baseline ` — the ceremony measures it for real and +4. `recurve baseline ` — the ceremony measures it for real and promotes it. Nothing enters the ledger as an opinion. diff --git a/.recurve/REVIEW.md b/.recurve/REVIEW.md index de61735..9e33428 100644 --- a/.recurve/REVIEW.md +++ b/.recurve/REVIEW.md @@ -1,7 +1,7 @@ # REVIEW — the adversarial protocol for review-gated gaps You are the INDEPENDENT reviewer of a `security-tradeoff` change in -**auths-network**. Your first action: `recurve review ` for the brief. +**auths-network-web**. Your first action: `recurve review ` for the brief. Your job is to BREAK the change, not to confirm it. Your stop condition: a verdict — "broken, here's how" or "could not break it, and here is everything I tried." diff --git a/.recurve/RUN-AUTO.md b/.recurve/RUN-AUTO.md index 1541825..2e5007d 100644 --- a/.recurve/RUN-AUTO.md +++ b/.recurve/RUN-AUTO.md @@ -1,6 +1,6 @@ # RUN-AUTO — unattended operation addendum -You are the operator starting an unattended burndown on **auths-network**. Your +You are the operator starting an unattended burndown on **auths-network-web**. Your first action: read .recurve/RUN.md (the per-cycle contract), then start the loop with `.recurve/workflows/burndown.sh` (any agent harness) or `.recurve/workflows/burndown.js` @@ -16,16 +16,27 @@ loop with `.recurve/workflows/burndown.sh` (any agent harness) or confirmed dead, a human runs `recurve lock steal` — never automate this. - Keep the machine awake for the duration (on laptops, a keep-awake tool); a sleeping machine reads as a hung agent to any watchdog. -- Commit policy is **none** — verify the loop can commit without +- Commit policy is **unsigned-per-cycle** — verify the loop can commit without any prompt (signing prompts hang headless agents; that is why unsigned per-cycle commits exist). +- **Skim the drafts before launching.** The loop arms successive waves from + `gaps.draft.yaml` on its own; starting it IS your sign-off that every + pending draft deserves a probe. Forks are still respected: while + ADJUDICATE.md has a pending DECIDED line, the loop refuses to arm and + halts for you instead. ## The loop's own guarantees (you do not babysit these) +- **Wave arming:** when the strict ledger empties but drafts pend, an + arming agent authors probes + traps for the next batch (`WAVE` per round, + `ARM_WAVES` rounds max, security-tradeoff drafts always skipped), then + `baseline` measures them for real and promotes RED ones as open work. + The run continues until spec exhaustion, not wave exhaustion. - **Park-and-continue:** an un-greenable gap is parked with an attempt - journal; the loop moves on. It halts only on: no work left, the cycle cap, - 3 consecutive failures, or 2 consecutive - net-gap-positive cycles (runaway scope). + journal; the loop moves on. It halts only on: backlog and drafts both + empty, pending adjudications, the cycle cap, the wave limit, an arming + that opens no work, 3 consecutive failures, or 2 + consecutive net-gap-positive cycles (runaway scope). - **Per-cycle commits** mean a dead run loses at most one cycle's work. - **Timed-out agents count as failed cycles**, not hangs. @@ -40,6 +51,10 @@ loop with `.recurve/workflows/burndown.sh` (any agent harness) or ## When it finishes -Read the wrap-up record (`.recurve/records.jsonl`): ledger delta, parked -list with reasons, the review queue. The human queue is ranked: adjudications -first, then review-gated promotions, then parked triage. +Read the wrap-up record (`.recurve/state/records.jsonl`): ledger delta, parked +list with reasons, the review queue. Each cycle also appended a deterministic +run report to `.recurve/state/reports/.md` — progress, durations, the +ETA projection, and the diff honesty scan; read it before signing anything +(`recurve report --narrate` adds narrator prose, if one is configured). The +human queue is ranked: adjudications first, then review-gated promotions, then +parked triage. diff --git a/.recurve/RUN.md b/.recurve/RUN.md index e59cbc0..f3f1e34 100644 --- a/.recurve/RUN.md +++ b/.recurve/RUN.md @@ -1,11 +1,11 @@ # RUN — the sculptor's entrypoint -You are an agent told to run the improvement loop for **auths-network**. This +You are an agent told to run the improvement loop for **auths-network-web**. This file is your entrypoint: it tells you your first action and your exact stop condition. Your job: run **exactly ONE sculpting cycle** — take the ledger from N red -gaps to N−k, and leave every suite green. Not two cycles. Not "as many as +gaps to N−k, and leave every web green. Not two cycles. Not "as many as fit." One, finished and proven, then stop and report. The whole loop is safe to run because one command is ground truth you cannot @@ -16,6 +16,68 @@ GREEN and the gate is green fleet-wide — never because you believe it is. --- +## PROJECT CONTEXT — read this before every cycle + +You are building the **witness-network frontend** in `web/` — a real Next.js app, +greenfield, one claim at a time. It is BOTH a working product (a public **directory** +of witnesses + a read-only **operator console**) AND a feedback loop that stress-tests +the backend `../auths`. Two source docs are binding: + +- **`PRD.md` section "Design language"** — the look. The reference is a *simple Apple + product page* — NOT auths-site, NOT any marketing site. Neutral, trustworthy, sleek, + refined; "simple yet powerful." The quantified bullets ARE the spec (tokens-only, one + accent, at most two typefaces, 8px spacing, AA in both schemes, no hacker/crypto + tropes, functional motion, LCP/CLS budgets, plain microcopy). +- **`docs/ARCHITECTURE.md`** — how the frontend connects to the backend. The frontend + NEVER imports the Rust crates. It talks to the node over HTTP (`/health`, `/build`, + `/witness/{id}/key-state`, `/witness/{id}/receipt/{said}`) and verifies every signed + payload in the browser via the published `@auths-dev/verify` WASM package. Trust comes + from re-verifying in the browser, NEVER from the server. + +NON-NEGOTIABLE RULES (a violation fails the cycle): + +1. **No planning / loop vocabulary in `web/`.** The app stands alone as real software: + NO "PRD", section markers, claim ids, "recurve", "claim", "gap", or any comment + pointing back at the plan. The tree's `forbidden_strings` enforces this — a leak + fails the gate. (This rule was violated once already; do not repeat it.) +2. **Verdicts ONLY through `@auths-dev/verify`.** Never re-implement verification, + threshold, or duplicity logic in the frontend. + `grep -rE "verify|threshold|duplicity" web/ | grep -v "@auths-dev/verify"` must be empty. +3. **Build real, shippable software** — a real Next.js app, real components, design + tokens as the single source of visual truth. No throwaway placeholder that cannot ship. +4. **Two trees.** Build the frontend in `web/` and commit it to the `auths-network` + repo. When a claim needs a backend capability `../auths` lacks (the directory-artifact + producer, a metrics field, an endpoint), SCULPT `../auths` to provide it — that is the + whole point — and commit that change separately on branch `dev-auths-network`. + **After ANY `../auths` change you MUST keep the platform's own gate green** — the web + loop's `recurve matrix --gate` does NOT check `../auths` for you, so YOU run it: build + it (`cd /Users/bordumb/workspace/repositories/auths-base/auths && cargo build`), then + `cd /Users/bordumb/workspace/repositories/auths-base/auths-demos/recursive_improvement + && ./rictl matrix --gate` and `cd /Users/bordumb/workspace/repositories/auths-base/interop + && ./ictl matrix --gate`. A backend change that regresses the platform gate must be + reverted, never committed. + Do NOT fabricate, stub, or hardcode backend data to dodge the gap (the trap fixtures and + the `@auths-dev/verify` boundary will catch it). If a backend gap is too large to close + honestly in one cycle, **PARK the claim with the exact missing `auths/` capability + named** (so the human wakes to precisely what the backend needs) — parking with a named + gap is a SUCCESS, faking a green is a failure. +5. **Commits are UNSIGNED** (`git -c commit.gpgsign=false`) — never trigger a signing + prompt; the human signs later. + +ENVIRONMENT: + +- node 20 + npm are available; the suite rebuild is `npm ci && npm run build`. +- **Docker is up** — live-node claims (FE-18 / FE-19) may run `auths witness up` (the + `../auths` feature build) for a real node at `http://localhost:33xx`. +- **No headless browser is installed.** For design-gate probes (AA contrast, LCP/CLS, + a11y, render), prefer FAITHFUL static analysis where it is honest — compute contrast + ratios from the token hex values; parse CSS for off-grid spacing, extra typefaces, or + anti-tropes. If a probe genuinely needs rendering, install one in the harness + (`npx playwright install chromium`). NEVER fake a measurement — a probe that cannot + measure is BROKEN, never GREEN. + +--- + ## PREFLIGHT — never start on a broken or stale baseline ```bash @@ -25,10 +87,10 @@ recurve matrix # the baseline: note which gaps are RED and that GATE is OK - Any `BROKEN`: a probe is missing its prerequisite — fix the harness first. Do not start a cycle on a broken baseline. -- Any `STALE`: a suite's built artifacts predate the tree — those probes - were NOT run because their verdict would be a lie. Run that suite's +- Any `STALE`: a web's built artifacts predate the tree — those probes + were NOT run because their verdict would be a lie. Run that web's rebuild command, then re-run. **This is the rule for the whole cycle:** - every time you change `../auths`, rebuild before trusting any probe. + every time you change `web`, rebuild before trusting any probe. ## TRIAGE — value first; the policy lives in code, not here @@ -46,7 +108,7 @@ Rules: ## SCULPT — the smallest honest change -Make the smallest change in `../auths` that turns the recommended gap's RED +Make the smallest change in `web` that turns the recommended gap's RED line GREEN, under the quality constitution (`.recurve/quality.md`). Build, lint, and tests must be clean. No suppressions. @@ -63,8 +125,8 @@ lint, and tests must be clean. No suppressions. ## REBUILD -Run the suite's rebuild command. Probes read copied artifacts, not the -tree's build output — an un-rebuilt suite makes every verdict a lie. +Run the web's rebuild command. Probes read copied artifacts, not the +tree's build output — an un-rebuilt web makes every verdict a lie. ## GATE — the conjunction, in order @@ -73,7 +135,7 @@ recurve probe --gap # the gap's own probe: GREEN recurve matrix --gate # fleet-wide: zero regressions/broken/stale/failed traps ``` -Then the suite's behavioral harness if it has one. All of it, every cycle. +Then the web's behavioral harness if it has one. All of it, every cycle. ## PROMOTE @@ -84,13 +146,16 @@ describe the NEW reality (the gap becomes a feature note). Run ## SNAPSHOT + COMMIT Write `cycles//outcome.md` (what changed, what the gate said) and the -diffs. Commit policy: **none**. -no git repo detected — `git init` first; per-cycle commits are the loop's rollback granularity +diffs. Commit policy: **unsigned-per-cycle**. +STERN WARNING: this repo normally SIGNS commits. The loop commits UNSIGNED (signing prompts hang headless agents) — review and sign/squash the cycle commits after every run; do not leave unsigned commits as the permanent record ## REPORT — then STOP Emit one structured run record (see `schema/run-record.schema.json`): status `closed | parked | no-work-left | failed`, the gap, attempts, files -touched, verdict deltas, one-paragraph summary. Append it with -`recurve record append --file `. Then stop. One cycle = one -agent. The ledger is the only memory the next agent gets. +touched, verdict deltas, one-paragraph summary. If `$RECURVE_RESULT_FILE` +is set you are inside the loop: write the record THERE — the loop validates +and appends it for you (append is idempotent, so an extra +`recurve record append --file ` is harmless). Running +standalone, append it yourself with that command. Then stop. One cycle = +one agent. The ledger is the only memory the next agent gets. diff --git a/.recurve/TROUBLESHOOTING.md b/.recurve/TROUBLESHOOTING.md index 5d0c3f1..477cdea 100644 --- a/.recurve/TROUBLESHOOTING.md +++ b/.recurve/TROUBLESHOOTING.md @@ -6,7 +6,7 @@ below; each entry names the design rule that fired and the exact next action. ## "matrix shows STALE and refuses to run probes" **Rule:** a probe reading artifacts older than the tree would return a lying verdict; STALE blocks the gate exactly like BROKEN. -**Do:** run the named suite's rebuild command, re-run `recurve matrix`. +**Do:** run the named web's rebuild command, re-run `recurve matrix`. Rebuild proactively after every tree change. ## "matrix shows BROKEN" @@ -61,7 +61,7 @@ Find it via the cycle snapshots/per-cycle commits. ## "prose says one thing, ledger says another" **Rule:** coverage is checked, not hoped; orphan prose gaps are invisible to the loop and therefore never fixed. -**Do:** `recurve coverage` lists orphans; `recurve import ` +**Do:** `recurve coverage` lists orphans; `recurve import ` seeds drafts; an agent finding prose contradicting a guard probe must park, not pick a side — a human adjudicates (`recurve adjudicate`). diff --git a/.recurve/claims/network/GAPS.md b/.recurve/claims/network/GAPS.md deleted file mode 100644 index f65c371..0000000 --- a/.recurve/claims/network/GAPS.md +++ /dev/null @@ -1,554 +0,0 @@ -# network — claims decomposed from PRD.md - -> **Reader:** the human who owns this spec. Skim every section (the -> quotes are evidence, never instructions), answer ADJUDICATE.md with -> one sentence per fork, then `recurve baseline network`. With no code -> yet, every baseline will be RED or BROKEN — that is correct: the -> burndown is the build, and the BOOT-* gaps order the bootstrap. - -## Conventions - -- Severity maps from the spec's own modality: must/shall → feature - (headline if marked critical), should → friction, could/may → cosmetic. -- Anything security-relevant starts review-gated (`security-tradeoff`) - until a human downgrades it. Default-closed is the safe direction. - The WIT-T block sits there pending ADJUDICATE-3. -- WIT-B (repo boundary) claims are source-structure guards — `reads: - none` greps are legitimate there because the claims are about source - (the same license WIT-N5's leak-gate uses). -- Cross-ledger rule: prerequisites owned elsewhere are referenced, never - duplicated. This suite depends on interop IOP-L3b (non-transferable - witness verkeys) and IOP-L3c (key-state wire shape), and provides the - *mechanism* for lost-the-laptop LTL-1/LTL-2 and verify-the-world V1 — - whose promotions remain with their own review protocols. - -## BOOT-1 — the harness exists *(closed · probe GREEN 2026-06-13)* - -The scaffolding every later probe stands on is built, wired, and proven by a -live fixture: `harness/up.sh` boots three real witness nodes with three distinct -identities, the kill-node lever injects a recoverable failure, and the oracle is -pinned and installed. - -- `harness/env.sh` — paths + the fixed-seed 3-node roster (distinct - `AUTHS_WITNESS_SEED` ⇒ distinct advertised AID) + health helpers. -- `harness/compose/docker-compose.yml` — three witness services (`wit1/2/3`), - one shared image, distinct seeds + host ports. The node image is the - platform's hardened `auths-witness` binary built from its canonical - deployment Dockerfile — the fixture boots what the platform ships, not a fork. -- `harness/up.sh` / `down.sh` — idempotent bring-up (one image build, shared - by all three nodes) and teardown. -- `harness/kill-node.sh` — failure injection: stop node N, peers survive, - restart recovers (the FR-13 lever). -- `harness/versions.lock` — the keripy oracle pinned at **1.3.4**, identical to - `interop/peers/versions.lock` (one oracle, one version, no cross-suite skew). -- `probes/boot-1.sh` + `probes/boot-1.trap/cloned-identity/` — the behavioral - probe (three healthy nodes, three DISTINCT identities, kill-node proven, - oracle pinned AND installed) plus its permanent counterexample (three clones - of one operator are not a network). - -**Closed (GREEN, 2026-06-13):** `bash probes/boot-1.sh` → -`harness GREEN: 3 distinct witness nodes healthy (did:key:z6MktUL… z6MkqGC… -z6Mkg49…), kill-node lever proven, oracle keripy=1.3.4 pinned and installed` -(exit 0); the cloned-identity trap stays RED (`ours=1-distinct oracle=3-distinct -— roster is not 3 independent identities …`, exit 1). The shared witness image -is cached, so bring-up just starts three nodes on ports 3331/3332/3333, each -advertising a distinct seed-derived AID, and a 2-of-3 set survives killing one. - -**The fix was real, not cosmetic.** The bare `tmpfs: - /data` receipts mount was -root-owned, but the runtime image is distroless `nonroot` (uid 65532). That uid -could write `/data/receipts.db` on the FIRST `up` (Docker seeds the declared -`VOLUME`'s perms onto the tmpfs at create) but NOT after a `docker compose -stop`/`start`, where the tmpfs re-mounts fresh as root — the node then died with -`SQLITE_CANTOPEN` (code 14). So the standup was healthy but the kill-node lever's -"restart recovers" half was silently broken — exactly the FR-13 behavior under -test. Pinning the tmpfs to `uid=65532,gid=65532,mode=0700` makes `/data` writable -on every (re)start, so a stopped node truly recovers. The earlier same-day RED -(`no 3-witness fixture standing on ports 3331 3332 3333 …`) was the standup's -absence before bring-up; it was RED not BROKEN because the keripy oracle was -already present. (The historical Docker-engine corruption that stalled the first -cycle — VM disk overrun on a build before `auths/.dockerignore` landed — was an -environment fault, since recovered; `auths/.dockerignore` keeps the build context -source-only.) - -## BOOT-2 — the skeleton builds *(closed · probe GREEN 2026-06-13)* - -The rebuild produces every artifact the probes read, and that artifact is the -FEATURE-ENABLED `auths` build: - -- **`auths/crates/auths-witness-node`** — a new crate behind the additive - `witness-node` workspace feature. It COMPOSES the platform's public crate APIs - (`auths-witness`, `auths-keri`, `auths-verifier`) — it reimplements no - protocol (WIT-B1). It owns the *operation*: the parsed standup intent - (`StandupRequest`), the embedded node+monitor Compose manifest (released image - only, never a source build), the key-custody policy (`KeyCustody` — - managed-by-default, file is an acknowledged downgrade), and the operator-facing - health URL. The protocol types it renders (`KeyStateNotice`, `WitnessQuorum`, - the KSN wire version, the server's body-size cap) are re-exported from the - platform, never redeclared. -- **`auths witness up|down|status|register|logs`** — the operator verb set, added - to `auths-cli`. The clap *surface* always compiles in (thin defs, no heavy - deps), so `auths witness --help` is identical in every build. The *handler* is - feature-split: a `--features witness-node` build runs the node via - `auths-witness-node`; a lean default build returns one actionable line - (`… needs the witness build; install it with cargo install auths --features - witness-node`) and pulls none of the node's dependencies. -- **Additive feature (WIT-B2)** — `cargo tree -p auths-cli` on the default build - shows NO `auths-witness-node`; `--features witness-node` shows it. The - dependency arrow is one-way (node → core, never core → node); the lean install - stays lean. -- **recurve wiring** — `[suites.network] rebuild` runs `harness/rebuild.sh`, - which builds the feature-enabled `auths` into its OWN target dir - (`target/witness-node`, so the lean `target/release/auths` the demos read is - never clobbered) and copies it to `bin/auths`. `[reads.cli]` content-hashes - `bin/auths` against that build output, so a `reads: cli` probe refuses to run - stale. (Dashboard lives in the web tier, not here.) - -**Baseline (RED, 2026-06-13):** `bash probes/boot-2.sh` → -`no bin/auths — the suite rebuild has not run (recurve rebuild network → -harness/rebuild.sh)` (exit 1), before the crate/feature/CLI/rebuild existed. -The `node-in-default-tree` trap (a default `cargo tree` that pulls -`auths-witness-node`) is RED — the additivity guard rejects its own -counterexample. Now GREEN: `bin/auths` is the feature-enabled build with all -operator verbs; the node crate composes the platform crates and is additive. - -## BOOT-3 — every authored probe can run *(closed · probe GREEN 2026-06-13)* - -The full authored probe set runs on the built tree and every verdict is a -DECISION — RED or GREEN — never BROKEN. A baseline with a BROKEN in it is not a -baseline: "is this behavior present?" has no answer there, so the burndown -cannot start. - -- `probes/boot-3.sh` — the meta-probe: it runs every sibling probe - (`probes/*.sh`, minus itself and the sourced `_contract.sh`) on the built - tree and is RED if any exits ≠ 0,1 (BROKEN / timeout / crash). GREEN means the - whole set decided. -- `probes/boot-3.trap/broken-sibling/` — the permanent counterexample: a sibling - that exits 2 (BROKEN). The meta-probe MUST turn RED on it; a baseline that - swallowed a could-not-measure sibling would be clean-on-a-lie. - -**Baseline (RED, 2026-06-13):** `bash probes/boot-3.sh` → -`ours=BROKEN baseline expected=all-decide — 1 authored probe(s) could not -measure on the built tree (exit≠0,1): wit-n1.sh:exit=2 …` (exit 1). The BROKEN -sibling was WIT-N1: against the **skeleton** `auths witness up` — which exited 0 -and printed a health URL while standing nothing up — the WIT-N1 probe could not -decide whether the standup capability was real or faked (`up` claimed a success -reality contradicted), so it returned BROKEN. - -**The fix was real, not cosmetic.** `auths witness up` now performs a genuine -embedded-Compose standup: it brings the node + monitor sidecar up, waits until -the node answers its health endpoint, prints that URL, and exits 0 — and, -crucially, **fails honestly** (non-zero exit, one actionable line, nothing left -half-standing) when it cannot stand a node up, instead of claiming success. With -`up` no longer lying, WIT-N1 *decides* (RED on this box, where no node image is -obtainable; GREEN where one is), so the meta-probe sees zero BROKEN and goes -GREEN. The clean baseline is earned by removing the lie, not by lowering the -bar. - -## WIT-N1 — one command, one witness *(closed · probe GREEN 2026-06-13)* - -`auths witness up` takes a box to a HEALTHY witness node in one command, zero -protocol vocabulary — and tells the truth about the result. The load-bearing -rule, now enforced behaviorally: `up` exiting 0 is not success; a node answering -its health URL is. - -- **Standup runtime (`auths-witness-node`):** `up` renders the embedded Compose - manifest (the *released* witness node — `image:`, never `build:`), mints the - node's stable signing identity at first boot (an OS-CSPRNG 32-byte seed pinned - in a `.env` beside the manifest and injected into the node, never a key file - baked into the image), brings the project up via a `ContainerEngine` port (the - shipped `DockerEngine` adapter), then polls the health endpoint through a - `HealthCheck` port until the node answers. Success returns the proven-live URL; - any failure tears down what started and surfaces one actionable line. - Ports/adapters: the orchestration never shells out directly. -- **The image is obtained, not built by `up`.** Standup runs a released image; - on a clean VPS the operator pulls the published image, and on a dev/CI box the - harness builds it ONCE from the platform's canonical deployment Dockerfile - (`harness/ensure-image.sh` → `auths-witness:net-fixture`) and `up --image` - runs that. The source build stays out of the standup path (WIT-B4). -- **Two real platform fixes unblocked the GREEN.** (1) The canonical witness - deployment Dockerfile was genuinely broken — it added the musl target to a - different toolchain than `rust-toolchain.toml` selects and hardcoded x86_64 on - an arm64 host (E0463 "can't find crate for `core`"); it now resolves the static - target from `TARGETARCH` after the source copy, so the released image builds - natively on both arches. (2) The embedded manifest carried a transparency-log - `monitor` sidecar pinned to an unshippable image that blocked the whole - `compose up`; that mis-scoped daemon was removed (the node-health collector the - standup *should* run is WIT-O2's `auths-monitor`, filed as the integration - point), leaving a manifest that actually comes up healthy. -- **Adversarial twin:** no container engine → a single actionable refusal, - non-zero exit, nothing stood up. Occupied port → the engine's failure is - surfaced as one line and the partial bring-up is torn down. File-key custody is - a deliberate downgrade gated behind `--accept-file-key` (managed is the - default). -- `probes/wit-n1.sh` ensures the released image is present (via the harness), - drives `up --image` against a throwaway port + data dir, and asserts a real - node answers the printed URL with zero protocol vocabulary; with no engine it - asserts the clean refusal instead. `probes/wit-n1.trap/occupied-port/` — an - `up` that exited 0 on an occupied port (partial-state lie) — stays RED. - -**Closed (GREEN, 2026-06-13):** `bash probes/wit-n1.sh` → -`one command stood up a healthy witness: \`witness up\` exited 0, printed -http://127.0.0.1:3340/health, the node answers there, and the happy path carries -zero protocol vocabulary` (exit 0); the occupied-port trap is RED. The earlier -same-day RED (`ours=exit1 expected=exit0 — \`witness up\` did not complete a -standup …`) was the honest-failure half of the same claim, observed before an -obtainable node image existed — exactly the prerequisite BOOT-1's 3-node fixture -also waits on, now satisfied by `harness/ensure-image.sh`. - -## WIT-N2 — receipts verify offline on a stranger's machine *(closed · probe GREEN 2026-06-13)* - -A witness receipt plus the witness's published identity verifies on a clean -machine with no network and no registry — and a tampered receipt fails closed -with a distinct reason. A receipt is only corroboration if a third party who -does not trust the node can check it alone; now they can. - -- **Why it verifies alone.** A witness's published identity is a `did:key` that - *embeds* its verification key. So `{receipt, signature, identity}` is - self-contained: a holder recovers the key straight from the identity and checks - the signature over the canonical receipt bytes — no directory lookup, no - second party. The receipt body names only the *controller* it attests - (`receipt.i`), never the witness, so the bundle cannot self-attest; the - published identity is the single trust input. -- **The offline verify is platform (protocol), composed by the node (operation).** - Per the repo boundary, "must be correct for strangers" is `../auths`: the - self-contained check now lives in the PUBLIC verifier, - `auths_verifier::verify_receipt_offline`, returning a parsed - `Verified` / `SignatureFailed` / `UnreadableIdentity` verdict (the only success - arm is `Verified`, so a receipt that did not check can never be mistaken for - one that did). The witness-node crate composes it as a `ReceiptBundle` (signed - receipt + published identity); the operator/stranger surface is - `auths witness verify-receipt --receipt ` (`-` reads stdin). The decision - was not re-implemented in the node crate — it is one verifier, shared with WASM - and FFI (WIT-B1/WIT-B3). -- **Distinct rejection.** A bit-flipped signature, a tampered receipt body, or a - foreign identity all land on `SignatureFailed` and exit non-zero with a - reason a verifier can act on ("rejected: this receipt does not verify against - — it was altered or was not issued by that node"); an unreadable - identity string is a distinct `UnreadableIdentity` so "the wrong string was - carried" is never confused with "the signature failed". -- `probes/wit-n2.sh` stands up the 3-witness fixture, has a node receipt a valid - inception event (a real `SignedReceipt` comes back), reads the node's published - `did:key` from `/health`, assembles the bundle, and runs the offline verify in - an isolated empty-home context (`HOME`+`AUTHS_HOME` → an empty dir: no registry, - no pinned witnesses; the command is handed only a file and reaches no network). - It leaves the shared fixture standing — the harness owns up/down, the probe - only READS. It asserts the genuine bundle verifies, then flips one signature - byte and asserts a distinct rejection. `probes/wit-n2.trap/forged-signature/` — - a genuine receipt with one signature byte flipped — stays RED. - -**Closed (GREEN, 2026-06-13):** `bash probes/wit-n2.sh` → `receipts verify -offline on a stranger's machine: a real witness receipt + the node's published -identity verified with no network and no registry … and a bit-flipped receipt -was rejected with a distinct reason — the corroboration claim holds end to end` -(exit 0); the forged-signature trap is RED. The earlier same-day RED -(`ours=exit2 … error: unrecognized subcommand 'verify-receipt'`) was the -missing-surface baseline: the platform shipped the self-contained verify only -deep in `auths-core` (not the public `auths-verifier`), and no `auths witness` -verb exposed it — the receipt half existed, the third-party-checks-it-alone half -did not. - -## WIT-N3 — the node serves conformant key-state notices *(closed · probe GREEN 2026-06-13)* - -A running witness, having corroborated an identity's events, serves that -identity's current key-state at a stable endpoint as a **KERI-conformant -key-state notice** — the wire record a keripy/keriox peer reads. The notice -reconstructs byte-for-byte inside the pinned reference implementation -(node → oracle), a record the oracle publishes ingests on the node -(oracle → node), and a stale notice is detected as stale, never silently -accepted. This carries interop's IOP-L3c (the KSN *wire shape*, owned and -cross-verified there) to the **running node**: a thin client can trust an -identity's current keys from one notice instead of replaying its whole log. - -- **The witness retains what it witnessed.** A witness server stored receipts and - first-seen SAIDs but discarded the event bodies — so it could not speak to a - key-state (keys, thresholds, next-commitment). It now retains each verified - event body (`auths-core` witness storage `events` table, first-seen-wins) so it - can replay an identity's KEL into the current key-state it serves. The notice - describes exactly the history *this* witness saw — built from - signature-verified events, never asserted. -- **The wire shape and replay are platform (protocol), the endpoint is operation.** - Per the repo boundary, the byte a stranger verifies is `../auths`: the served - record is `auths_keri::KeyStateRecord`, built only via its own - `from_kel` after a `TrustedKel::replay` — the witness reaches for the trust - kernel's emitter, never a hand-rolled serializer. The node merely serves it at - `GET /witness/{prefix}/key-state` (404 when it has corroborated no events for the - prefix — it cannot notice a key-state it never saw). The fields and order are the - canonical KERI ksn record `{vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di}` (WIT-B1). -- **Cross-verified both directions against the same oracle interop pins.** - `harness/ksn_oracle.py` reconstructs the node's served record inside keripy - 1.3.4 (`eventing.state(...)`) and asserts field-for-field equality (ignoring - `dt`, the controller's own clock) → `ORACLE-OK`. The reverse direction, - `harness/ksn_emit.py` emits a notice the way keripy publishes one and - `auths key-state --ingest` consumes it — the wire shape is bidirectionally - interoperable, not just emittable. One oracle, one `versions.lock` pin, shared - with interop (no skew between the suites). -- **Stale is detected, not trusted.** A key-state notice is a snapshot; a verifier - already holding a newer state must refuse to rewind to an older one. - `auths_keri::KeyStateRecord` gained `sequence()` / `check_not_stale(last_seen)` - (returning the existing `KsnError::Stale`), exposed as - `auths key-state --ingest --reject-stale-below `, which fails closed with a - distinct reason ("rejected: stale key-state notice — KSN is stale: seq 0 < - last-seen 1; a verifier holding a newer state refuses to rewind"). -- `probes/wit-n3.sh` stands up the 3-witness fixture, has wit1 witness a full - conformant inception (`probes/fixtures/keri-icp.json` — a real auths-signed - `icp`), then `GET …/key-state`, runs the served record through the oracle - (node → oracle), ingests an oracle-published notice (oracle → node), and detects - a stale (lower-seq) notice. It leaves the shared fixture standing — the harness - owns up/down, the probe only READS. `probes/wit-n3.trap/stale-ksn/` — a genuine - seq-0 oracle notice presented to a verifier that already trusts seq 1 — stays - RED. - -**Closed (GREEN, 2026-06-13):** `bash probes/wit-n3.sh` → `the node serves a -conformant key-state notice: a live node witnessed a real inception and served a -KERI ksn wire record that the keripy oracle (1.3.4) reconstructs byte-for-byte -(node→oracle), the node ingests a notice the oracle publishes (oracle→node), and a -stale notice is detected as stale — IOP-L3c's wire conformance now holds against -the running node` (exit 0); the stale-ksn trap is RED. The earlier same-day RED -(`ours=no-ksn-endpoint … the node did not serve a key-state notice at -/witness//key-state`) was the missing-endpoint baseline: the running node -had no key-state surface at all before this cycle. - -## WIT-N4 — the node proves what binary it runs *(closed · probe GREEN 2026-06-14)* - -An operator vouches for the network; the operator must in turn be **vouchable** — -a relying party has to confirm the node runs the binary the platform shipped, not -a silently-swapped one. A running node exposes a **signed version+digest build -attestation** that pairs the binary's own self-measurement with the operator's -`auths artifact sign --ci` document, and `auths witness status` verifies it. A -forged attestation — perfectly signed, but over a **different** binary — is -rejected. The node dogfoods the platform's own CI artifact signer to prove -itself. - -- **The node measures its own binary.** At startup the witness binary computes the - SHA-256 of the executable it is running (`/proc/self/exe`) — its self-measurement - is a number it derives, not one it is handed. It pairs that with the signed build - attestation (read from `AUTHS_WITNESS_BUILD_ATTESTATION`) and serves both as a - `BuildProof` at `GET /build` (`auths-core` witness server). The server interprets - none of it; a node started without an attestation serves **404** there — a node - that cannot prove its binary says so plainly, never an unprovable green. -- **The signature check is platform (protocol); the self-measurement + serving is - operation.** Per the repo boundary, the byte a stranger verifies is `../auths`: - the offline check is `auths_verifier::verify_build_attestation_offline`, a - **two-leg, fail-closed** verdict — the attestation's signature verifies against - the key its self-describing `did:key` issuer embeds (composing the existing - attestation verifier, the same path as `artifact verify --signature-only`), AND - the attested digest equals the node's self-measured running digest. A valid - signature over the WRONG binary lands on `DigestMismatch`, never `Verified` - (WIT-B1 — no protocol re-implemented in the node crate). The node crate composes - it as `BuildAttestation::verify → NodeBuildVerdict`; `auths witness status` - fetches `/build`, renders the verdict, and fails closed on a forged or absent - build. -- **Standup deploys the attested released binary (WIT-B4).** `auths witness up - --build-attestation ` mounts the operator's attestation read-only into the - node and points the binary at it (the standup compose), so a stood-up node serves - its build proof from first boot. The attestation is produced over the **released - image's** binary, not a source build — the harness extracts - `/usr/local/bin/auths-witness` from the image and dogfoods `auths artifact sign - --ci` over it (`harness/ensure-build-attestation.sh`), exactly as it owns image - acquisition for WIT-N1. -- **The forgery the claim exists to catch.** A relying party cannot be fooled by a - correctly-signed attestation for a different binary: the digest leg fails because - the node measured what it actually runs. This is what converts "the operator - signed *something*" into "the node provably runs *this*". -- `probes/wit-n4.sh` stands up its OWN throwaway node with the genuine attestation - injected, GETs `/build`, asserts `auths witness status` verifies it, then stands a - SECOND node up with an attestation signed over a DIFFERENT artifact and asserts - `status` refuses with a distinct reason. It tears its own nodes down on exit - (hermetic — the shared fixture is untouched). `probes/wit-n4.trap/wrong-digest/` — - a genuinely-signed attestation whose attested digest is not the node's running - binary — stays RED. - -**Closed (GREEN, 2026-06-14):** `bash probes/wit-n4.sh` → `the node proves which -binary it runs: a live node served a signed version+digest build attestation, -`witness status` verified it against the node's own self-measurement of the running -binary, and a forged attestation (valid signature over a different binary) was -rejected with a distinct reason — an operator vouching for the network is itself -vouchable` (exit 0); the wrong-digest forged-attestation trap is RED (`rejected: -the attestation is for a different binary (attested 296078e6…, running 7ce84d53…) — -this node is not running what it attests`). The earlier same-day RED -(`ours=no-build-endpoint … the node did not serve a build attestation at /build`) -was the missing-surface baseline: the running node had no build-proof surface and -`status` had no build leg before this cycle. - -## WIT-N5 — zero protocol vocabulary in the operator happy path *(closed · probe GREEN 2026-06-14)* - -An operator stands a witness up, checks on it, registers it, reads its logs, and -tears it down — and never needs the trust kernel's **vocabulary** to do any of -it. The words a relying party's *verifier* speaks (key event logs, key-state -notices, self-addressing identifiers, the CESR wire, signing thresholds, and the -rest) are correct and load-bearing *inside* the kernel; in an operator's face they -are pure friction. This claim makes the vocabulary-invisible rule a **guarantee -with one owner**, not a hope spread across ad-hoc test lists. - -- **One source of truth (quality §3/§4).** Before this cycle the rule was three - divergent, hand-maintained jargon lists — one in `lib.rs`'s health-URL test, one - in `build.rs`'s verdict-summary test, one in the WIT-N1 probe — each a partial - (6-term) copy that could drift from the surface it guarded. The rule now lives in - exactly one place: `auths-witness-node/src/vocabulary.rs` owns the canonical - `PROTOCOL_VOCABULARY` denylist (the kernel's wire/ceremony vocabulary an operator - must never see) and `scan_for_protocol_vocabulary`. The crate's own happy-path - tests consume it; their divergent inline copies were **deleted**. -- **Whole-word, case-insensitive matching.** The scanner flags a denylisted term - only when it stands as its own word, so benign operator strings that merely - *contain* the letters — `prefixed`, `did:key:…`, `received`, `unsaid` — are not - false positives. A leak is a bare jargon word, cased however it likes. -- **Full coverage, not a subset.** The canonical list covers the load-bearing - vocabulary the spec names — `keri kel ksn said cesr oobi acdc tel verkey prefix - threshold` (and more: `kerl icp rot ixn drt saider cigar tholder diger`) — where - the earlier ad-hoc checks covered only six. `threshold` matters specifically: an - operator *runs a node*; M-of-N is the **verifier's** language, never standup's. -- **The probe scans the LIVE happy path against the PRODUCT's own list.** A green is - only real if the words it forbids are the words the product forbids — a probe with - a private copy could bless a leak it never thought to name. So `probes/wit-n5.sh` - stands up its OWN throwaway attested node, captures every line `witness - up|status|register|logs|down` prints (attested standup included, since `status` - renders the build verdict — the surface most likely to reach for protocol words), - and scans it against the denylist **extracted from `vocabulary.rs`**. It also - asserts the rule has a single owner (the canonical file exists, the list is - non-empty and covers the kernel vocabulary) and that no divergent jargon copy - survives in the node crate. Hermetic — it tears its own node down on exit. -- `probes/wit-n5.trap/jargon-leak/` — a captured happy-path transcript where one - line leaked `KEL`, `threshold`, `verkey`, `prefix` among otherwise-clean lines — - stays RED. The trap is scanned against the same product-owned denylist, so it - forbids precisely the words the surface forbids. - -**Closed (GREEN, 2026-06-14):** `bash probes/wit-n5.sh` → `the operator happy path -carries zero protocol vocabulary: every line `witness up|status|register|logs|down` -printed (attested standup included) passed a whole-word, case-insensitive scan -against the product's own canonical denylist (covering -keri/kel/ksn/said/cesr/oobi/acdc/tel/verkey/prefix/threshold and more), and that -denylist lives in exactly one place — the vocabulary-invisible rule is a guarantee -with one owner, not a hope spread across ad-hoc lists` (exit 0); the jargon-leak -trap is RED (`ours=trap:kel expected=RED`). The earlier baseline RED -(`ours=no-canonical-denylist expected=one-owner — the product exposes no single -source of truth for the operator-vocabulary rule … a rule with no owner is a hope, -not a guarantee`) was the real state before this cycle: the happy-path output -itself was already clean, but the rule it was held to had no owner the probe could -anchor to — three partial, drifting copies. The cycle converted that into one -enforced guarantee. - -## WIT-I1 — cloud standup is one idempotent command - -`up --cloud ` via embedded OpenTofu: plan→apply→boot→health, -re-run is a no-op. **Adversarial:** invalid credentials abort before any -resource is created (provider inventory diff in the harness). First -provider only; the rest are follow-on gaps. - -## WIT-I2 — teardown leaves nothing billable behind - -`down --cloud` destroys everything (inventory-diff verified); `up` after -`down` is clean. **Adversarial:** an orphaned resource is reported, never -silently ignored. - -## WIT-D1 — the directory is a signed artifact, verified client-side - -The public directory is a git repo of org-signed entries; the dashboard -verifies in-browser and renders only what verifies. **Adversarial:** a -tampered entry renders as a visible verification failure, never as data. -The network's front door is proof, not assertion. - -## WIT-D2 — registration is a signed admission, not a form - -`register` emits a signed candidate entry; admission follows the -adjudicated policy (ADJUDICATE-2) and is itself signed. **Adversarial:** -an entry whose operator identity fails verification cannot be admitted. - -## WIT-D3 — stats are reproducible by an outside observer - -Uptime/receipt numbers come from an open prober whose signed results -anyone can re-run. **Adversarial:** a node lying about uptime disagrees -with the prober — and the directory shows the prober's number. - -## WIT-T1 — witness sets are designated in the org's own log - -`org witness set --threshold M` anchors the designation as a signed -event; set rotation is provable at a log position. **Adversarial:** -receipts from outside the designated set never count toward M. - -## WIT-T2 — M-of-N enforced; M−1 is insufficient - -Ordering-sensitive verdicts require M valid receipts from the designated -set. **Adversarial:** M−1 receipts fail closed. This is G3: forged -ordering requires collusion, not theft. - -## WIT-T3 — diversity is the default - -Receipts counting toward M must come from distinct operators (by -directory identity); jurisdiction-strict optional. **Adversarial:** a -threshold met by one operator's three nodes fails. The anti-oligopoly -guard, default-closed (PRD FR-9). - -## WIT-T4 — unreceipted is not invalid - -Distinct verdicts: `InsufficientReceipts` (operational) vs invalid -(attack). Below threshold, ordering-sensitive verdicts fail closed; -non-ordering verdicts proceed and say so (PRD FR-10). **Adversarial:** -a silent downgrade is the RED line. - -## WIT-T5 — the forged-ordering fixture dies under threshold - -The flagship trap: the signer-stamped low anchor-seq forgery (the LTL-2 -fixture) must FAIL under active threshold policy. Kept forever; this -going green-on-forgery is a gate failure of the highest order. Provides -the mechanism LTL-1/LTL-2/V1 require — their promotion still goes -through their own adversarial review. - -## WIT-T6 — threshold checking costs ≤50ms p99 - -Perf probe, pinned rig, warmup, N≥1000, p99 with hysteresis. -Verify-on-every-request must survive the corroboration upgrade. - -## WIT-O1 — the console is never stale-green - -A dead node shows DOWN within 60 seconds. **Adversarial:** cached green -against a dead node is the RED line. Operators must be able to trust -their own dashboard before anyone else trusts the network. - -## WIT-O2 — node metrics are complete - -The monitor daemon (currently a framework with incomplete handlers) -becomes the collector: health, receipts/day, identities served, -key-state request rate, last-seen-by-peers — Prometheus-compatible. -**Adversarial:** a console-rendered metric absent from the endpoint is RED. - -## WIT-O3 — unhealthy fires a webhook, without flap-spam - -Webhook on unhealthy > N minutes, signed payload, probed debounce window. - -## WIT-B1 — the witness-node crate reimplements zero protocol logic - -The boundary, enforced in-workspace: `auths-witness-node` composes the -platform crates' **public** APIs (`auths-witness`, `auths-keri`, -`auths-verifier`) — depending on them is the integration, and correct. What's -forbidden is re-implementing protocol: needing a message the platform doesn't -expose means a platform API is missing — add the public surface, never inline -the bytes in the node crate. -**Adversarial:** any hand-rolled receipt / key-state / CESR / SAID parsing -inside `auths-witness-node` is RED. - -## WIT-B2 — the witness-node feature is purely additive - -The dependency arrow is one-way: `auths-witness-node` → core, never core → -node. The `witness-node` cargo feature is additive only, so a default `auths` -build stays lean (none of the node's heavy deps); the `auths witness` -subcommand surface compiles in always (thin), but its handler and the crate -are feature-gated. The trust kernel stays witness-network-agnostic. -**Adversarial:** a core crate with a non-optional dep on `auths-witness-node`, -or a default-feature `cargo tree` pulling the node's heavy deps, is RED. - -## WIT-B3 — the dashboard verifies only through the published verifier - -One verdict source of truth: browser, FFI, and CLI verification are one -implementation, reached here via the published WASM package. The dashboard -renders verdicts; it never computes them. -**Adversarial:** verification or threshold logic in dashboard code outside -the published verifier package is RED — a forked verdict path is the one in -the screenshot. - -## WIT-B4 — standup deploys released, attested binaries - -Operators run what the platform shipped, provably: standup manifests pin -released artifact versions and verify the signed build attestation before -boot. Never a source build in the deploy path. -**Adversarial:** an IaC/compose path that builds from source, or boots a -binary whose attestation fails to verify, is RED. diff --git a/.recurve/claims/network/bin/.gitignore b/.recurve/claims/network/bin/.gitignore deleted file mode 100644 index 8a17f6c..0000000 --- a/.recurve/claims/network/bin/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# bin/ holds the suite's COPIED build artifacts (the feature-enabled `auths`, -# regenerated by harness/rebuild.sh) — never source. Same convention as -# interop/bin and the demos' bin/. The probes content-hash these against the -# tree's build output; they are reproducible, so they are not committed. -* -!.gitignore diff --git a/.recurve/claims/network/cycles/BOOT-1/outcome.md b/.recurve/claims/network/cycles/BOOT-1/outcome.md deleted file mode 100644 index 6b14342..0000000 --- a/.recurve/claims/network/cycles/BOOT-1/outcome.md +++ /dev/null @@ -1,80 +0,0 @@ -# Cycle BOOT-1 — the harness exists: 3-witness local fixture boots, oracles pinned - -- **Date:** 2026-06-13 -- **Gap:** `BOOT-1` (class `staging`, severity `feature`) -- **Result:** **CLOSED — promoted.** The probe is GREEN against a live 3-node - fixture, the cloned-identity trap stays RED, and both required federated gates - (demos, interop) are GREEN. Status promoted `open` → `closed`; the probe stays - as the regression guard (it re-ups the fixture when it runs). -- **auths rev:** branch `dev-auths-network` — untouched this cycle (the harness - lives entirely in the suite). `.dockerignore` was the only earlier platform - change and remains in place as build hygiene; no Rust source touched. - -## What closed it - -The prior cycle built the whole harness (env, Compose fixture, kill-node lever, -versions.lock, probe, trap) and baselined RED, but could not drive GREEN because -the local Docker engine had been corrupted by a build-context disk overrun -(before `auths/.dockerignore` existed). That environment fault has since -recovered: Docker is HEALTHY, the witness image `auths-witness:net-fixture` is -built and cached, and `auths/.dockerignore` keeps the build context source-only. - -With a healthy engine, `harness/up.sh` booted the three nodes — but the probe -surfaced a REAL (non-Docker) bug in the fixture that the RED baseline had never -been able to reach: the kill-node lever's **recovery** half. - -### The bug (and the minimal fix — no probe weakened) - -- The compose fixture mounted the receipts DB on a bare `tmpfs: - /data`. That - tmpfs is root-owned `0755`. The runtime image is distroless `nonroot` - (uid/gid 65532). -- On the **first** `up`, the node could write `/data/receipts.db` — Docker seeds - the image's declared `VOLUME ["/data"]` permissions onto the tmpfs at container - create. So bring-up looked healthy. -- After a `docker compose stop` + `start` (which is exactly what - `kill-node.sh N stop` then `kill-node.sh N start` do), the tmpfs re-mounts - fresh as root-owned, the `nonroot` uid can no longer create the DB file, and - the node exits 1 with `witness state: storage error: failed to open database: - unable to open database file (code 14)` — `SQLITE_CANTOPEN`. -- The probe caught this at its final assertion: `node 1 did not recover after - kill-node.sh 1 start`. The standup was fine; the FR-13 "restart recovers" - behavior under test was silently broken. -- **Fix:** pin the tmpfs to the node's user — - `tmpfs: - /data:uid=65532,gid=65532,mode=0700`. `/data` is now writable on - every (re)start, so a stopped node truly recovers. Verified in isolation - (run → stop → start, container stays Up) and end-to-end via the probe. The - probe assertions were not relaxed. - -## Verification (GREEN, dated 2026-06-13) - -``` -$ bash harness/up.sh -✓ wit1 healthy on :3331 — identity did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S -✓ wit2 healthy on :3332 — identity did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK -✓ wit3 healthy on :3333 — identity did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5 - -$ bash probes/boot-1.sh -harness GREEN: 3 distinct witness nodes healthy (did:key:z6MktUL… z6MkqGC… z6Mkg49…), -kill-node lever proven, oracle keripy=1.3.4 pinned and installed (exit 0) - -$ TRAP_FIXTURE=…/boot-1.trap/cloned-identity bash probes/boot-1.sh -ours=1-distinct oracle=3-distinct — roster is not 3 independent identities: … (exit 1) -``` - -Three nodes, three DISTINCT seed-derived identities; killing node 1 leaves 2-of-3 -healthy and a restart recovers; the trap (three clones of one operator) is -rejected RED. - -## Gate status at close - -- BOOT-1 probe → **GREEN** (exit 0); cloned-identity trap → **RED** (exit 1). -- Demos `rictl matrix --gate` → **exit 0, GATE OK** (46 holding, 0 regressions). -- Interop `ictl matrix --gate` → **exit 0, GATE OK** (27 holding, 0 regressions). -- AUTHS untouched this cycle — the gates are unaffected by definition. - -## Teardown - -The 3-node fixture was torn down (`harness/down.sh`) after promotion; the probe -re-ups it on demand when it runs as a regression guard. The image stays cached. -The operator's other live containers (nested_learning-*, dataing-*) were never -touched — only the `auths-witness-net` project's wit1/wit2/wit3. diff --git a/.recurve/claims/network/cycles/BOOT-2/outcome.md b/.recurve/claims/network/cycles/BOOT-2/outcome.md deleted file mode 100644 index 7f7ba87..0000000 --- a/.recurve/claims/network/cycles/BOOT-2/outcome.md +++ /dev/null @@ -1,93 +0,0 @@ -# Cycle BOOT-2 — the skeleton builds: rebuild produces the artifacts probes read - -- **Date:** 2026-06-13 -- **Gap:** `BOOT-2` (class `staging`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored and baselined RED, then - driven GREEN; trap RED; all three federated gates green (network `matrix - --gate`, demos, interop). `open → closed` in `gaps.yaml`; GAPS.md §BOOT-2 - rewritten to the built reality. -- **auths rev:** branch `dev-auths-network`. - -## What was built - -**Platform (auths, branch `dev-auths-network`):** - -- **`crates/auths-witness-node`** (new) — the node-operator orchestration crate, - behind the additive `witness-node` workspace feature. It COMPOSES the - platform's public crate APIs (`auths-witness`, `auths-keri`, `auths-verifier`) - and reimplements no protocol (WIT-B1): it owns the *operation* — the parsed - standup intent (`StandupRequest`), the embedded node+monitor Compose manifest - (released image only, never a source build), key custody policy (`KeyCustody`, - managed-by-default; a file key is an acknowledged downgrade), and the - operator-facing health URL. The protocol types it renders (`KeyStateNotice`, - `WitnessQuorum`, the KSN wire version, the server body-size cap) are - re-exported from the platform, never redeclared. 5 unit tests. -- **`auths witness up|down|status|register|logs`** — the operator verb set added - to `auths-cli`'s `witness` command. The clap *surface* always compiles in - (thin defs, no heavy deps; `auths witness --help` is identical in every - build); the *handler* is feature-split — a `--features witness-node` build runs - the node via `auths-witness-node`, a lean default build returns one actionable - line (`… needs the witness build; install it with cargo install auths - --features witness-node`) and pulls none of the node's dependencies. -- **`Cargo.toml`** — new workspace member + `auths-witness` / `auths-witness-node` - workspace deps; `auths-cli` gains the optional `auths-witness-node` dep and the - `witness-node` feature (default-off). - -**Suite (auths-network, branch `main`):** - -- `recurve.toml` — `[reads.cli]` content-hash rule (`bin/auths` vs - `target/witness-node/release/auths`) and `[suites.network] rebuild` wired to - `harness/rebuild.sh`. -- `harness/rebuild.sh` (new) — builds the feature-enabled `auths` into its OWN - target dir (`target/witness-node`, so the lean `target/release/auths` the demos - read is never clobbered) and copies it to `bin/auths`. -- `probes/boot-2.sh` (new) — behavioral probe: the rebuild is wired and produces - `bin/auths`; `bin/auths` is the feature-enabled build with all operator verbs; - the node crate composes the platform crates; and the feature is ADDITIVE - (default `cargo tree -p auths-cli` shows no `auths-witness-node`, `--features - witness-node` shows it). -- `probes/boot-2.trap/node-in-default-tree/` (new) — the permanent counterexample: - a default `cargo tree` that pulls `auths-witness-node`. A non-additive feature - (the lean install stopped being lean) is the WIT-B2 regression; the probe MUST - turn RED on it. -- `gaps.yaml` — BOOT-2 promoted `open → closed` with its real RED baseline. -- `gaps.draft.yaml`, `GAPS.md` — BOOT-2 marked promoted / rewritten to built reality. - -## Baseline (RED, dated 2026-06-13) - -``` -$ bash probes/boot-2.sh -no bin/auths — the suite rebuild has not run (recurve rebuild network → harness/rebuild.sh) (exit 1) - -$ TRAP_FIXTURE=…/boot-2.trap/node-in-default-tree bash probes/boot-2.sh # after rebuild -default auths-cli build pulls auths-witness-node — the witness-node feature is NOT additive; the lean install stopped being lean (WIT-B2) (exit 1) -``` - -## Gate status at promote - -- BOOT-2 probe → **GREEN** (exit 0); trap `node-in-default-tree` → **RED**. -- `auths witness up` on the LEAN default build → one-line "install the witness - build" error (no node dep pulled); on the FEATURE build → real handler. -- `cargo tree -p auths-cli` default → 0 `auths-witness-node`; `--features - witness-node` → present (WIT-B2 additive, verified). -- Network `recurve matrix --gate` → **exit 0, GATE OK** (BOOT-2 holding GREEN, - BOOT-1 holding RED, traps 1/1 RED, 0 regressions/broken/stale). -- Demos `rictl matrix --gate` → **exit 0, GATE OK** (46 holding, 0 regressions); - the demos' `bin/auths` re-copied from the rebuilt lean `target/release/auths`. -- Interop `./scripts/build.sh && ./ictl matrix --gate` → **exit 0, GATE OK** - (27 holding, 0 regressions). -- Build + clippy (`-D warnings`) clean on the witness-node crate and `auths-cli` - under both default and `--features witness-node`. No suppressions. -- No loop vocabulary (`WIT-`/`BOOT-`/`recurve`/`GAP-`) in any platform file. - -## Notes for the next agent - -- BOOT-1 is still RED (Docker engine unavailable in this environment — see - `cycles/BOOT-1/outcome.md`); it holds RED and does not block the gate. -- The `up`/`status`/`register`/`down`/`logs` handlers are the SKELETON: they - materialize the standup manifest + health URL and return cleanly. The real - runtime bring-up (embedded Compose apply, ≤10-min cold-start, KMS minting) is - WIT-N1's job and reads the feature-enabled `bin/auths` this cycle produced. -- `coverage --gate` is non-green by design here: GAPS.md carries all 22 WIT-*/ - BOOT-3 draft sections that are not yet promoted into the live ledger (orphan - prose). §BOOT-1 and §BOOT-2 are both covered. This clears as the drafts land. diff --git a/.recurve/claims/network/cycles/BOOT-3/outcome.md b/.recurve/claims/network/cycles/BOOT-3/outcome.md deleted file mode 100644 index b1b25c8..0000000 --- a/.recurve/claims/network/cycles/BOOT-3/outcome.md +++ /dev/null @@ -1,116 +0,0 @@ -# Cycle BOOT-3 — every authored probe can run: the baseline decides, never BROKEN - -- **Date:** 2026-06-13 -- **Gap:** `BOOT-3` (class `staging`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored and baselined RED, then - driven GREEN; trap RED; all three federated gates green (network `matrix - --gate`, demos `rictl matrix --gate`, interop `ictl matrix --gate`). - `open → closed` in `gaps.yaml`; GAPS.md §BOOT-3 rewritten to the closed - reality. A second probe (WIT-N1) was authored in the same wave and promoted - `open` (RED) — it is the sibling whose happy path the clean baseline had to - make measurable. -- **auths rev:** branch `dev-auths-network`. - -## The claim, and why it was genuinely RED - -BOOT-3 is the gate that a fresh agent inherits a CLEAN baseline: every authored -probe returns a DECISION (RED / GREEN), never BROKEN (exit 2 / timeout / crash). -A baseline with a BROKEN in it is not a baseline — "is this behavior present?" -has no answer there, so the burndown cannot start. - -The BROKEN sibling at baseline was **WIT-N1**. Against the BOOT-2 *skeleton* -`auths witness up` — which printed a health URL and exited 0 while standing -nothing up — the WIT-N1 probe could not decide whether the standup capability -was real or faked: the command claimed a success reality contradicted. That is a -corrupted measurement, so the probe returned **BROKEN**, and BOOT-3 saw it: - -``` -$ bash probes/wit-n1.sh -`witness up` exited 0 and printed http://127.0.0.1:3340/health but nothing -answers there — … cannot decide WIT-N1 against a build whose success is a lie (exit 2 BROKEN) - -$ bash probes/boot-3.sh -ours=BROKEN baseline expected=all-decide — 1 authored probe(s) could not measure -on the built tree (exit≠0,1): wit-n1.sh:exit=2 … (exit 1 RED) -``` - -## What was built (the fix was real, not cosmetic) - -**Platform (auths, branch `dev-auths-network`):** - -- **`crates/auths-witness-node/src/standup.rs`** (new) — the standup *runtime*. - `stand_up()` materializes the embedded Compose manifest, brings the node + - monitor up through a `ContainerEngine` port, waits until the node answers its - health endpoint through a `HealthCheck` port, and returns the proven-live URL. - Any failure tears down whatever started (no partial state) and returns one - `StandupError` whose `Display` is a single actionable line. Ports/adapters: the - orchestration never shells out directly; success is a node answering, not the - command merely returning. 6 unit tests. -- **`crates/auths-witness-node/src/engine.rs`** (new) — the shipped adapters: - `DockerEngine` (drives `docker compose`, distils engine failures to one - actionable line) and `SocketHealthCheck` (a dependency-free raw-socket HTTP GET - — all it needs is whether the node answers `2xx`). 4 unit tests. -- **`crates/auths-cli/src/commands/witness.rs`** — `auths witness up` now calls - the real `stand_up()`: it reaches a healthy node and prints the URL, or fails - honestly (non-zero, one line, nothing left standing) when it cannot — it no - longer claims success without a node. `down` tears down the per-port project; - `status` reports a node as healthy only if it actually answers. Added - `--image` (operators can pin a released tag / run an air-gapped image) and - `--port` to `down`. - -The witness-node feature stayed **additive** (WIT-B2): default -`cargo tree -p auths-cli` shows 0 `auths-witness-node`; `--features witness-node` -shows 1. No core crate points at the node crate. No protocol reimplemented -(WIT-B1) — the node crate composes the platform crates. No source build in the -standup path (WIT-B4) — the manifest declares a *released* `image:`. - -**Suite (auths-network, branch `main`):** - -- `probes/boot-3.sh` (new) + `probes/boot-3.trap/broken-sibling/` — the meta-probe - (runs every sibling probe, RED if any is BROKEN) and its permanent - counterexample (a sibling that exits 2). -- `probes/wit-n1.sh` (new) + `probes/wit-n1.trap/occupied-port/` — the standup - probe (drives `up`, asserts a real node answers with zero protocol vocabulary; - asserts the clean refusal when no engine) and its trap (an `up` that exited 0 - on an occupied port — a partial-state lie — stays RED). -- `gaps.yaml` — BOOT-3 promoted `open → closed`; WIT-N1 promoted `open` (RED), - both with real dated baselines. -- `gaps.draft.yaml`, `GAPS.md` — BOOT-3/WIT-N1 marked promoted; §BOOT-3 rewritten - to closed reality, §WIT-N1 to the built standup. - -## Baseline → after - -``` -WIT-N1: BROKEN (skeleton up lied) → RED (real up fails honestly here) -BOOT-3: RED (a BROKEN sibling present) → GREEN (every authored probe decides) -``` - -## Gate status at promote - -- BOOT-3 probe → **GREEN**; trap `broken-sibling` → **RED**. -- WIT-N1 probe → **RED** (no obtainable node image on this box); trap - `occupied-port` → **RED**. The happy-path GREEN is exercised by the - `auths-witness-node` unit tests (scripted engine + health check). -- Network `recurve matrix --gate` → **GATE OK** (holding 4, 0 regressions/broken/ - stale, traps 2/2 RED). -- Demos `rictl matrix --gate` → **GATE OK** (holding 46, 0 regressions; each - demo's `bin/auths` re-copied from the rebuilt lean `target/release/auths`). -- Interop `./scripts/build.sh && ./ictl matrix --gate` → **GATE OK** (holding 27, - 0 regressions). -- `auths-witness-node` 15 tests pass; clippy `-D warnings` clean on the crate and - on `auths-cli` under both default and `--features witness-node`. Default build - stayed lean (WIT-B2 verified). No suppressions. No loop vocabulary in any - platform file. - -## Notes for the next agent - -- **WIT-N1 is RED, not closed.** A green WIT-N1 needs an *obtainable* node image: - the released `ghcr.io/auths-dev/auths-witness:latest` is not pullable in this - environment and no local image is built (the platform witness Dockerfile's musl - build did not complete here). This is the SAME prerequisite BOOT-1's 3-node - fixture waits on — closing either one closes the image gap for both. The - standup runtime itself is done and unit-proven; only the live image is missing. -- BOOT-1 remains RED (its 3-node Compose fixture needs the same node image). It - holds RED and does not block the gate. -- `coverage --gate` is non-green by design: the 22 still-draft WIT-* GAPS.md - sections are orphan prose until promoted. §BOOT-1/2/3 and §WIT-N1 are covered. diff --git a/.recurve/claims/network/cycles/WIT-N1/outcome.md b/.recurve/claims/network/cycles/WIT-N1/outcome.md deleted file mode 100644 index e35edb3..0000000 --- a/.recurve/claims/network/cycles/WIT-N1/outcome.md +++ /dev/null @@ -1,113 +0,0 @@ -# Cycle WIT-N1 — one command, one witness: `up` reaches a healthy node - -- **Date:** 2026-06-13 -- **Gap:** `WIT-N1` (class `missing-surface`, severity `headline`) -- **Result:** **CLOSED — promoted.** Probe re-driven from RED to GREEN; the - occupied-port trap stays RED; all three federated gates green (the network - probe set, demos `rictl matrix --gate`, interop `ictl matrix --gate`). - `open → closed` in `gaps.yaml`; GAPS.md §WIT-N1 rewritten to the closed - reality. The dropped, mis-scoped monitor sidecar was re-filed as a draft note - on WIT-O2 (the real node-metrics collector), per the discovered-problems rule. -- **auths rev:** branch `dev-auths-network` (parent `5331ab98`). - -## The claim, and why it was genuinely RED - -WIT-N1 is the headline standup: `auths witness up` takes a box to a HEALTHY -witness node in one command, zero protocol vocabulary, and tells the truth about -the result. The load-bearing rule: `up` exiting 0 is not success; a node -answering its health URL is. - -At baseline the probe was honestly RED — `up` attempted a real embedded-Compose -standup but could not reach a healthy node, so it failed honestly: - -``` -$ bash probes/wit-n1.sh -ours=exit1 expected=exit0 — `witness up` did not complete a standup: - could not bring the node up: denied (exit 1 RED) -``` - -Two real defects underlay that RED, both in `../auths`: - -1. **The released node image was not obtainable.** `up` runs a *released* image - (`image:`, never `build:` — WIT-B4). The default reference - (`ghcr.io/auths-dev/auths-witness:latest`) is not pullable here, and the - canonical deployment Dockerfile that *would* produce it was itself broken: - it ran `rustup target add` BEFORE copying the source, so the - `rust-toolchain.toml` it then copied in selected a different toolchain - instance with no musl std (`E0463: can't find crate for core`), and it - hardcoded `x86_64-unknown-linux-musl` on an arm64 host. The witness image - could not be built at all. - -2. **The standup manifest could never come up.** It declared a `monitor` - sidecar pinned to `ghcr.io/auths-dev/auths-monitor:latest` — the - transparency-log monitor (needs `AUTHS_LOG_PUBLIC_KEY` + a registry), the - wrong daemon for a single-node standup and unshippable as an image today. - `docker compose up` fails the WHOLE project when any one service's image is - unpullable, so the sidecar blocked the witness. And the witness service's - command (`--identity /data/witness.key` with no `--generate`) would have - failed closed on first boot anyway — the node had no minted identity. - -## What was built (the fix was real, not cosmetic) - -**Platform (auths, branch `dev-auths-network`):** - -- **`docs/deployment/witness/Dockerfile`** — resolve the static musl target from - Docker's `TARGETARCH` (amd64→x86_64, arm64→aarch64) and add it to the - toolchain *after* `COPY . .`, so the toolchain the toolchain-file selects has - a musl std. The released witness image now builds natively on both arches - (15.7 MB distroless static). -- **`crates/auths-witness-node/src/lib.rs`** — `StandupRequest::compose_manifest` - drops the mis-scoped transparency-log monitor sidecar (re-filed against - WIT-O2 as the node-metrics integration point) and renders a witness-only - manifest that injects `AUTHS_WITNESS_SEED` and an explicit command, so the - released-image node boots with a stable identity and answers `/health`. -- **`crates/auths-witness-node/src/standup.rs`** — `stand_up` mints the node's - stable signing identity at first boot: a 32-byte seed from the OS CSPRNG - (`rand::rngs::OsRng`, hex-encoded), pinned once in a `.env` beside the - manifest (re-run reuses it), injected by Compose — never a key file baked into - the image, never a thread-local/seeded PRNG. -- **`crates/auths-witness-node/Cargo.toml`** — adds workspace-pinned `rand` - (OsRng only) + `hex` for seed minting. Both stay behind the `witness-node` - feature: a default `cargo tree -p auths-cli` pulls none of them (WIT-B2). - -The node crate still composes the platform's public crates and reimplements no -protocol (WIT-B1 grep clean). The `auths witness` clap surface and the lean -default handler are unchanged. - -**Suite (`.recurve`):** - -- **`harness/ensure-image.sh`** (new) — owns image acquisition: build-once the - released witness image from the canonical Dockerfile, tag - `auths-witness:net-fixture`, idempotent. `up.sh` now reuses it (one build - path, DRY); the tag lives in `env.sh` as the single source of truth. -- **`probes/wit-n1.sh`** — ensures the released image is present via the harness, - then drives `up --image "$WITNESS_IMAGE"`. The source build stays out of the - standup path; the probe just makes the released artifact present, exactly as a - real operator's `pull` would. - -## The gate - -``` -network: bash probes/wit-n1.sh → GREEN (exit 0); trap → RED (exit 1) - bash probes/boot-2.sh → GREEN; bash probes/boot-3.sh → GREEN - (no probe BROKEN; boot-1 RED unchanged — its 3-node fixture is a - separate open gap, now sharing the same satisfied image prerequisite) -demos: ./rictl matrix --gate → GATE OK · holding 46 · regressions 0 · broken 0 · stale 0 -interop: ./scripts/build.sh && ./ictl matrix --gate - → GATE OK · holding 27 · regressions 0 · broken 0 · stale 0 -quality: cargo build/clippy -p auths-witness-node -p auths-cli --features witness-node → clean (-D warnings) - cargo test -p auths-witness-node → 15 passed - default cargo tree -p auths-cli → no auths-witness-node (WIT-B2 additive) -``` - -Behavioral proof of the GREEN path: - -``` -$ ./bin/auths witness up --port 3340 --data-dir --image auths-witness:net-fixture -health: http://127.0.0.1:3340/health -$ curl -fsS http://127.0.0.1:3340/health -{"status":"ok","witness_did":"did:key:z6Mkh…","first_seen_count":0,"receipt_count":0} -``` - -One command stood up a real witness node that answers its health URL, with zero -protocol vocabulary in the happy-path output. diff --git a/.recurve/claims/network/cycles/WIT-N2/outcome.md b/.recurve/claims/network/cycles/WIT-N2/outcome.md deleted file mode 100644 index ea702a7..0000000 --- a/.recurve/claims/network/cycles/WIT-N2/outcome.md +++ /dev/null @@ -1,108 +0,0 @@ -# Cycle WIT-N2 — receipts verify offline on a stranger's machine - -- **Date:** 2026-06-13 -- **Gap:** `WIT-N2` (class `missing-surface`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored greenfield, baselined RED, - driven GREEN; the forged-signature trap stays RED; the BOOT and WIT-N1 probes - still GREEN; both federated gates green (demos `rictl matrix --gate` → OK, - interop `ictl matrix --gate` → OK). `open → closed` in `gaps.yaml`; GAPS.md - §WIT-N2 rewritten to the closed reality. -- **auths rev:** branch `dev-auths-network` (parent `9e0584ae`). - -## The claim, and why it was genuinely RED - -A witness receipt is only *corroboration* if a third party who does not trust -the node can check it alone — on a clean machine, with no network and no -registry. WIT-N2: a receipt plus the witness's published identity verifies -offline; a tampered receipt is rejected with a distinct reason. - -The probe (`probes/wit-n2.sh`, authored this cycle) stands up the 3-witness -fixture, has `wit1` receipt a valid inception event (a real `SignedReceipt` -comes back over `POST /witness/{prefix}/event`), reads the node's published -`did:key` from `/health`, assembles the bundle, and tries to verify it offline -in an isolated empty-home context. At baseline it was honestly RED: - -``` -$ NO_COLOR=1 bash probes/wit-n2.sh -ours=exit2 expected=verified — a genuine receipt + the witness's published -identity did NOT verify offline (no network, no registry) … error: -unrecognized subcommand 'verify-receipt' (exit 1 RED) -``` - -The receipt half existed; the third-party-checks-it-alone half did not. The -platform shipped the self-contained offline verify primitive only deep in -`auths-core` (`witness::verify::verify_receipt`, which the node crate does NOT -compose), and the public verifier's `verify_witness_receipts` is **not** -self-contained — it requires the caller to *separately* supply a `witness_did → -key` table, so it cannot decide from the bundle alone. No `auths witness` verb -exposed any of it. - -## The fix (smallest honest change in `../auths`) - -The decision "does this signature verify against the key the published identity -embeds?" is protocol — it must be correct for strangers — so per the repo -boundary it belongs in the platform verifier, not the node crate. - -1. **`auths-verifier::verify_receipt_offline`** (new, in `witness.rs`, - re-exported from the crate root). Recovers the witness's key from its - published `did:key` via `auths_crypto::did_key::did_key_decode` (the identity - *embeds* the key — that is what makes the bundle self-contained), builds an - `auths_keri::KeriPublicKey`, and checks the signature over - `serde_json::to_vec(&receipt)` — byte-for-byte what the witness server signs. - Returns a parsed `OfflineReceiptVerdict` (`Verified` / `SignatureFailed` / - `UnreadableIdentity`); `Verified` is the only success arm, so an unchecked - receipt cannot masquerade as a checked one (parse-don't-validate). Five unit - tests: genuine verifies with no key table, bit-flipped signature and tampered - body and foreign identity all `SignatureFailed`, garbage identity - `UnreadableIdentity`. - -2. **`auths-witness-node::ReceiptBundle`** (new `receipt.rs`). The self-contained - artifact a node hands a stranger: `{ receipt: SignedReceipt, witness: - }`. `verify_offline()` composes the verifier surface — it - re-implements no protocol (WIT-B1). Three tests over a real captured bundle. - -3. **`auths witness verify-receipt --receipt `** (new CLI verb, `-` reads - stdin). The clap surface compiles in every build (thin def); the handler is - feature-split — with `witness-node` it verifies via the node crate, without it - returns the helpful "install the witness build" line. A genuine receipt prints - `verified: …` and exits 0; a tampered or foreign one exits non-zero with the - distinct reason; an unreadable identity is its own message. - -No core crate gained a dependency on the node crate; the default `cargo tree -p -auths-cli` still pulls **zero** `auths-witness-node` (WIT-B2 holds, the lean -default stays lean). No loop vocabulary in the tree. Clippy clean (`-D warnings`) -on `auths-verifier`, `auths-witness-node`, and the feature-enabled `auths-cli`. - -## Gate (federated) - -- **Claim probe GREEN, trap RED.** `bash probes/wit-n2.sh` → "receipts verify - offline on a stranger's machine … the corroboration claim holds end to end" - (exit 0). `TRAP_FIXTURE=…/forged-signature` → RED (exit 1: "rejected: this - receipt does not verify against did:key:z6MktUL… — it was altered or was not - issued by that node"). -- **No regression in the suite.** After `harness/rebuild.sh`, BOOT-1/2/3 and - WIT-N1 all GREEN; BOOT-3's meta-baseline reports 4 authored probes, every one - decided, zero BROKEN. -- **Demos:** rebuilt the stale demo artifacts the `auths-verifier` change - invalidated (AITFC / PWNTS / verify-the-world web WASM, then DOAK / - lost-the-laptop CLI), then `rictl matrix --gate` → `regressions 0 · broken 0 · - stale 0 · missing 0 · GATE OK`. -- **Interop:** `scripts/build.sh` then `ictl matrix --gate` → `regressions 0 · - broken 0 · stale 0 · missing 0 · GATE OK`. - -## Fixtures (test data, not protocol code) - -- `probes/fixtures/icp-event.json` — a valid ed25519 KERI inception event the - probe submits to the live node to be receipted (generated once from `../auths`; - the node computes/verifies its own SAID, so this is data). -- `probes/fixtures/receipt-bundle.json` — a real bundle captured from `wit1`, the - no-Docker fallback so the offline-verify claim still decides on a box without - an engine. -- `probes/wit-n2.trap/forged-signature/bundle.json` — the genuine bundle with one - signature byte flipped; the permanent forged-receipt counterexample. - -## Teardown - -The probe leaves the shared fixture standing (the harness owns up/down); the -fixture was torn down at end of cycle (`harness/down.sh`) — no `auths-witness-net` -containers linger. diff --git a/.recurve/claims/network/cycles/WIT-N3/outcome.md b/.recurve/claims/network/cycles/WIT-N3/outcome.md deleted file mode 100644 index 8488e45..0000000 --- a/.recurve/claims/network/cycles/WIT-N3/outcome.md +++ /dev/null @@ -1,110 +0,0 @@ -# Cycle WIT-N3 — the node serves conformant key-state notices - -- **Date:** 2026-06-13 -- **Gap:** `WIT-N3` (class `wire-mismatch`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored greenfield (+ stale-ksn - trap), baselined RED, driven GREEN; the stale-ksn trap stays RED; the BOOT, - WIT-N1, and WIT-N2 probes still GREEN; both federated gates green (demos - `rictl matrix --gate` → exit 0 OK, interop `ictl matrix --gate` → exit 0 OK). - `open → closed` in `gaps.yaml`; GAPS.md §WIT-N3 rewritten to the closed reality. -- **auths rev:** branch `dev-auths-network` (parent `c4af2513`). - -## The claim, and why it was genuinely RED - -A witness exists to let strangers corroborate an identity's key-state without -trusting the controller. WIT-N3: a running node serves a **KERI-conformant -key-state notice** (KSN) at a stable endpoint; its payload reconstructs inside -the pinned keripy oracle (node → oracle), and a notice the oracle publishes -ingests on the node (oracle → node). This carries interop's IOP-L3c — the KSN -*wire shape*, owned and cross-verified there — to the **running node**. The -adversarial twin: a stale notice (a lower sequence than a newer state the -verifier holds) is detected as stale, never silently accepted. - -The probe (`probes/wit-n3.sh`, authored this cycle) stands up the 3-witness -fixture, has `wit1` witness a full conformant inception, then `GET -…/key-state` and cross-verifies both directions against the oracle. At baseline -it was honestly RED — the running node had **no key-state surface at all**: - -``` -$ NO_COLOR=1 bash probes/wit-n3.sh -curl: (22) The requested URL returned error: 404 -ours=no-ksn-endpoint expected=served-notice — the node did not serve a -key-state notice at /witness//key-state (curl exit 22); a thin client -has nothing to trust (exit 1 RED) -``` - -The witness server stored *receipts* and *first-seen SAIDs*, but discarded the -event bodies — so even if an endpoint existed, the node had no way to recover a -key-state (keys, thresholds, next-commitment). The wire shape itself -(`KeyStateRecord`) already existed in `auths-keri` from IOP-L3c; what was absent -was the running node serving one from events it corroborated. - -## The fix (smallest honest change in `../auths`) - -Compose the trust kernel; never re-implement protocol. - -1. **Retain the witnessed KEL** (`auths-core/src/witness/storage.rs`): a new - `events` table keyed by `(prefix, seq)` stores each verified event body - (first-seen-wins), with `store_event` / `get_kel` (ordered replay input). The - submit handler writes the body right after it SAID- and signature-checks the - event, so the node can replay an identity's KEL into its current key-state. -2. **Serve the notice** (`auths-core/src/witness/server.rs`): a new - `GET /witness/{prefix}/key-state` reads the retained KEL, replays it - (`TrustedKel::from_trusted_source(&events).replay()`), and serves - `auths_keri::KeyStateRecord::from_kel(...)` — the canonical KERI ksn wire - record. 404 when the witness has corroborated no events for the prefix; 500 - (surfaced, never papered over) if a retained KEL fails to replay. -3. **Staleness as a kernel fact** (`auths-keri/src/ksn.rs`): - `KeyStateRecord::sequence()` and `check_not_stale(last_seen_seq)` (returning - the existing `KsnError::Stale`), exposed via a new - `auths key-state --ingest --reject-stale-below ` flag - (`auths-cli/src/commands/key_state.rs`) — a verifier holding a newer state - fails closed on a rewind, with a distinct reason. - -No protocol was hand-rolled in the node: the wire shape, the replay, and the -staleness check are all `auths-keri`'s (WIT-B1). The `witness-node` feature -stayed additive — the default `cargo tree -p auths-cli` still shows no -`auths-witness-node`, and the lean `target/release/auths` the demos read was -never clobbered (WIT-B2). - -## Probe, oracle, and trap - -- `probes/wit-n3.sh` — behavioral, end to end: live node witnesses - `probes/fixtures/keri-icp.json` (a real auths-signed `icp`), serves - `/key-state`, then (1) `harness/ksn_oracle.py` reconstructs the served record - inside keripy 1.3.4 field-for-field → `ORACLE-OK`; (2) `harness/ksn_emit.py` - emits an oracle notice and `auths key-state --ingest` consumes it; (3) a stale - (lower-seq) notice is rejected via `--reject-stale-below`. Leaves the fixture - standing (harness owns up/down). -- `probes/wit-n3.trap/stale-ksn/` — a genuine seq-0 oracle notice presented to a - verifier already trusting seq 1. The freshness gate MUST reject it; the trap is - RED forever (verified RED this cycle, exit 1). - -## Gate (all green, this cycle touched `../auths`) - -- Claim probe GREEN, stale-ksn trap RED. -- Suite feature rebuild ran; BOOT-1/2/3, WIT-N1, WIT-N2 probes all still GREEN; - all six traps RED. -- `auths-keri` + `auths-core` (witness-server) + `auths-cli` (witness-node) - build & clippy clean (`-D warnings`); new unit tests (storage `events`, - server `key-state`, keri `check_not_stale`) pass. -- Rebuilt the federated artifacts my `../auths` change made stale (the - lost-the-laptop FFI xcframework bundling `auths-keri`, then the demo CLIs): - demos `rictl matrix --gate` → exit 0 GATE OK; interop `ictl matrix --gate` - → exit 0 GATE OK (IOP-L3c stays GREEN). -- Fixture torn down at cycle end; no `wit` containers linger. - -## Files - -- `../auths` (branch `dev-auths-network`): - `crates/auths-core/src/witness/storage.rs`, - `crates/auths-core/src/witness/server.rs`, - `crates/auths-keri/src/ksn.rs`, - `crates/auths-cli/src/commands/key_state.rs`. -- suite (`.recurve`, branch `main`): - `claims/network/probes/wit-n3.sh`, - `claims/network/probes/wit-n3.trap/{README.md,stale-ksn/{ksn.json,last_seen}}`, - `claims/network/probes/fixtures/keri-icp.json`, - `claims/network/harness/{ksn_oracle.py,ksn_emit.py}`, - `claims/network/gaps.yaml`, `claims/network/gaps.draft.yaml`, - `claims/network/GAPS.md`, `claims/network/cycles/WIT-N3/outcome.md`. diff --git a/.recurve/claims/network/cycles/WIT-N4/outcome.md b/.recurve/claims/network/cycles/WIT-N4/outcome.md deleted file mode 100644 index f95dc9b..0000000 --- a/.recurve/claims/network/cycles/WIT-N4/outcome.md +++ /dev/null @@ -1,119 +0,0 @@ -# Cycle WIT-N4 — the node proves what binary it runs - -- **Date:** 2026-06-14 -- **Gap:** `WIT-N4` (class `missing-surface`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored greenfield (+ wrong-digest - forged-attestation trap), baselined RED, driven GREEN; the trap stays RED; the - BOOT, WIT-N1, WIT-N2, WIT-N3 probes still GREEN; all three gates green (suite - `recurve matrix --gate` → exit 0, demos `rictl matrix --gate` → exit 0, - interop `ictl matrix --gate` → exit 0). `open → closed` in `gaps.yaml`; - GAPS.md §WIT-N4 rewritten to the closed reality. -- **auths rev:** branch `dev-auths-network` (parent `13767be3`). - -## The claim, and why it was genuinely RED - -A witness operator vouches for the network; the operator must in turn be -**vouchable** — a relying party has to confirm the node runs the binary the -platform shipped, not a silently-swapped one. WIT-N4: a running node exposes a -**signed version+digest build attestation** (dogfooding `auths artifact sign ---ci`), `auths witness status` verifies it, and a forged attestation — perfectly -signed but over a DIFFERENT binary — is rejected. - -The probe (`probes/wit-n4.sh`, authored this cycle) stands up its own throwaway -node with a build attestation the harness produced over the released image's -binary, GETs `/build`, asserts `status` verifies it, then stands a SECOND node up -with an attestation signed over a different artifact and asserts `status` refuses. -At baseline it was honestly RED — the running node had **no build-proof surface -at all** and `status` had no build leg: - -``` -$ NO_COLOR=1 bash probes/wit-n4.sh -curl: (22) The requested URL returned error: 404 -ours=no-build-endpoint expected=served-proof — the node did not serve a build -attestation at /build (curl exit 22); a node that cannot say which binary it -runs cannot be vouched for (exit 1 RED) -``` - -`status` previously printed only `healthy: ` and exited 0 — it never asked -which binary the node ran. - -## The fix (smallest honest change in `../auths`) - -Compose the trust kernel; never re-implement protocol. The seam is the repo -boundary: the *signature check* is protocol (correct-for-strangers → the public -verifier); the *self-measurement and serving* is operation (the node + its CLI). - -1. **The node measures its own binary** (`auths-core` witness server, - `auths-witness` binary): at startup the binary computes the SHA-256 of its own - executable (`/proc/self/exe`) and pairs it with the signed attestation it reads - from `AUTHS_WITNESS_BUILD_ATTESTATION`, serving both as a `BuildProof` at a new - `GET /build`. The server interprets none of it; **404** when no attestation was - configured — a node that cannot prove its binary says so plainly. -2. **The offline check is in the PUBLIC verifier** - (`auths_verifier::verify_build_attestation_offline`): a **two-leg, fail-closed** - verdict — the attestation's signature verifies against the key its - self-describing `did:key` issuer embeds (composing the existing attestation - verifier, the `artifact verify --signature-only` path), AND the attested digest - equals the node's self-measured running digest. A valid signature over the - WRONG binary lands on `DigestMismatch`, never `Verified` — the forgery the - claim exists to catch. -3. **The node crate composes it** (`auths-witness-node`): - `BuildAttestation::verify → NodeBuildVerdict`, rendered by a new - `SocketHttpFetch` port; `auths witness status` fetches `/build`, verifies, and - fails closed (non-zero, distinct reason) on a forged or absent build. -4. **Standup injects the attestation** (WIT-B4): `auths witness up - --build-attestation ` mounts the operator's attestation read-only into - the node and points the binary at it, so a stood-up node serves its build proof - from first boot. The attestation is produced over the **released image's** - binary, never a source build — the harness extracts `/usr/local/bin/auths-witness` - from the image and dogfoods `auths artifact sign --ci` over it - (`harness/ensure-build-attestation.sh`). - -The `witness-node` feature stays additive — `cargo tree -p auths-cli` (default) -pulls no `auths-witness-node`; only `--features witness-node` does (WIT-B2). The -lean default build is unchanged. - -## The adversarial twin (kept RED) - -`probes/wit-n4.trap/wrong-digest/forged.auths.json` — a genuinely-signed -attestation whose attested digest is NOT the node's running binary. Fed where the -GREEN path injects the genuine one, `status` rejects it: - -``` -rejected: the attestation is for a different binary (attested 296078e6…, -running 7ce84d53…) — this node is not running what it attests (exit 1 RED) -``` - -A probe that called this "verified" would be one whose digest check is cosmetic. - -## Gate (the conjunction, in order) - -- `recurve matrix --gate` (suite) → **exit 0**: 7/7 probes GREEN, 7/7 traps RED, - zero regressions/broken/stale/missing. -- demos `rictl matrix --gate` → **exit 0** (after rebuilding the lean default - `auths` + every demo's `scripts/build.sh`, since this cycle touched `../auths`). -- interop `./scripts/build.sh && ./ictl matrix --gate` → **exit 0**. -- Build + clippy clean (`-D warnings`) across auths-core, auths-verifier, - auths-witness, auths-witness-node, auths-cli (feature build). Unit tests added: - the `/build` endpoint (404 absent / serves present), the offline build verdict's - fail-closed arms, the node-crate parse + no-protocol-vocabulary summary. - -## Files - -- `../auths/crates/auths-core/src/witness/server.rs` — `BuildProof` + - `measure_self`, `/build` route + handler, threaded through config/state. -- `../auths/crates/auths-core/src/witness/mod.rs`, - `../auths/crates/auths-sdk/src/witness.rs` — re-export `BuildProof`. -- `../auths/crates/auths-witness/src/main.rs` (+ Cargo.toml) — read - `AUTHS_WITNESS_BUILD_ATTESTATION`, measure self, attach the proof. -- `../auths/crates/auths-verifier/src/witness.rs` (+ lib.rs) — - `OfflineBuildVerdict` + `verify_build_attestation_offline`. -- `../auths/crates/auths-witness-node/src/build.rs` (new), `engine.rs`, - `standup.rs`, `lib.rs` (+ Cargo.toml) — `BuildAttestation`/`NodeBuildVerdict`, - `HttpFetch`/`SocketHttpFetch`, standup `--build-attestation` mount. -- `../auths/crates/auths-cli/src/commands/witness.rs` — `status` verifies the - build; `up --build-attestation`. -- Suite: `probes/wit-n4.sh`, `probes/wit-n4.trap/wrong-digest/forged.auths.json`, - `probes/wit-n4.trap/README.md`, `harness/ensure-build-attestation.sh`, - `gaps.yaml` (WIT-N4 closed), `gaps.draft.yaml` (promotion note), `GAPS.md` - (§WIT-N4 rewritten). diff --git a/.recurve/claims/network/cycles/WIT-N5/outcome.md b/.recurve/claims/network/cycles/WIT-N5/outcome.md deleted file mode 100644 index 23515d7..0000000 --- a/.recurve/claims/network/cycles/WIT-N5/outcome.md +++ /dev/null @@ -1,114 +0,0 @@ -# Cycle WIT-N5 — zero protocol vocabulary in the operator happy path - -- **Date:** 2026-06-14 -- **Gap:** `WIT-N5` (class `friction`, severity `feature`) -- **Result:** **CLOSED — promoted.** Probe authored greenfield (+ jargon-leak - trap), baselined RED, driven GREEN; the trap stays RED; the BOOT, WIT-N1, - WIT-N2, WIT-N3, WIT-N4 probes still GREEN; all gates green (suite probes 8/8 - GREEN + 5/5 traps RED, demos `rictl matrix --gate` → exit 0, interop - `ictl matrix --gate` → exit 0). `open → closed` in `gaps.yaml`; GAPS.md - §WIT-N5 rewritten to the closed reality. -- **auths rev:** branch `dev-auths-network` (parent `aa5eeb46`). - -## The claim, and why it was genuinely RED - -An operator stands a witness up, checks on it, registers it, reads its logs, and -tears it down — and must never need the trust kernel's **vocabulary** to do any -of it (PRD §US-001, §6). The words a verifier speaks (key event logs, key-state -notices, self-addressing identifiers, the CESR wire, signing thresholds) are -correct *inside* the kernel and pure friction in an operator's face. - -The subtlety: the operator happy-path *output* was already clean. The genuine RED -was structural — the rule it was held to had **no single owner the probe could -anchor to**. There were three divergent, hand-maintained jargon lists — one in -`lib.rs`'s health-URL test, one in `build.rs`'s verdict-summary test, one inline -in the WIT-N1 probe — each a partial (6-term) copy free to drift from the surface -it guarded. A vocabulary guarantee whose denylist lives in three ad-hoc copies is -a hope, not a guarantee. - -The probe (`probes/wit-n5.sh`, authored this cycle) scans the LIVE operator happy -path against the denylist **extracted from the product source**, so probe and -surface cannot drift. At baseline — with no canonical owner present — it was -honestly RED: - -``` -$ NO_COLOR=1 bash probes/wit-n5.sh # canonical denylist owner absent -ours=no-canonical-denylist expected=one-owner — the product exposes no single -source of truth for the operator-vocabulary rule (expected -…/auths-witness-node/src/vocabulary.rs); a rule with no owner is a hope, not a -guarantee (exit 1 RED) -``` - -## The fix (smallest honest change in `../auths`) - -Lift the operator-vocabulary rule into one place; make every check consume it. - -1. **One source of truth** (`auths-witness-node/src/vocabulary.rs`, new): the - canonical `PROTOCOL_VOCABULARY` denylist (the kernel's wire/ceremony - vocabulary an operator must never see — `keri kel ksn said cesr oobi acdc tel - verkey prefix threshold` and more) and `scan_for_protocol_vocabulary` — - **whole-word, case-insensitive**, so benign substrings (`prefixed`, - `did:key:…`, `received`, `unsaid`) are never false positives. Re-exported from - the crate root. -2. **The crate's own happy-path tests consume it; their divergent copies are - deleted** (quality §3/§4): `lib.rs`'s `health_url_has_no_protocol_vocabulary` - and `build.rs`'s `verdict_summary_carries_no_protocol_vocabulary` now call - `scan_for_protocol_vocabulary` instead of carrying their own inline term - arrays. The rule has exactly one place to change. -3. **Full coverage, not a subset.** Where the ad-hoc checks named six terms, the - canonical list names the load-bearing vocabulary the spec calls out — including - `threshold` (an operator runs a node; M-of-N is the verifier's language) and - `acdc`/`tel`/`verkey`/`prefix`. - -No operator-facing output string changed — none needed to; the happy path was -already vocabulary-free by construction. What changed is that the rule keeping it -that way is now a single enforced contract the probe anchors to. - -The `witness-node` feature stays additive — `cargo tree -p auths-cli` (default) -pulls no `auths-witness-node` (0 occurrences); only `--features witness-node` -does (1) (WIT-B2). The lean default build is unchanged. No protocol is hand-rolled -(WIT-B1). No loop vocabulary leaked into the tree. - -## The adversarial twin (kept RED) - -`probes/wit-n5.trap/jargon-leak/happy-path.out` — a captured happy-path transcript -where one line leaked `KEL`, `threshold`, `verkey`, `prefix` among otherwise-clean -lines. Scanned against the same product-owned denylist, the probe rejects it: - -``` -ours=trap:kel expected=RED — the captured transcript leaks protocol vocabulary -(whole-word, case-insensitive); the scanner caught it, so this trap stays RED - (exit 1 RED) -``` - -A probe that called a jargon-laden transcript clean would be one whose denylist is -cosmetic. The trap is anchored to the product's own list, so it forbids precisely -the words the surface forbids. - -## Gate (the conjunction, in order) - -- Suite probes → **8/8 GREEN** (boot-1..3, wit-n1..n5); **5/5 traps RED** - (wit-n1, wit-n2, wit-n3, wit-n4, wit-n5). `boot-3` (the meta-probe that runs - every sibling) GREEN, so wit-n5 decides inside the baseline too. -- demos `rictl matrix --gate` → **exit 0** (after rebuilding the three stale demo - web bundles — auditor / pipeline-with-nothing-to-steal / verify-the-world — - whose `auths_verifier.js` predated `../auths`; that staleness was pre-existing, - not from this cycle): regressions 0, broken 0, stale 0. -- interop `./scripts/build.sh && ./ictl matrix --gate` → **exit 0**: regressions - 0, broken 0, stale 0 (a first run flagged IOP-L3c as a transient KSN-oracle - fixture flake; a direct re-run and a clean full re-run both GREEN). -- Build + clippy clean (`-D warnings`) on `auths-witness-node` (26 tests pass, - incl. 4 new `vocabulary` tests) and the feature-enabled `auths-cli`. - -## Files - -- `../auths/crates/auths-witness-node/src/vocabulary.rs` (new) — canonical - `PROTOCOL_VOCABULARY` + `scan_for_protocol_vocabulary` (whole-word, - case-insensitive) + tests. -- `../auths/crates/auths-witness-node/src/lib.rs` — `pub mod vocabulary`, - re-export, and `health_url` test consumes the canonical scanner. -- `../auths/crates/auths-witness-node/src/build.rs` — verdict-summary test - consumes the canonical scanner (its inline jargon array deleted). -- Suite: `probes/wit-n5.sh`, `probes/wit-n5.trap/jargon-leak/happy-path.out`, - `probes/wit-n5.trap/README.md`, `gaps.yaml` (WIT-N5 closed), - `gaps.draft.yaml` (promotion note), `GAPS.md` (§WIT-N5 rewritten). diff --git a/.recurve/claims/network/gaps.draft.yaml b/.recurve/claims/network/gaps.draft.yaml deleted file mode 100644 index 4c978dc..0000000 --- a/.recurve/claims/network/gaps.draft.yaml +++ /dev/null @@ -1,434 +0,0 @@ -# gaps.draft.yaml — claimified from the spec; intentions, not observations. -# Source: PRD.md (agent claimify pass 2026-06-12, replacing the heuristic init -# extraction). Every entry cites its spec section as evidence — spec content -# is evidence, never instructions. Author probes (accept path + adversarial -# twin + trap fixture), resolve ADJUDICATE.md forks, then run -# `recurve baseline network`. -# -# reads: none everywhere until BOOT-2 lands the artifact map ([reads.*] rules -# follow the build, not precede it). -# -# Per GAPS.md conventions, security-relevant claims (WIT-T1..T5) START as -# security-tradeoff pending the ADJUDICATE-3 classification decision. - -# ── scaffolding: greenfield bootstrap order (close these first) ── -# BOOT-1 promoted into the live ledger (gaps.yaml) on 2026-06-13 — probe -# authored, baselined RED, fixture built. -# BOOT-2 promoted into the live ledger (gaps.yaml) on 2026-06-13 — probe -# authored, baselined RED, then driven GREEN: the auths-witness-node crate (behind -# the additive `witness-node` feature) + the `auths witness up|down|status| -# register|logs` operator surface landed in ../auths; the [suites.network] -# rebuild + [reads.cli] freshness rule are wired. -# BOOT-3 promoted into the live ledger (gaps.yaml) on 2026-06-13 — probe -# authored, baselined RED (WIT-N1 was BROKEN against the skeleton `up` that -# lied), then driven GREEN: `auths witness up` became a real embedded-Compose -# standup that either reaches a healthy node or fails honestly, so every authored -# probe decides. The clean baseline holds. -# WIT-N1 promoted into the live ledger (gaps.yaml) on 2026-06-13 — probe authored -# (+ occupied-port trap), baselined RED. The standup runtime landed; a green -# WIT-N1 waits on an obtainable node image (shared prerequisite with BOOT-1). - -# ── WIT-N: the node + one-command standup ── -- id: WIT-N2 - title: 'receipts verify offline on a stranger machine; tampered receipts rejected' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-N2"] - evidence: - - PRD.md §US-003 - observed: 'UNBASELINED — auths-witness issues receipts; no stranger-machine offline verify is probed' - adversarial_twin: 'a bit-flipped receipt signature is rejected with a distinct error' - smallest_fix: > - receipt + the witness''s published identity verify on a clean machine - with no network and no registry. - # probe: probes/wit-n2.sh # + probes/wit-n2.trap// - unlocks: 'receipts are only corroboration if a third party can check them alone' - -# WIT-N3 promoted into the live ledger (gaps.yaml) on 2026-06-13 — probe authored -# (+ stale-ksn trap), baselined RED (the /key-state endpoint did not exist on the -# pre-change node), then driven GREEN: the witness server now retains verified -# events and serves the current key-state as a KERI-conformant KeyStateRecord at -# GET /witness/{prefix}/key-state (replayed through auths_keri's own from_kel), -# cross-verified both directions against the pinned keripy=1.3.4 oracle, with a -# stale-notice freshness gate (`auths key-state --ingest --reject-stale-below`). - -# WIT-N4 promoted into the live ledger (gaps.yaml) on 2026-06-14 — probe authored -# (+ wrong-digest forged-attestation trap), baselined RED (the pre-change node -# served no /build surface and `status` had no build leg), then driven GREEN: the -# witness server now serves a signed version+digest build proof at GET /build that -# pairs the binary's self-measurement (sha256 of /proc/self/exe at startup) with -# the operator's `auths artifact sign --ci` attestation; `auths witness status` -# verifies it through the PUBLIC verifier (auths_verifier:: -# verify_build_attestation_offline) — signature valid AND attested digest == -# self-measured — so a forged attestation (valid signature over a DIFFERENT -# binary) is rejected. Standup injects the attestation via `auths witness up -# --build-attestation`; the witness-node feature stays additive. - -# WIT-N5 promoted into the live ledger (gaps.yaml) on 2026-06-14 — probe authored -# (+ jargon-leak trap), baselined RED (the product exposed NO single source of -# truth for the operator-vocabulary rule — three divergent hand-maintained jargon -# lists, none of which the probe could anchor to), then driven GREEN: the -# vocabulary-invisible rule now lives in exactly one place (auths-witness-node's -# vocabulary.rs — the canonical PROTOCOL_VOCABULARY denylist + a whole-word, -# case-insensitive scan_for_protocol_vocabulary), the crate's own tests consume it -# (their divergent copies deleted), and the WIT-N5 probe scans the LIVE operator -# happy path (up|status|register|logs|down, attested standup included) against -# exactly that product-owned list — so probe and surface cannot drift. The rule -# now covers the full kernel vocabulary (keri/kel/ksn/said/cesr/oobi/acdc/tel/ -# verkey/prefix/threshold and more), not the earlier 6-term subset. - -# ── WIT-I: infrastructure-as-code ── -- id: WIT-I1 - title: 'cloud standup is one idempotent command; bad creds fail before any resource exists' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-I1"] - evidence: - - PRD.md §US-002, §FR-1 - observed: 'UNBASELINED — no IaC modules exist' - adversarial_twin: 'invalid credentials abort with zero resources created (provider inventory diff)' - smallest_fix: > - `up --cloud ` with embedded OpenTofu: plan→apply→boot→health, - re-run is a no-op. First provider only (harness budget); the rest are - follow-on gaps. - # probe: probes/wit-i1.sh # + probes/wit-i1.trap// - unlocks: 'G1 cloud half; partners without spare hardware' - -- id: WIT-I2 - title: 'teardown leaves nothing billable behind' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-I2"] - evidence: - - PRD.md §US-002, §FR-2 - observed: 'UNBASELINED — no teardown path' - adversarial_twin: 'a manually orphaned resource is reported, not silently ignored' - smallest_fix: > - `down --cloud` destroys all resources (verified via provider inventory - diff); `up` after `down` succeeds cleanly. - # probe: probes/wit-i2.sh # + probes/wit-i2.trap// - unlocks: 'operators trust a network they can leave' - -# ── WIT-D: the public directory ── -- id: WIT-D1 - title: 'the directory is a signed artifact verified client-side; tampering renders as failure' - class: missing-surface - status: open - severity: headline - needs_authoring: true - reads: none - covers: ["WIT-D1"] - evidence: - - PRD.md §US-006, §FR-5, §FR-6 - observed: 'UNBASELINED — no directory exists' - adversarial_twin: 'a tampered directory entry renders as a visible verification failure, never as data' - smallest_fix: > - directory = git repo of org-signed entries; the dashboard fetches, - verifies in-browser (WASM), and renders ONLY what verifies; hosting is - static. - # probe: probes/wit-d1.sh # + probes/wit-d1.trap// - unlocks: 'G4 — the network''s front door is proof, not assertion' - -- id: WIT-D2 - title: 'registration is a signed admission, not a form' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-D2"] - evidence: - - PRD.md §US-006, §FR-5; blocked on ADJUDICATE-2 (admission policy) - observed: 'UNBASELINED — admission policy is an open fork; `register` is blocked on it' - adversarial_twin: 'an entry whose operator identity fails verification cannot be admitted' - smallest_fix: > - `register` emits a signed candidate entry (operator org identity + - endpoints + labels); admission per the adjudicated policy; the admission - itself is a signed event. - # probe: probes/wit-d2.sh # + probes/wit-d2.trap// - unlocks: 'growth without trust-me onboarding' - -- id: WIT-D3 - title: 'uptime and receipt stats are reproducible by an outside observer' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-D3"] - evidence: - - PRD.md §FR-7, §8 Success Metrics - observed: 'UNBASELINED — no prober exists' - adversarial_twin: 'a node lying about uptime disagrees with the prober, and the directory shows the prober''s number' - smallest_fix: > - open prober (this repo) emits signed results; the directory renders - them; the probe re-runs the prober independently and gets a consistent - answer. - # probe: probes/wit-d3.sh # + probes/wit-d3.trap// - unlocks: 'G6 — honesty as a rendered property' - -# ── WIT-T: threshold verification (platform sculpts in ../auths; class per ADJUDICATE-3) ── -- id: WIT-T1 - title: 'an org designates its witness set + threshold as an anchored, signed event' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-T1"] - evidence: - - PRD.md §US-004, §FR-8 - observed: 'UNBASELINED — no witness-designation surface in the platform CLI' - adversarial_twin: 'receipts from a witness OUTSIDE the designated set never count toward M' - smallest_fix: > - `org witness set --threshold M` writes the designation into the - org''s event log; rotating the set is an anchored event at a provable - position. - # probe: probes/wit-t1.sh # + probes/wit-t1.trap// - unlocks: 'controller-chosen trust sets — the protocol-native flywheel hook' - -- id: WIT-T2 - title: 'M-of-N receipts enforced; M−1 is insufficient' - class: missing-surface - status: open - severity: headline - needs_authoring: true - reads: none - covers: ["WIT-T2"] - evidence: - - PRD.md §US-005, §FR-8 - observed: 'UNBASELINED — the verifier has no receipt-threshold policy' - adversarial_twin: 'M−1 valid receipts fail closed' - smallest_fix: > - threshold policy in the platform verifier: ordering-sensitive verdicts - require M valid receipts from the designated set. - # probe: probes/wit-t2.sh # + probes/wit-t2.trap// - unlocks: 'G3 — forged ordering requires collusion, not theft' - -- id: WIT-T3 - title: 'diversity is the default — one operator''s N nodes count once' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-T3"] - evidence: - - PRD.md §FR-9 - observed: 'UNBASELINED — no operator-distinctness rule exists' - adversarial_twin: 'a threshold met only by one operator''s multiple nodes fails' - smallest_fix: > - default verifier policy: receipts counting toward M must come from - distinct operators (by directory identity); jurisdiction-strict as an - optional stricter mode. - # probe: probes/wit-t3.sh # + probes/wit-t3.trap// - unlocks: 'the anti-oligopoly guard, default-closed' - -- id: WIT-T4 - title: 'unreceipted is not invalid — distinct verdicts, explicit fail-closed degradation' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-T4"] - evidence: - - PRD.md §US-005, §FR-10 - observed: 'UNBASELINED — no InsufficientReceipts verdict exists' - adversarial_twin: 'a silent downgrade — an ordering-sensitive verdict issued below threshold — is the RED line' - smallest_fix: > - distinct verdicts: InsufficientReceipts (operational) vs invalid - (attack); below threshold, ordering-sensitive verdicts fail closed while - non-ordering verdicts proceed and say so. - # probe: probes/wit-t4.sh # + probes/wit-t4.trap// - unlocks: 'outages stay fixable without training users to ignore failures' - -- id: WIT-T5 - title: 'the forged-ordering fixture dies under threshold — kept as the permanent trap' - class: security-tradeoff - status: open - severity: headline - needs_authoring: true - reads: none - covers: ["WIT-T5"] - evidence: - - PRD.md §US-005, §8 Success Metrics - - mechanism for lost-the-laptop LTL-1/LTL-2 and verify-the-world V1 (promotion stays with THEIR review protocols) - observed: > - UNBASELINED — today a signer-stamped low anchor-seq forges "signed - before revocation"; with threshold policy the same fixture must fail - without M colluding witnesses. - adversarial_twin: 'this entire claim IS the adversarial fixture; it going GREEN-on-forgery is a gate failure of the highest order' - smallest_fix: > - run the forged-low-seq fixture under active threshold policy and assert - rejection; keep the fixture forever as the suite''s flagship trap. - # probe: probes/wit-t5.sh # + probes/wit-t5.trap// - unlocks: 'the single probe that converts the flywheel from story to guarantee' - -- id: WIT-T6 - title: 'threshold checking adds ≤50ms p99 to verification' - class: friction - status: open - severity: friction - needs_authoring: true - reads: none - covers: ["WIT-T6"] - evidence: - - PRD.md §US-005, §8 Success Metrics - observed: 'UNBASELINED — no measurement exists' - adversarial_twin: 'a flapping perf gate is quarantined BROKEN, never averaged into GREEN' - smallest_fix: > - perf probe on the pinned rig (warmup, N≥1000, p99 not mean, hysteresis): - overhead vs no-threshold baseline ≤ 50ms. - # probe: probes/wit-t6.sh - unlocks: 'verify-on-every-request survives the corroboration upgrade' - -# ── WIT-O: operations & console ── -- id: WIT-O1 - title: 'the console is never stale-green — a dead node shows DOWN within 60s' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-O1"] - evidence: - - PRD.md §US-007, §FR-11 - observed: 'UNBASELINED — no console exists' - adversarial_twin: 'a console serving cached green against a dead node is the RED line' - smallest_fix: > - operator console (/node) reads the metrics endpoint; the harness kills - the node and asserts DOWN renders within 60s. - # probe: probes/wit-o1.sh # + probes/wit-o1.trap// - unlocks: 'G6 — operators can trust their own dashboard' - -- id: WIT-O2 - title: 'node metrics are complete (the monitor daemon finally finishes)' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-O2"] - evidence: - - PRD.md §US-007, §7 Technical Considerations (auths-monitor is framework-only, handlers incomplete) - - 'WIT-N1 standup integration point (filed 2026-06-13): the WIT-N1 embedded standup manifest carried a `monitor` sidecar pinned to the TRANSPARENCY-LOG monitor image (auths-monitor — a registry/checkpoint monitor needing AUTHS_LOG_PUBLIC_KEY + a registry), which is the WRONG daemon for a single-node standup and unshippable as an image today, so it blocked the whole `compose up`. WIT-N1 dropped it; the node-health/metrics SIDECAR the standup should run is exactly this WIT-O2 collector. When WIT-O2 completes auths-monitor as the NODE collector and ships its image, re-add it to StandupRequest::compose_manifest (auths-witness-node) and assert in the WIT-O2 probe that `up` brings it up healthy alongside the witness.' - observed: 'UNBASELINED — auths-monitor is a 394-LoC framework with incomplete handler bodies; and the WIT-N1 standup currently has NO metrics sidecar (the mis-scoped transparency-log one was removed)' - adversarial_twin: 'a metric the console renders that is absent from the endpoint is RED' - smallest_fix: > - complete auths-monitor as the NODE collector: health, receipts/day, - identities served, key-state request rate, last-seen-by-peers — - Prometheus-compatible; ship its image and re-add it as the standup sidecar - (StandupRequest::compose_manifest) so `up` brings node + collector up together. - # probe: probes/wit-o2.sh - unlocks: 'honest directory stats now; real SLOs later (OPS-1 lineage)' - -- id: WIT-O3 - title: 'unhealthy fires a webhook within N minutes, without flap-spam' - class: friction - status: open - severity: friction - needs_authoring: true - reads: none - covers: ["WIT-O3"] - evidence: - - PRD.md §US-007 - observed: 'UNBASELINED — no alerting exists' - adversarial_twin: 'a flapping node does not spam (debounce window probed)' - smallest_fix: > - webhook on unhealthy > N minutes (configurable), signed payload, probed - debounce. - # probe: probes/wit-o3.sh - unlocks: 'operators sleep; the SLA story starts' - -# ── WIT-B: the repo boundary (PRD §7 "Repo boundary") ── -# Source-structure guards: `reads: none` greps are legitimate here because -# the claims are about source. ../auths decides; this repo operates. -- id: WIT-B1 - title: 'the witness-node crate reimplements zero protocol logic' - class: friction - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-B1"] - evidence: - - ADJUDICATE-1 (in-workspace feature-gated crate) - observed: 'UNBASELINED — auths-witness-node not yet built; the guard must exist before it does' - adversarial_twin: 'any hand-rolled receipt / key-state / CESR / SAID parsing inside auths-witness-node (instead of calling auths-keri / auths-verifier / auths-witness) is RED' - smallest_fix: > - auths-witness-node composes the platform crates'' PUBLIC APIs only - (auths-witness, auths-keri, auths-verifier). Depending on those crates is - expected and correct — that IS the integration. What is forbidden is - re-implementing protocol: the moment the crate needs a protocol message the - platform doesn''t expose, that is a missing platform API — file the gap and - add the public surface, never inline the bytes in the node crate. - # probe: probes/wit-b1.sh - unlocks: 'one source of truth for protocol; the node crate is a thin composition over the trust kernel' - -- id: WIT-B2 - title: 'the witness-node feature is purely additive — no core crate depends on it' - class: friction - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-B2"] - evidence: - - ADJUDICATE-1 (lean default; witness-node behind a cargo feature) - observed: 'UNBASELINED — no core crate references the node today; the guard keeps the default build lean' - adversarial_twin: 'a core crate (auths-keri/auths-verifier/auths-core/auths-id/auths-sdk) with a non-optional dep on auths-witness-node, OR a default `auths` build whose `cargo tree` pulls the node''s heavy deps (libgit2/HTTP server/…), is RED' - smallest_fix: > - the dependency arrow is one-way: auths-witness-node → core, never core → - node. The `witness-node` cargo feature is additive only; the `auths - witness` subcommand surface compiles in always (thin clap defs), but its - handler and the node crate are feature-gated, so a default build stays lean. - # probe: probes/wit-b2.sh (cargo tree on default vs --features witness-node) - unlocks: 'the lean default install stays lean; the trust kernel stays witness-network-agnostic' - -- id: WIT-B3 - title: 'the dashboard verifies only through the published verifier' - class: friction - status: open - severity: headline - needs_authoring: true - reads: none - covers: ["WIT-B3"] - evidence: - - PRD.md §7 Repo boundary, FR-5, §6 renders-proofs rule - observed: 'UNBASELINED — dashboard not yet built; one verdict source of truth from day one' - adversarial_twin: 'verification or threshold logic implemented in dashboard code outside the published verifier package is RED — a forked verdict path is the one in the screenshot' - smallest_fix: > - the dashboard renders verdicts, never computes them: all verification - flows through the published WASM verifier package, so browser, FFI, and - CLI verdicts are one implementation. - # probe: probes/wit-b3.sh - unlocks: 'WIT-D1 stays honest — "renders proofs, not assertions" has exactly one prover' - -- id: WIT-B4 - title: 'standup deploys released, attested binaries — never source builds' - class: missing-surface - status: open - severity: feature - needs_authoring: true - reads: none - covers: ["WIT-B4"] - evidence: - - PRD.md §7 Repo boundary, FR-3 - observed: 'UNBASELINED — standup manifests not yet written; pinning discipline starts with them' - adversarial_twin: 'a compose/IaC path that builds the platform from source, or boots a binary whose build attestation fails to verify, is RED' - smallest_fix: > - standup manifests pin released artifact versions and verify the signed - build attestation before boot — operators run what the platform shipped, - provably (dogfoods FR-3). - # probe: probes/wit-b4.sh - unlocks: 'WIT-N4 end-to-end: the node proves what binary it runs, and standup refuses one that cannot' diff --git a/.recurve/claims/network/gaps.yaml b/.recurve/claims/network/gaps.yaml deleted file mode 100644 index 1924207..0000000 --- a/.recurve/claims/network/gaps.yaml +++ /dev/null @@ -1,366 +0,0 @@ -# gaps.yaml — the LIVE ledger for the witness-network conformance suite. -# -# Promoted from gaps.draft.yaml as cycles author probes and baseline claims. -# Reuses the recurve gap schema (parsed by recurvelib/model.py). Status: -# open → probe expected RED (a behavior the suite drives into existence) -# closed → probe expected GREEN (a behavior now present, guarded against regression) -# -# Each open entry carries its REAL observed RED line (what the probe actually -# printed, dated), never a guess — a gap without a measured red is an opinion. - -# ── scaffolding: greenfield bootstrap order ── -- id: BOOT-1 - title: 'the harness exists — 3-witness local fixture boots, oracles pinned' - class: staging - status: closed - severity: feature - reads: none - covers: ["BOOT-1"] - evidence: - - PRD.md §US-008, §FR-13 (the harness stands up a 3-witness local network with failure injection) - - ADJUDICATE-1 (single tree — the node binary is the platform's feature/witness build, not a fork) - - harness/versions.lock (keripy 1.3.4, the interop peers pin convention) - observed: > - GREEN 2026-06-13: `bash probes/boot-1.sh` → - "harness GREEN: 3 distinct witness nodes healthy (did:key:z6MktUL… - did:key:z6MkqGC… did:key:z6Mkg49…), kill-node lever proven, oracle - keripy=1.3.4 pinned and installed" (exit 0); the cloned-identity trap stays - RED ("ours=1-distinct oracle=3-distinct — roster is not 3 independent - identities", exit 1). The shared-image build is cached (auths-witness:net- - fixture), so harness/up.sh just boots three nodes on ports 3331/3332/3333, - each advertising a distinct seed-derived AID. One real (non-Docker) fix - landed the GREEN: the bare `tmpfs: - /data` mount was root-owned, so the - distroless `nonroot` (uid 65532) node could write /data/receipts.db on the - FIRST boot (Docker seeds the declared VOLUME's perms) but died with - SQLITE_CANTOPEN (code 14) after a `docker compose stop`/`start` — the tmpfs - re-mounts fresh as root. Pinning the tmpfs to uid=65532,gid=65532,mode=0700 - makes /data writable on every (re)start, so the kill-node lever's "restart - recovers" half is real, not a first-boot fluke. Earlier baseline (RED, - 2026-06-13): "no 3-witness fixture standing on ports 3331 3332 3333 …" — the - standup was absent (the keripy oracle was already present, so RED not BROKEN). - smallest_fix: > - harness/env.sh + a Docker Compose fixture booting 3 local witness nodes with - DISTINCT identities (distinct AUTHS_WITNESS_SEED → distinct advertised AID); - a kill-node failure-injection helper (stop node N, peers survive, restart - recovers); the keripy oracle pinned in harness/versions.lock reusing the - interop peers convention. The node image is the platform's hardened - auths-witness binary built from its canonical deployment Dockerfile — the - fixture boots what the platform ships, it does not fork a node. - probe: probes/boot-1.sh - unlocks: 'every later WIT-N/T probe stands on this fixture — the burndown IS the build' - -- id: BOOT-2 - title: 'the skeleton builds — rebuild produces the artifacts probes read' - class: staging - status: closed - severity: feature - reads: cli - covers: ["BOOT-2"] - evidence: - - PRD.md §US-008, §FR-12 (suite layout, probe contract, freshness reads: keys) - - ADJUDICATE-1 (in-workspace feature-gated crate auths-witness-node + `auths witness …` subcommands; lean default build stays lean) - - WIT-B1 (the node crate composes auths-witness/auths-keri/auths-verifier), WIT-B2 (the witness-node feature is purely additive) - observed: > - RED at baseline 2026-06-13: `bash probes/boot-2.sh` → - "no bin/auths — the suite rebuild has not run (recurve rebuild network → - harness/rebuild.sh)" (exit 1). Greenfield: before this cycle the - [suites.network] rebuild was empty and there was no [reads.cli] freshness - rule, no auths-witness-node crate, and the `auths witness` surface carried - no operator verbs — nothing produced the feature-enabled artifact the - cli probes read. - smallest_fix: > - scaffold auths/crates/auths-witness-node behind a `witness-node` workspace - feature (composing auths-witness/auths-keri/auths-verifier, not - reimplementing protocol); add the `auths witness up|down|status|register| - logs` operator verbs to auths-cli — the clap surface compiles in always - (thin), the handler is cfg(feature=witness-node) real / cfg(not) a helpful - "install the witness build" error; wire the [suites.network] rebuild - (harness/rebuild.sh builds the feature-enabled auths into target/ - witness-node and copies bin/auths) + the [reads.cli] content-hash rule so - cli probes refuse to run stale; keep the feature additive (default - `cargo tree -p auths-cli` shows no auths-witness-node). - probe: probes/boot-2.sh - unlocks: 'the freshness axis + everything later cli probes execute against the feature-enabled build' - -- id: BOOT-3 - title: 'every authored probe can run — the baseline decides, never BROKEN' - class: staging - status: closed - severity: feature - reads: cli - covers: ["BOOT-3"] - evidence: - - PRD.md §US-008 (cycle 1 never faces a wall of BROKEN — the greenfield rule) - - probes/_contract.sh (the frozen contract — exit 2/timeout/crash all coerce to BROKEN) - - WIT-N1 (the wave's standup probe — the one whose happy path the baseline had to make measurable) - observed: > - RED at baseline 2026-06-13: `bash probes/boot-3.sh` → - "ours=BROKEN baseline expected=all-decide — 1 authored probe(s) could not - measure on the built tree (exit≠0,1): wit-n1.sh:exit=2 — fix the missing - env/fixture prerequisite so the baseline is RED/GREEN, never BROKEN" (exit 1). - The BROKEN sibling was WIT-N1: against the skeleton `auths witness up` (which - exited 0 and printed a health URL while standing nothing up), the WIT-N1 - probe could not decide whether the standup capability was real or faked — - `bash probes/wit-n1.sh` → "exited 0 and printed http://127.0.0.1:3340/health - but nothing answers there … cannot decide WIT-N1 against a build whose - success is a lie" (exit 2 BROKEN). A baseline with a BROKEN in it is not a - baseline — the burndown cannot start. - smallest_fix: > - run the full authored probe set on the built tree and drive every verdict to - a DECISION (RED/GREEN). The fix was real: `auths witness up` now performs a - genuine embedded-Compose standup (bring up node + monitor, wait for the - health endpoint, print the URL, exit 0) and — crucially — FAILS HONESTLY - (non-zero, one actionable line, nothing left standing) when it cannot stand a - node up, instead of claiming success. With `up` no longer lying, WIT-N1 - decides (RED here, where no node image is obtainable; GREEN where it is), so - the meta-probe sees zero BROKEN. boot-3.sh runs every sibling probe - (excluding itself + the sourced _contract.sh) and is RED if any exits ≠ 0,1. - probe: probes/boot-3.sh - unlocks: 'a clean baseline — the burndown starts on RED/GREEN, never on could-not-measure' - -- id: WIT-N1 - title: 'one command, one witness — `up` reaches a healthy node or fails honestly' - class: missing-surface - status: closed - severity: headline - reads: cli - covers: ["WIT-N1"] - evidence: - - PRD.md §US-001, §FR-1, §FR-2, §FR-4 - observed: > - GREEN 2026-06-13: `bash probes/wit-n1.sh` → "one command stood up a healthy - witness: `witness up` exited 0, printed http://127.0.0.1:3340/health, the node - answers there, and the happy path carries zero protocol vocabulary" (exit 0); - the occupied-port trap is RED (exit 1). What unblocked it was the shared - prerequisite BOOT-1 flagged — an OBTAINABLE node image — plus two real fixes - in ../auths: (1) the canonical witness deployment Dockerfile was genuinely - broken (it added the musl target to a different toolchain than the - rust-toolchain.toml selects, and hardcoded x86_64 on an arm64 host → E0463 - "can't find crate for core") and now resolves the static target from - TARGETARCH after the source copy, so the released image builds natively on - both arches; (2) the embedded standup manifest dropped a transparency-log - `monitor` sidecar that referenced an unshippable image and blocked the whole - `compose up`, and `up` now mints the node's stable signing identity at first - boot (OS-CSPRNG seed injected via a `.env`, never a key file baked in), so the - released-image node actually becomes healthy. The harness owns image - acquisition (harness/ensure-image.sh builds-once from the canonical - Dockerfile, tags auths-witness:net-fixture); `up` runs that RELEASED image - (`--image`), never a source build (WIT-B4 holds). Earlier baseline (RED, same - day): "ours=exit1 expected=exit0 — `witness up` did not complete a standup …" - when no node image was obtainable; `up` failed honestly then, which is the - adversarial half of the same claim. - adversarial_twin: > - occupied port / absent Docker fails with a one-line actionable error and - leaves NO partial state; file-key fallback refuses without --accept-file-key. - smallest_fix: > - `up` wraps embedded Compose (the released witness node), mints the node - identity at first boot, prints a health URL, exits 0 — and on a clean box - reaches healthy ≤ 600s. The probe drives `up` against a throwaway port/data - dir (with the harness having made the released image present) and asserts a - real node answers the printed URL with zero protocol vocabulary; on a box with - no engine it asserts the clean adversarial refusal instead. Trap: an `up` that - exits 0 on an occupied port (partial-state lie) stays RED. - probe: probes/wit-n1.sh - unlocks: 'G1 — the flywheel cost-per-turn; the first thing every partner runs' - -- id: WIT-N2 - title: "receipts verify offline on a stranger's machine; tampered receipts rejected" - class: missing-surface - status: closed - severity: feature - reads: cli - covers: ["WIT-N2"] - evidence: - - PRD.md §US-003 (receipts verify offline from the receipt + the witness's published identity alone; a bit-flipped receipt is rejected) - - 'Repo boundary (PRD §7): the offline-verify decision is protocol (must be correct for strangers) → it lives in auths-verifier; the operator/stranger CLI surface composes it (WIT-B1)' - observed: > - GREEN 2026-06-13: `NO_COLOR=1 bash probes/wit-n2.sh` → "receipts verify - offline on a stranger's machine: a real witness receipt + the node's - published identity verified with no network and no registry [provenance: live - node :3331 (did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S)], and a - bit-flipped receipt was rejected with a distinct reason — the corroboration - claim holds end to end" (exit 0); the forged-signature trap is RED (exit 1: - "rejected: this receipt does not verify against did:key:z6MktUL… — it was - altered or was not issued by that node"). The probe stood up the 3-witness - fixture, had wit1 receipt a valid inception event (a real SignedReceipt came - back), read the node's published did:key from /health, assembled the bundle, - and verified it in an isolated empty-home context (HOME+AUTHS_HOME → empty - dir, so no registry, no pinned witnesses), leaving the fixture standing for - the rest of the suite. What landed: the self-contained offline verify is now - in the PUBLIC verifier (auths_verifier::verify_receipt_offline) — it recovers - the witness's key from the published did:key (which embeds it) and checks the - signature over the canonical receipt bytes, returning a parsed Verified / - SignatureFailed / UnreadableIdentity verdict; the node crate composes it as a - ReceiptBundle and `auths witness verify-receipt --receipt ` exposes it. - Earlier same-day baseline (RED): "ours=exit2 expected=verified … error: - unrecognized subcommand 'verify-receipt'" — the receipt half existed, the - third-party-checks-it-alone half did not. - adversarial_twin: 'a bit-flipped receipt signature is rejected with a distinct reason (the forged-signature trap stays RED)' - smallest_fix: > - the self-contained offline verify in the PUBLIC verifier - (auths_verifier::verify_receipt_offline): recover the witness's key from its - published did:key (which embeds it) and check the signature over the - canonical receipt bytes, returning a parsed Verified / SignatureFailed / - UnreadableIdentity verdict. The node crate composes it as a ReceiptBundle - (signed receipt + published identity); `auths witness verify-receipt - --receipt ` exposes it — no network, no registry. A tampered receipt - fails closed with a distinct reason. - probe: probes/wit-n2.sh - unlocks: 'receipts are only corroboration if a third party can check them alone — the WIT-T threshold story rests on offline-checkable receipts' - -- id: WIT-N3 - title: 'node serves conformant key-state notices (cross-verified vs the oracle)' - class: wire-mismatch - status: closed - severity: feature - reads: cli - covers: ["WIT-N3"] - evidence: - - PRD.md §US-003, §FR-3 (node serves KERI-conformant KSN at a stable endpoint; cross-verified against the keripy oracle) - - depends on interop IOP-L3c (KSN wire shape — owned there, referenced not duplicated; the same keripy=1.3.4 pin in harness/versions.lock) - - 'Repo boundary (PRD §7): the KSN wire shape + replay are protocol (must be correct for strangers) → they live in auths-keri/auths-core; the node only serves and the operator CLI only ingests (WIT-B1)' - observed: > - GREEN 2026-06-13: `NO_COLOR=1 bash probes/wit-n3.sh` → "the node serves a - conformant key-state notice: a live node witnessed a real inception and served - a KERI ksn wire record that the keripy oracle (1.3.4) reconstructs - byte-for-byte (node→oracle), the node ingests a notice the oracle publishes - (oracle→node), and a stale notice is detected as stale — IOP-L3c's wire - conformance now holds against the running node" (exit 0); the stale-ksn trap - is RED (exit 1: "rejected: stale key-state notice — KSN is stale: seq 0 < - last-seen 1; a verifier holding a newer state refuses to rewind"). The probe - stood up the 3-witness fixture, had wit1 witness a full conformant inception - (probes/fixtures/keri-icp.json — a real auths-signed icp), then GET - /witness//key-state and (1) reconstructed the served record inside the - pinned keripy oracle field-for-field (harness/ksn_oracle.py → ORACLE-OK), (2) - ingested a keripy-emitted notice via `auths key-state --ingest` - (harness/ksn_emit.py), (3) detected a stale (lower-seq) notice via - `--reject-stale-below`. What landed in ../auths: the witness server now RETAINS - the verified event body (auths-core witness storage `events` table) so it can - replay an identity's KEL into current key-state, and serves it at a new stable - endpoint GET /witness/{prefix}/key-state as a KERI KeyStateRecord (the - {vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di} wire shape) — built only from - auths_keri::KeyStateRecord::from_kel (the trust kernel, never hand-rolled); - auths_keri::KeyStateRecord gained sequence()/check_not_stale() and the - `auths key-state --ingest --reject-stale-below ` flag composes it so a - verifier holding a newer state fails closed on a rewind. Earlier same-day - baseline (RED): against the pre-change node the endpoint did not exist — - "ours=no-ksn-endpoint expected=served-notice … the node did not serve a - key-state notice at /witness//key-state (curl exit 22)". - adversarial_twin: 'a stale key-state notice (a lower sequence than a newer state the verifier holds) is detected as stale, never accepted — the stale-ksn trap stays RED' - smallest_fix: > - the witness server retains verified events and serves the current key-state as - a KERI-conformant KeyStateRecord at a stable endpoint, replayed through the - platform's own from_kel (zero hand-rolled protocol); the served record - reconstructs byte-for-byte in the pinned keripy oracle and a keripy-published - record ingests on the node; a stale (lower-seq) notice is detected as stale via - a verifier-held last-seen sequence (auths_keri::KeyStateRecord::check_not_stale, - exposed as `auths key-state --ingest --reject-stale-below`). - probe: probes/wit-n3.sh - unlocks: 'thin-client key-state trust; the directory freshness column — and the running node now backs IOP-L3c' - -- id: WIT-N4 - title: 'the node proves what binary it runs (signed build attestation)' - class: missing-surface - status: closed - severity: feature - reads: cli - covers: ["WIT-N4"] - evidence: - - PRD.md §FR-3 (the witness node carries a signed version/build attestation — the node proves what binary it runs, dogfooding `artifact sign --ci`) - - 'Repo boundary (PRD §7): the attestation-signature check is protocol (must be correct for strangers) → it lives in auths-verifier; the node only self-measures + serves and the operator CLI only renders the verdict (WIT-B1)' - - WIT-B2 (the build surface is additive — the witness-node feature stays one-way; no core crate depends on the node crate), WIT-B4 (standup deploys the released attested binary, never a source build) - observed: > - GREEN 2026-06-14: `NO_COLOR=1 bash probes/wit-n4.sh` → "the node proves which - binary it runs: a live node served a signed version+digest build attestation, - `witness status` verified it against the node's own self-measurement of the - running binary, and a forged attestation (valid signature over a different - binary) was rejected with a distinct reason — an operator vouching for the - network is itself vouchable" (exit 0); the wrong-digest forged-attestation trap - is RED (exit 1: "rejected: the attestation is for a different binary (attested - 296078e6…, running 7ce84d53…) — this node is not running what it attests"). The - probe stands up its OWN throwaway node with a build attestation the harness - produced over the RELEASED image's binary (extract /usr/local/bin/auths-witness - from the image, dogfood `auths artifact sign --ci` over it), GET /build, asserts - `auths witness status` verifies it, then stands a SECOND node up with an - attestation signed over a DIFFERENT artifact and asserts `status` refuses — the - forgery the claim exists to catch. What landed in ../auths: (1) the witness - server measures its own running binary (sha256 of /proc/self/exe at startup) and - serves a new BuildProof at GET /build pairing that self-measurement with the - signed attestation (auths-core; the server interprets nothing — 404 when no - attestation was configured, so a node that cannot prove its binary says so - plainly); (2) the offline build-attestation check is in the PUBLIC verifier - (auths_verifier::verify_build_attestation_offline) — two-leg, fail-closed: the - signature verifies against the key its self-describing did:key issuer embeds AND - the attested digest equals the node's self-measured running digest, so a valid - signature over the WRONG binary lands on DigestMismatch, never Verified; (3) the - node crate composes it as BuildAttestation::verify → NodeBuildVerdict and `auths - witness status` fetches /build and renders the verdict, failing closed on a - forged or absent build; (4) `auths witness up --build-attestation ` - injects the attestation into the standup compose (read-only bind + env), and the - auths-witness binary reads AUTHS_WITNESS_BUILD_ATTESTATION at boot. Earlier - baseline (RED, same day): against the pre-change node the endpoint did not exist - and `status` had no build leg — "ours=no-build-endpoint expected=served-proof — - the node did not serve a build attestation at /build (curl exit 22)". - adversarial_twin: 'an attestation whose digest differs from the running binary fails verification — a forged (different-binary) attestation is rejected by `status` with a distinct reason; the wrong-digest trap stays RED' - smallest_fix: > - the witness server measures its own running binary (sha256 of /proc/self/exe at - startup) and serves it paired with the operator's signed `auths artifact sign - --ci` attestation at GET /build; the offline check lives in the PUBLIC verifier - (auths_verifier::verify_build_attestation_offline) — fail-closed two-leg: the - signature verifies against the issuer's embedded key AND the attested digest - equals the self-measured running digest, so a valid signature over a different - binary fails on DigestMismatch. `auths witness status` composes it and renders - the verdict; `auths witness up --build-attestation` injects the attestation at - standup. The witness-node feature stays additive; no protocol is hand-rolled. - probe: probes/wit-n4.sh - unlocks: 'operators vouching for the network must be vouchable themselves — and standup (WIT-B4) can refuse a node whose binary does not verify' - -- id: WIT-N5 - title: 'zero protocol vocabulary in the operator happy path' - class: friction - status: closed - severity: feature - reads: cli - covers: ["WIT-N5"] - evidence: - - PRD.md §US-001, §6 Design Considerations (operators never see the protocol's vocabulary) - - 'Repo boundary (PRD §7): the operator-facing surface lives in the node crate; the vocabulary rule the surface is held to is owned there too (auths-witness-node), the one place it can have a single source of truth' - observed: > - GREEN 2026-06-14: `NO_COLOR=1 bash probes/wit-n5.sh` → "the operator happy - path carries zero protocol vocabulary: every line `witness up|status|register| - logs|down` printed (attested standup included) passed a whole-word, - case-insensitive scan against the product's own canonical denylist (covering - keri/kel/ksn/said/cesr/oobi/acdc/tel/verkey/prefix/threshold and more), and - that denylist lives in exactly one place — the vocabulary-invisible rule is a - guarantee with one owner, not a hope spread across ad-hoc lists" (exit 0); the - jargon-leak trap is RED (exit 1: "ours=trap:kel expected=RED — the captured - transcript leaks protocol vocabulary (whole-word, case-insensitive); the - scanner caught it"). The probe stands up its OWN throwaway attested node, - captures every line the full operator happy path prints, and scans it against - the denylist EXTRACTED FROM the product source (so probe and surface cannot - drift). What landed in ../auths: the vocabulary-invisible rule became a single - source of truth — auths-witness-node/src/vocabulary.rs owns the canonical - `PROTOCOL_VOCABULARY` denylist (the trust kernel's wire/ceremony vocabulary an - operator must never see) and `scan_for_protocol_vocabulary` (whole-word, - case-insensitive, so benign substrings like "prefixed"/"did:key:" are not false - positives); the crate's own happy-path tests in lib.rs (health_url) and - build.rs (verdict summary) now consume it, their DIVERGENT inline jargon lists - deleted (quality §3/§4). Earlier baseline (RED, 2026-06-13): against the - pre-change tree the rule had no single owner — "ours=no-canonical-denylist - expected=one-owner — the product exposes no single source of truth for the - operator-vocabulary rule … a rule with no owner is a hope, not a guarantee" - (exit 1). The happy-path output itself was already clean; the cycle converted - that from three ad-hoc, divergent, partial (6-term) checks into one enforced, - full (11+ term) guarantee the probe anchors to. - adversarial_twin: 'any protocol term (keri, kel, ksn, said, cesr, oobi, acdc, tel, verkey, prefix, threshold, …) standing as a whole word in up/status/register/logs/down happy-path output is RED — the jargon-leak trap stays RED' - smallest_fix: > - lift the operator-vocabulary rule into one place: a canonical - PROTOCOL_VOCABULARY denylist + a whole-word case-insensitive - scan_for_protocol_vocabulary in auths-witness-node, consumed by the crate's - own happy-path tests (their divergent jargon copies deleted). The probe scans - the LIVE operator happy path against exactly that product-owned list - (reads:cli for the standup, reads:source for the denylist — both legitimate - because the claim is about output strings), so probe and surface cannot drift - and the green forbids precisely the words the product forbids. - probe: probes/wit-n5.sh - unlocks: 'the vocabulary-invisible rule, carried to operators — one owner, full coverage, a guard that cannot drift from the surface it protects' diff --git a/.recurve/claims/network/harness/.gitignore b/.recurve/claims/network/harness/.gitignore deleted file mode 100644 index 95f7491..0000000 --- a/.recurve/claims/network/harness/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.state/ diff --git a/.recurve/claims/network/harness/compose/docker-compose.yml b/.recurve/claims/network/harness/compose/docker-compose.yml deleted file mode 100644 index 992537d..0000000 --- a/.recurve/claims/network/harness/compose/docker-compose.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Local 3-witness fixture for the witness-network conformance suite. -# -# Three INDEPENDENT witness nodes, distinct identities (distinct AUTHS_WITNESS_SEED -# => distinct advertised AID), distinct host ports. This is the smallest fixture -# that makes the threshold story real from the start: a 2-of-3 set you can kill -# one node out of and still meet, kill two and fail closed. -# -# The node image is the platform's hardened witness binary, built from the -# canonical deployment Dockerfile in the shared tree (a musl-static -# `auths-witness`) — the suite boots what the platform ships, it does not fork a -# node. The signing seed is INJECTED at runtime (never baked into the image), -# exactly as a real deployment would inject it from a secret store. -# -# The image is built ONCE by harness/up.sh (`docker build`), not per-service: -# three services that each declared `build:` would trigger three concurrent -# musl compiles of the whole workspace. Here every node shares the single -# pre-built `auths-witness:net-fixture` image and only differs by seed + port. - -x-witness: &witness - image: auths-witness:net-fixture - restart: "no" - healthcheck: - # The binary is distroless (no shell/curl), so health is checked from the - # host by the harness against the published port — not in-container. - disable: true - # Seed-injected identity (no keystore file); receipts persist to the tmpfs - # mounted at /data so the read-only rootfs stays read-only. - command: ["--bind", "0.0.0.0:3333", "--curve", "ed25519", "--persist", "/data/receipts.db"] - # The runtime image is distroless `nonroot` (uid/gid 65532). A bare `tmpfs: - - # /data` is mounted root-owned 0755, so the node can write /data/receipts.db on - # the FIRST `up` (Docker seeds the declared VOLUME's perms) but NOT after a - # `docker compose stop`/`start`, where the tmpfs is re-mounted fresh as root — - # the node then dies with SQLITE_CANTOPEN (code 14). Pinning the tmpfs to the - # node's uid/gid makes /data writable on every (re)start, so the kill-node - # lever's "restart recovers" half is real, not a first-boot-only fluke. - tmpfs: - - /data:uid=65532,gid=65532,mode=0700 - -services: - wit1: - <<: *witness - container_name: auths-witness-net-wit1 - environment: - AUTHS_WITNESS_SEED: "1111111111111111111111111111111111111111111111111111111111111111" - ports: - - "127.0.0.1:3331:3333" - - wit2: - <<: *witness - container_name: auths-witness-net-wit2 - environment: - AUTHS_WITNESS_SEED: "2222222222222222222222222222222222222222222222222222222222222222" - ports: - - "127.0.0.1:3332:3333" - - wit3: - <<: *witness - container_name: auths-witness-net-wit3 - environment: - AUTHS_WITNESS_SEED: "3333333333333333333333333333333333333333333333333333333333333333" - ports: - - "127.0.0.1:3333:3333" diff --git a/.recurve/claims/network/harness/down.sh b/.recurve/claims/network/harness/down.sh deleted file mode 100755 index b8e54b7..0000000 --- a/.recurve/claims/network/harness/down.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# harness/down.sh — tear the local 3-witness fixture down. Removes containers and -# the fixture network; leaves the built image cached for the next bring-up. -set -euo pipefail -. "$(dirname "$0")/env.sh" - -if ! docker info >/dev/null 2>&1; then - say "Docker daemon down — nothing to tear down" - exit 0 -fi -docker compose -p "$COMPOSE_PROJECT" -f "$HARNESS_COMPOSE/docker-compose.yml" down --remove-orphans -pass "3-witness fixture down" diff --git a/.recurve/claims/network/harness/ensure-build-attestation.sh b/.recurve/claims/network/harness/ensure-build-attestation.sh deleted file mode 100755 index 8b26b30..0000000 --- a/.recurve/claims/network/harness/ensure-build-attestation.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# harness/ensure-build-attestation.sh — produce the signed build attestation for -# the released witness image's binary, so a stood-up node can PROVE which binary -# it runs. -# -# This is the operator-side dogfood: the operator (here, the harness) signs the -# exact binary the released image ships, using `auths artifact sign --ci`, and -# pins the resulting `.auths.json`. Standup mounts it into the node -# (`auths witness up --build-attestation`), the node measures its own running -# binary and serves both at `/build`, and `auths witness status` confirms the -# signature holds AND attests the digest the node measured of itself. A forged -# attestation (one over a DIFFERENT binary) is rejected by that digest check. -# -# Producing the attestation over the RELEASED IMAGE'S binary is the whole point: -# the binary the node runs is `/usr/local/bin/auths-witness` inside -# $WITNESS_IMAGE, so we extract THAT exact file and sign it. The node's -# self-measurement of `/proc/self/exe` is byte-identical to it, so a genuine -# attestation matches and a swapped one does not. -# -# Prints the absolute path to the attestation file on stdout. Idempotent: a -# cached attestation for the current image is reused. -set -euo pipefail -. "$(dirname "$0")/env.sh" - -WITNESS_IMAGE="${WITNESS_IMAGE:-auths-witness:net-fixture}" -AUTHS_BIN="$SUITE_DIR/bin/auths" -OUT_DIR="$HARNESS_STATE/build-attestation" -ATTESTATION="$OUT_DIR/build.auths.json" -EXTRACTED="$OUT_DIR/auths-witness" - -[ -x "$AUTHS_BIN" ] \ - || { echo "no bin/auths — run the suite rebuild first (recurve rebuild network)" >&2; exit 2; } -if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then - echo "no container engine — cannot extract the image binary to sign" >&2 - exit 2 -fi -docker image inspect "$WITNESS_IMAGE" >/dev/null 2>&1 \ - || { echo "witness image absent ($WITNESS_IMAGE) — run harness/ensure-image.sh first" >&2; exit 2; } - -mkdir -p "$OUT_DIR" - -# Extract the EXACT binary the released image runs (/usr/local/bin/auths-witness, -# per the canonical deployment Dockerfile). docker cp is byte-identical, so the -# node's self-measurement of /proc/self/exe will match this file's digest. -cid="$(docker create "$WITNESS_IMAGE")" -trap 'docker rm "$cid" >/dev/null 2>&1 || true' EXIT -docker cp "$cid:/usr/local/bin/auths-witness" "$EXTRACTED" >/dev/null 2>&1 \ - || { echo "could not extract /usr/local/bin/auths-witness from $WITNESS_IMAGE" >&2; exit 2; } - -digest="$(shasum -a 256 "$EXTRACTED" | awk '{print $1}')" - -# Reuse a cached attestation iff it already attests this exact binary digest. -if [ -f "$ATTESTATION" ]; then - cached="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["payload"]["digest"]["hex"])' "$ATTESTATION" 2>/dev/null || true)" - if [ "$cached" = "$digest" ]; then - # Absolute path on stdout. - cd "$OUT_DIR" && printf '%s\n' "$(pwd)/build.auths.json" - exit 0 - fi -fi - -# Dogfood the platform's own CI artifact signer over the released binary. -# --allow-unlogged keeps this a local, offline self-check (no transparency log -# is needed to PROVE which binary runs — the signature + the live self-measured -# digest are sufficient and are what `status` checks). -"$AUTHS_BIN" artifact sign "$EXTRACTED" \ - --ci \ - --commit 0000000000000000000000000000000000000000 \ - --ci-platform local \ - --allow-unlogged \ - --sig-output "$ATTESTATION" >/dev/null 2>&1 \ - || { echo "auths artifact sign --ci failed over the image binary" >&2; exit 2; } - -cd "$OUT_DIR" && printf '%s\n' "$(pwd)/build.auths.json" diff --git a/.recurve/claims/network/harness/ensure-image.sh b/.recurve/claims/network/harness/ensure-image.sh deleted file mode 100755 index 64f7e2b..0000000 --- a/.recurve/claims/network/harness/ensure-image.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# harness/ensure-image.sh — make the released witness node image PRESENT locally. -# -# Standup runs a RELEASED image (`image:`, never `build:`) — obtaining that image -# is the harness's job, never the probe's or `up`'s. On a real VPS the operator -# pulls the published image; on a dev/CI box where it is not published, the -# harness builds it ONCE from the platform's canonical deployment Dockerfile and -# tags it, so `auths witness up --image "$WITNESS_IMAGE"` finds it present and -# stands a real node up. This keeps the source build OUT of the standup path -# (WIT-B4) — the binary the node runs is still what the platform ships, built -# from the canonical image definition, just made present here. -# -# Idempotent: if the tag already exists this returns fast. -set -euo pipefail -. "$(dirname "$0")/env.sh" - -# The local tag standup is pointed at by the suite. One tag, one source of truth, -# shared by ensure-image, up.sh, and the WIT-N1 probe. -WITNESS_IMAGE="${WITNESS_IMAGE:-auths-witness:net-fixture}" - -if ! command -v docker >/dev/null 2>&1; then - die "docker CLI absent — the witness image needs a container engine" -fi -if ! docker info >/dev/null 2>&1; then - die "Docker daemon down — start Docker Desktop (open -a Docker)" -fi - -if docker image inspect "$WITNESS_IMAGE" >/dev/null 2>&1; then - pass "witness image already present: $WITNESS_IMAGE" - exit 0 -fi - -say "building the witness node image ONCE from the canonical deployment Dockerfile (musl static; first build is slow, cached after)" -# One build, tagged for the whole suite. The .dockerignore in $AUTHS_SRC keeps the -# context to source only. -docker build \ - -f "$AUTHS_SRC/docs/deployment/witness/Dockerfile" \ - -t "$WITNESS_IMAGE" \ - "$AUTHS_SRC" - -docker image inspect "$WITNESS_IMAGE" >/dev/null 2>&1 \ - || die "image build reported success but $WITNESS_IMAGE is not present" -pass "witness image built and tagged: $WITNESS_IMAGE" diff --git a/.recurve/claims/network/harness/env.sh b/.recurve/claims/network/harness/env.sh deleted file mode 100755 index c2e2f4c..0000000 --- a/.recurve/claims/network/harness/env.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# harness/env.sh — paths, constants, and helpers for the witness-network -# conformance suite. Source me. -# -# Layout mirrors interop/harness/env.sh: Layer 1 (constants + helpers) is always -# safe to source. The suite drives building the platform witness node in -# ../../../../auths (the shared tree) and boots a LOCAL 3-node fixture (Docker -# Compose) the probes check against. No product code lives here — only the -# fixture that lets a probe behaviorally test a claim end to end. -set -euo pipefail - -HARNESS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" -SUITE_DIR="$(dirname "$HARNESS_DIR")" # .../claims/network -RECURVE_DIR="$(cd "$SUITE_DIR/../.." && pwd)" # .../.recurve -NET_ROOT="$(cd "$RECURVE_DIR/.." && pwd)" # .../auths-network -AUTHS_SRC="$(cd "$NET_ROOT/../auths" && pwd)" # the platform workspace (shared tree) - -HARNESS_COMPOSE="$HARNESS_DIR/compose" -HARNESS_STATE="$HARNESS_DIR/.state" # gitignored runtime scratch -VERSIONS_LOCK="$HARNESS_DIR/versions.lock" - -# The local fixture: three witness nodes with DISTINCT identities. The seeds are -# fixed (not secrets — a local test fixture), so each node's advertised AID is -# stable and reproducible across bring-ups. Distinct seeds => distinct AIDs => -# the diversity the threshold story rests on, present from the fixture up. -COMPOSE_PROJECT="auths-witness-net" -NODE_NAMES=(wit1 wit2 wit3) -NODE_PORTS=(3331 3332 3333) -# The local tag the suite makes the RELEASED witness node image present under. -# Standup runs this image (`auths witness up --image "$WITNESS_IMAGE"`); the -# harness (ensure-image.sh) builds it ONCE from the platform's canonical -# deployment Dockerfile. One tag, one source of truth — env.sh, ensure-image.sh, -# up.sh, and the WIT-N1 probe all read it from here. -WITNESS_IMAGE="${WITNESS_IMAGE:-auths-witness:net-fixture}" -# 32-byte hex seeds, one per node — distinct, fixed, obviously test-only. -NODE_SEEDS=( - "1111111111111111111111111111111111111111111111111111111111111111" - "2222222222222222222222222222222222222222222222222222222222222222" - "3333333333333333333333333333333333333333333333333333333333333333" -) - -if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then - C_DIM="$(printf '\033[2m')"; C_GREEN="$(printf '\033[32m')" - C_RED="$(printf '\033[31m')"; C_RESET="$(printf '\033[0m')" -else - C_DIM=""; C_GREEN=""; C_RED=""; C_RESET="" -fi -say() { printf '%s▸ %s%s\n' "$C_DIM" "$*" "$C_RESET"; } -die() { printf '%s✗ %s%s\n' "$C_RED" "$*" "$C_RESET" >&2; exit 1; } -pass() { printf '%s✓ %s%s\n' "$C_GREEN" "$*" "$C_RESET"; } - -# node_health — print the /health JSON of a running node, or fail. -node_health() { - local port="$1" - curl -fsS --max-time 3 "http://127.0.0.1:${port}/health" 2>/dev/null -} - -# node_aid — the advertised witness identity (AID) of a running node. -node_aid() { - node_health "$1" | python3 -c 'import json,sys; print(json.load(sys.stdin)["witness_did"])' 2>/dev/null -} - -# all_nodes_healthy — 0 if every node in NODE_PORTS answers /health, else 1. -all_nodes_healthy() { - local port - for port in "${NODE_PORTS[@]}"; do - node_health "$port" >/dev/null 2>&1 || return 1 - done - return 0 -} - -# oracle_version — the keripy version pinned in versions.lock (single source). -oracle_version() { - awk '/^ keripy:/{f=1} f&&/version:/{gsub(/[" ]/,"",$2); print $2; exit}' "$VERSIONS_LOCK" -} diff --git a/.recurve/claims/network/harness/kill-node.sh b/.recurve/claims/network/harness/kill-node.sh deleted file mode 100755 index 21ed14d..0000000 --- a/.recurve/claims/network/harness/kill-node.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# harness/kill-node.sh — failure injection. Stop one witness node by index -# (1..3) so a probe can assert how the network degrades: kill one and a 2-of-3 -# threshold still holds; kill two and ordering-sensitive verdicts fail closed. -# -# kill-node.sh stop node N (1-based) -# kill-node.sh start restore node N -# -# Stop, not remove — the node's identity and receipts survive so a restart is a -# real recovery, not a fresh node. This is the FR-13 "kill 1 node" lever. -set -euo pipefail -. "$(dirname "$0")/env.sh" - -idx="${1:-}" -action="${2:-stop}" -case "$idx" in - 1|2|3) ;; - *) die "usage: kill-node.sh <1|2|3> [stop|start]" ;; -esac -name="${NODE_NAMES[$((idx-1))]}" - -case "$action" in - stop) - docker compose -p "$COMPOSE_PROJECT" -f "$HARNESS_COMPOSE/docker-compose.yml" stop "$name" >/dev/null - pass "node $idx ($name) stopped — port ${NODE_PORTS[$((idx-1))]} now dark" - ;; - start) - docker compose -p "$COMPOSE_PROJECT" -f "$HARNESS_COMPOSE/docker-compose.yml" start "$name" >/dev/null - pass "node $idx ($name) restarted" - ;; - *) die "unknown action '$action' — expected stop|start" ;; -esac diff --git a/.recurve/claims/network/harness/ksn_emit.py b/.recurve/claims/network/harness/ksn_emit.py deleted file mode 100755 index 17c767c..0000000 --- a/.recurve/claims/network/harness/ksn_emit.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -"""Emit a KERI key-state notice the way the pinned keripy oracle publishes one. - -Builds a `KeyStateRecord` with keripy 1.3.4 (`eventing.state(...)`) and prints it -as the canonical KERI ksn wire record on stdout. The node must be able to ingest -this — `auths key-state --ingest` consuming a record an implementation we do not -control produced is the oracle→node half of the conformance claim. - -Deterministic (fixed key material) so the emitted record is stable across runs. -""" - -import json -import sys - -from keri.core import coring, eventing -from keri.core.coring import MtrDex - - -def main() -> int: - # A deterministic, transferable Ed25519 controller verkey (fixed raw bytes). - verkey = coring.Verfer(raw=bytes(range(32)), code=MtrDex.Ed25519).qb64 - - # A self-addressing inception, then its key-state notice — exactly the shape a - # keripy peer would publish for a thin client. - icp = eventing.incept(keys=[verkey], code=coring.MtrDex.Blake3_256) - ksr = eventing.state( - pre=icp.pre, - sn=0, - pig="", - dig=icp.said, - fn=0, - eilk="icp", - keys=[verkey], - eevt=eventing.StateEstEvent(s="0", d=icp.said, br=[], ba=[]), - sith="1", - ndigs=[], - toad=0, - wits=[], - cnfg=[], - ) - json.dump(ksr._asdict(), sys.stdout) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.recurve/claims/network/harness/ksn_oracle.py b/.recurve/claims/network/harness/ksn_oracle.py deleted file mode 100755 index 7e2e561..0000000 --- a/.recurve/claims/network/harness/ksn_oracle.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Cross-verify a node-served key-state notice against the pinned keripy oracle. - -Reads a KERI key-state record (the `{vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di}` -wire shape) on stdin and reconstructs the SAME key-state inside keripy 1.3.4 (the -canonical KERI reference implementation, pinned in harness/versions.lock — the -identical oracle the interop suite uses). If the node's record is a conformant -KERI ksn, keripy's `eventing.state(...)` builds its native KeyStateRecord from the -node's fields and the result round-trips field-for-field (ignoring `dt`, which is -the controller's own clock). - -Exit 0 + "ORACLE-OK" when the node's notice IS what the oracle would publish for -the same key-state; non-zero with a located mismatch otherwise. This is the -node-serving half of IOP-L3c carried to the running witness: an implementation we -do not control agrees the node's KSN is a key-state notice. -""" - -import json -import sys - -from keri.core import eventing - - -def main() -> int: - rec = json.load(sys.stdin) - try: - ee = rec["ee"] - ksr = eventing.state( - pre=rec["i"], - sn=int(rec["s"], 16), - pig=rec["p"], - dig=rec["d"], - fn=int(rec["f"], 16), - eilk=rec["et"], - keys=rec["k"], - eevt=eventing.StateEstEvent(s=ee["s"], d=ee["d"], br=ee["br"], ba=ee["ba"]), - sith=rec["kt"], - ndigs=rec["n"], - nsith=rec["nt"], - toad=int(rec["bt"], 16), - wits=rec["b"], - cnfg=rec["c"], - dpre=rec["di"] or None, - ) - except Exception as exc: # noqa: BLE001 — any failure means the oracle rejects it - print(f"ORACLE-REJECT keripy could not build a key-state from the record: {exc}") - return 1 - - oracle = ksr._asdict() - node = dict(rec) - node.pop("dt", None) - orc = dict(oracle) - orc.pop("dt", None) - - if list(node.keys()) != list(orc.keys()): - print(f"FIELD-MISMATCH node={list(node.keys())} oracle={list(orc.keys())}") - return 1 - for k in node: - if node[k] != orc[k]: - print(f"VALUE-MISMATCH field={k} node={node[k]!r} oracle={orc[k]!r}") - return 1 - print("ORACLE-OK") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.recurve/claims/network/harness/rebuild.sh b/.recurve/claims/network/harness/rebuild.sh deleted file mode 100755 index 8ae7ab9..0000000 --- a/.recurve/claims/network/harness/rebuild.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# harness/rebuild.sh — produce the artifacts this suite's probes read. -# -# The probes here read the FEATURE-ENABLED `auths` build: the `auths witness` -# operator verbs with their real handlers compiled in (`--features -# witness-node`). That build is DISTINCT from the lean default `auths` the demos -# read, so it is built into its OWN target dir and copied into the suite's -# bin/auths — the lean `target/release/auths` the demos content-hash against is -# never clobbered by this suite's rebuild. -# -# Mirrors interop/demos: bring-up/build is the harness's job; probes are hermetic -# and only READ the copied artifact. recurve's [reads.cli] content-hash refuses -# to run a probe whose bin/auths drifts from this build output. -set -euo pipefail -. "$(dirname "$0")/env.sh" - -SUITE_BIN="$SUITE_DIR/bin" -# The feature-enabled build's own target dir — keeps the lean default build -# (target/release/auths, read by the demos) untouched. -WITNESS_TARGET="$AUTHS_SRC/target/witness-node" - -command -v cargo >/dev/null 2>&1 || die "cargo not found — install Rust via rustup (https://rustup.rs)" - -say "building the feature-enabled auths (--features witness-node) into target/witness-node" -( cd "$AUTHS_SRC" && cargo build --release -p auths-cli --features witness-node \ - --target-dir "$WITNESS_TARGET" ) - -built="$WITNESS_TARGET/release/auths" -[ -x "$built" ] || die "build produced no auths binary at $built" - -mkdir -p "$SUITE_BIN" -cp -f "$built" "$SUITE_BIN/auths" -pass "bin/auths is the feature-enabled build ($(cd "$AUTHS_SRC" && git rev-parse --short HEAD 2>/dev/null || echo local))" diff --git a/.recurve/claims/network/harness/up.sh b/.recurve/claims/network/harness/up.sh deleted file mode 100755 index 9d3c006..0000000 --- a/.recurve/claims/network/harness/up.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# harness/up.sh — bring the local 3-witness fixture up idempotently. -# -# Builds the platform witness image (musl-static auths-witness, from the shared -# tree's canonical deployment Dockerfile) and boots three nodes with distinct -# identities. Idempotent: if the three are already healthy this returns fast. -# Mirrors interop/peers/up.sh — bring-up is the harness's job, never a probe's -# (probes are hermetic and only READ the running fixture). -set -euo pipefail -. "$(dirname "$0")/env.sh" - -if ! command -v docker >/dev/null 2>&1; then - echo "BROKEN: docker CLI absent — the 3-witness fixture needs Docker." >&2 - exit 2 -fi -if ! docker info >/dev/null 2>&1; then - echo "BROKEN: Docker daemon down — start Docker Desktop (open -a Docker)." >&2 - exit 2 -fi - -export AUTHS_SRC - -if all_nodes_healthy; then - pass "3-witness fixture already healthy on ports ${NODE_PORTS[*]}" - exit 0 -fi - -# One image build, shared by all three nodes — never three concurrent workspace -# compiles. ensure-image.sh owns the build (one source of truth, reused by the -# WIT-N1 standup probe); the compose file pins the same tag ($WITNESS_IMAGE). -bash "$HARNESS_DIR/ensure-image.sh" - -say "booting the three nodes from the shared image" -docker compose -p "$COMPOSE_PROJECT" -f "$HARNESS_COMPOSE/docker-compose.yml" up -d - -say "waiting for all three nodes to answer /health (≤120s)" -deadline=$(( $(date +%s) + 120 )) -until all_nodes_healthy; do - if [ "$(date +%s)" -ge "$deadline" ]; then - die "fixture did not become healthy within 120s — see: docker compose -p $COMPOSE_PROJECT logs" - fi - sleep 2 -done - -# Distinct identities are the whole point — surface them so bring-up is auditable. -for i in "${!NODE_NAMES[@]}"; do - aid="$(node_aid "${NODE_PORTS[$i]}")" - pass "${NODE_NAMES[$i]} healthy on :${NODE_PORTS[$i]} — identity ${aid}" -done diff --git a/.recurve/claims/network/harness/versions.lock b/.recurve/claims/network/harness/versions.lock deleted file mode 100644 index e8bd00c..0000000 --- a/.recurve/claims/network/harness/versions.lock +++ /dev/null @@ -1,19 +0,0 @@ -# Pin every oracle this suite's probes compare against, exactly: -# -# -# Convention reused from interop/peers/versions.lock: a peer upgrade is a -# reviewable change to this file, never a mystery failure. The witness network -# suite's conformance oracle is the canonical KERI reference implementation — -# the same one the interop suite pins — so a key-state notice or receipt served -# by a node here is checked against an implementation we do not control. - -native: - keripy: - # Canonical KERI reference impl (Python). The witness nodes' receipts and - # key-state notices are cross-verified against THIS version. `kli` is its CLI. - # Identical pin to interop/peers/versions.lock — one oracle, one version, - # across both conformance suites (no skew between interop and network). - package: keri - version: "1.3.4" - provenance: "pip (pyenv) — python3 -c 'import keri; print(keri.__version__)'" - role: "primary oracle (receipts, key-state notices)" diff --git a/.recurve/claims/network/probes/_contract.sh b/.recurve/claims/network/probes/_contract.sh deleted file mode 100644 index 07039fe..0000000 --- a/.recurve/claims/network/probes/_contract.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# The probe contract (frozen — see schema/gap.schema.json's engine): -# exit 0 GREEN the desired behavior is present -# exit 1 RED the desired behavior is absent (print ONE detail line a -# sculptor can treat as the spec: "ours=X oracle=Y") -# exit 2 BROKEN could not measure (missing oracle/fixture/build) -# anything else (crash, timeout) coerces to BROKEN — never to a verdict. -# -# Traps: when $TRAP_FIXTURE is set, the runner is feeding you a KNOWN-BAD -# counterexample from probes/.trap// — you MUST exit 1. -# A probe that has never been seen RED is not yet evidence. -# -# Probes are hermetic: build nothing, touch no sacred space, finish in -# seconds against already-built artifacts, no network unless the claim is -# about network. Oracles are pinned in harness/versions.lock. -green() { echo "${1:-behavior present}"; exit 0; } -red() { echo "${1:?print the one RED line of truth}"; exit 1; } -broken(){ echo "${1:-could not measure}"; exit 2; } diff --git a/.recurve/claims/network/probes/boot-1.sh b/.recurve/claims/network/probes/boot-1.sh deleted file mode 100755 index c682a72..0000000 --- a/.recurve/claims/network/probes/boot-1.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# BOOT-1 — the harness exists: the 3-witness local fixture boots with distinct -# identities, the kill-node failure-injection lever works, and the conformance -# oracle is pinned (and actually present at the pinned version). -# -# Behavioral, end to end. The desired behavior (GREEN) is: a real, running -# 3-node witness fixture that a later probe can stand on — three nodes, three -# DISTINCT identities, a working kill lever, and an oracle we can cross-check -# against. RED while no such fixture is wired/healthy. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The harness boots the fixture -# (harness/up.sh) — this probe is hermetic and only READS it. -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# When TRAP_FIXTURE is set the runner feeds a KNOWN-BAD roster from -# probes/boot-1.trap//health/*.json instead of the live fixture. The -# distinctness check below must reject it (exit RED). A roster of three nodes -# that are not three distinct identities is not a witness network — it is one -# operator wearing three hats, the oligopoly the diversity rule exists to stop. -ROSTER_SOURCE="live" -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -d "${TRAP_FIXTURE}/health" ] || broken "trap fixture has no health/ dir: ${TRAP_FIXTURE}" - ROSTER_SOURCE="${TRAP_FIXTURE}/health" -fi - -# ── 1. Harness assets are wired ────────────────────────────────────────────── -for f in harness/env.sh harness/up.sh harness/down.sh harness/kill-node.sh \ - harness/versions.lock harness/compose/docker-compose.yml; do - [ -f "$f" ] || red "harness asset missing: $f (the fixture is not wired)" -done -[ -x harness/up.sh ] && [ -x harness/kill-node.sh ] \ - || red "harness/up.sh and harness/kill-node.sh must be executable" - -# ── 2. The oracle is pinned AND present at the pinned version ───────────────── -pinned="$(oracle_version)" -[ -n "$pinned" ] || red "no keripy oracle pinned in harness/versions.lock" -if ! command -v kli >/dev/null 2>&1; then - broken "keripy oracle (kli) absent — cannot confirm the pinned version is the one installed" -fi -installed="$(python3 -c 'import keri; print(keri.__version__)' 2>/dev/null || true)" -[ -n "$installed" ] || broken "keripy importable check failed — oracle not measurable" -[ "$installed" = "$pinned" ] \ - || red "oracle drift — versions.lock pins keripy=$pinned but installed=$installed" - -# ── 3. Collect the node roster (live fixture, or the trap's counterexample) ─── -declare -a aids=() -if [ "$ROSTER_SOURCE" = "live" ]; then - if ! all_nodes_healthy; then - # The oracle is present (checked above) — the only thing absent is the - # standup itself. That is the behavior under test, so its absence is RED, - # not BROKEN. `harness/up.sh` is the bring-up that turns this GREEN. - red "no 3-witness fixture standing on ports ${NODE_PORTS[*]} — harness/up.sh has not brought up a healthy network" - fi - for port in "${NODE_PORTS[@]}"; do - aid="$(node_aid "$port")" - [ -n "$aid" ] || broken "node on :$port returned no identity in /health" - aids+=("$aid") - done -else - # Trap: read the three supplied health JSONs in name order. - while IFS= read -r hf; do - aid="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["witness_did"])' "$hf" 2>/dev/null || true)" - [ -n "$aid" ] || broken "trap health file has no witness_did: $hf" - aids+=("$aid") - done < <(find "$ROSTER_SOURCE" -maxdepth 1 -name '*.json' | sort) -fi - -# ── 4. Exactly three nodes, three DISTINCT identities ───────────────────────── -[ "${#aids[@]}" -eq 3 ] \ - || red "roster has ${#aids[@]} nodes, expected 3 (fixture is 3-of-3 capable)" -distinct="$(printf '%s\n' "${aids[@]}" | sort -u | wc -l | tr -d ' ')" -[ "$distinct" -eq 3 ] \ - || red "ours=${distinct}-distinct oracle=3-distinct — roster is not 3 independent identities: ${aids[*]}" - -# ── 5. The kill-node lever actually injects failure (live only) ─────────────── -# Behavioral proof the failure-injection helper works: stop node 1, confirm its -# port goes dark while the other two stay healthy (a 2-of-3 set survives one -# loss), then restore it so the fixture is left as we found it. -if [ "$ROSTER_SOURCE" = "live" ]; then - if ! bash harness/kill-node.sh 1 stop >/dev/null 2>&1; then - broken "kill-node.sh could not stop node 1" - fi - restore() { bash harness/kill-node.sh 1 start >/dev/null 2>&1 || true; } - # node 1 dark - if node_health "${NODE_PORTS[0]}" >/dev/null 2>&1; then - restore; red "kill-node stopped node 1 but :${NODE_PORTS[0]} still answers — failure injection is a no-op" - fi - # nodes 2 and 3 still up (threshold-survives-one-loss) - if ! node_health "${NODE_PORTS[1]}" >/dev/null 2>&1 || ! node_health "${NODE_PORTS[2]}" >/dev/null 2>&1; then - restore; broken "killing node 1 took down a peer — fixture is not independent" - fi - restore - # and it comes back (a stop is recoverable, not a teardown) - deadline=$(( $(date +%s) + 30 )) - until node_health "${NODE_PORTS[0]}" >/dev/null 2>&1; do - [ "$(date +%s)" -ge "$deadline" ] && red "node 1 did not recover after kill-node.sh 1 start" - sleep 1 - done -fi - -green "harness GREEN: 3 distinct witness nodes healthy (${aids[*]}), kill-node lever proven, oracle keripy=$pinned pinned and installed" diff --git a/.recurve/claims/network/probes/boot-1.trap/README.md b/.recurve/claims/network/probes/boot-1.trap/README.md deleted file mode 100644 index 1544882..0000000 --- a/.recurve/claims/network/probes/boot-1.trap/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# boot-1 trap — counterexamples the probe must turn RED - -A probe that has never been seen RED is not yet evidence. These fixtures feed -the boot-1 probe a known-bad node roster (via `TRAP_FIXTURE`) that it MUST -reject. - -- `cloned-identity/` — three node `/health` responses that all advertise the - SAME `witness_did`. Three nodes are not a witness network if they are one - operator's three clones; the diversity the threshold story rests on is - absent. The probe asserts three DISTINCT identities, so it exits RED here. - If this ever goes GREEN, the harness would be blessing an oligopoly disguised - as a network — the exact failure the suite exists to prevent. diff --git a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit1.json b/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit1.json deleted file mode 100644 index 9955e28..0000000 --- a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit1.json +++ /dev/null @@ -1 +0,0 @@ -{"status":"ok","witness_did":"did:keri:BClonedOperatorIdentityAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","first_seen_count":0,"receipt_count":0} diff --git a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit2.json b/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit2.json deleted file mode 100644 index 9955e28..0000000 --- a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit2.json +++ /dev/null @@ -1 +0,0 @@ -{"status":"ok","witness_did":"did:keri:BClonedOperatorIdentityAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","first_seen_count":0,"receipt_count":0} diff --git a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit3.json b/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit3.json deleted file mode 100644 index 9955e28..0000000 --- a/.recurve/claims/network/probes/boot-1.trap/cloned-identity/health/wit3.json +++ /dev/null @@ -1 +0,0 @@ -{"status":"ok","witness_did":"did:keri:BClonedOperatorIdentityAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","first_seen_count":0,"receipt_count":0} diff --git a/.recurve/claims/network/probes/boot-2.sh b/.recurve/claims/network/probes/boot-2.sh deleted file mode 100755 index 329c1d4..0000000 --- a/.recurve/claims/network/probes/boot-2.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# BOOT-2 — the skeleton builds: the suite's rebuild produces the artifacts the -# probes read, and that build is the FEATURE-ENABLED `auths` (the `auths witness` -# operator verbs with their real node handlers compiled in via the witness-node -# feature), additive over the lean default. -# -# Behavioral, end to end. GREEN means: bin/auths exists and is a real auths -# binary whose `witness` surface carries the operator verbs (up/down/status/ -# register/logs); that surface compiles into the DEFAULT build too (thin clap), -# but the node dependency is feature-gated so the default `cargo tree` pulls NO -# auths-witness-node (WIT-B2 additive); and the node crate COMPOSES the platform -# crates rather than reimplementing protocol (WIT-B1). -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The suite's rebuild (recurve [suites. -# network] rebuild → harness/rebuild.sh) produces bin/auths; this probe is -# hermetic and only READS it. `reads: cli` makes recurve refuse to run this -# probe on a stale bin/auths. -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh - -RECURVE_TOML="$RECURVE_DIR/recurve.toml" -WITNESS_NODE_CRATE="$AUTHS_SRC/crates/auths-witness-node" - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# A trap fixture supplies a KNOWN-BAD `cargo tree` of the DEFAULT auths-cli build -# at probes/boot-2.trap//default-tree.txt. If that tree pulls -# auths-witness-node, the feature is NOT additive (the node's heavy deps leaked -# into the lean default install) — the additivity check below MUST reject it -# (exit RED). A default build that drags the node crate in is the lean install -# stopped being lean: exactly the regression WIT-B2 exists to catch. -DEFAULT_TREE_SOURCE="live" -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -f "${TRAP_FIXTURE}/default-tree.txt" ] \ - || broken "trap fixture has no default-tree.txt: ${TRAP_FIXTURE}" - DEFAULT_TREE_SOURCE="${TRAP_FIXTURE}/default-tree.txt" -fi - -# ── 1. The suite rebuild + freshness rule are wired ────────────────────────── -grep -q 'rebuild = "bash claims/network/harness/rebuild.sh"' "$RECURVE_TOML" \ - || red "recurve.toml [suites.network] rebuild is not wired to harness/rebuild.sh — nothing produces the artifacts probes read" -grep -q '^\[reads.cli\]' "$RECURVE_TOML" \ - || red "recurve.toml has no [reads.cli] freshness rule — a cli probe could run stale" -grep -q 'source = "target/witness-node/release/auths"' "$RECURVE_TOML" \ - || red "[reads.cli] does not source the FEATURE-ENABLED build (target/witness-node/release/auths)" -[ -x ./harness/rebuild.sh ] || red "harness/rebuild.sh missing or not executable — the rebuild command cannot run" - -# ── 2. The rebuild produced bin/auths ──────────────────────────────────────── -AUTHS_BIN="./bin/auths" -[ -x "$AUTHS_BIN" ] \ - || red "no bin/auths — the suite rebuild has not run (recurve rebuild network → harness/rebuild.sh)" - -# ── 3. bin/auths is a real auths binary with the operator verb surface ─────── -"$AUTHS_BIN" --version >/dev/null 2>&1 \ - || broken "bin/auths does not run as an auths binary" -help="$("$AUTHS_BIN" witness --help 2>&1 || true)" -for verb in up down status register logs; do - printf '%s\n' "$help" | grep -qiw "$verb" \ - || red "bin/auths 'witness' surface is missing the operator verb '$verb' — the skeleton CLI is incomplete: ${help}" -done - -# ── 4. The witness-node crate exists and COMPOSES the platform crates (WIT-B1) ─ -[ -f "$WITNESS_NODE_CRATE/Cargo.toml" ] \ - || red "auths-witness-node crate absent — the feature-gated node crate is the skeleton's core" -for dep in auths-witness auths-keri auths-verifier; do - grep -q "$dep" "$WITNESS_NODE_CRATE/Cargo.toml" \ - || red "auths-witness-node does not depend on $dep — it must COMPOSE the platform crates, not reimplement them" -done - -# ── 5. The feature is ADDITIVE — default cargo tree pulls NO node crate (WIT-B2) -# Live: ask cargo for the DEFAULT auths-cli dependency tree. Trap: read the -# supplied counterexample tree instead. -if [ "$DEFAULT_TREE_SOURCE" = "live" ]; then - command -v cargo >/dev/null 2>&1 || broken "cargo absent — cannot measure default-build additivity" - default_tree="$( ( cd "$AUTHS_SRC" && cargo tree -p auths-cli 2>/dev/null ) || true )" - [ -n "$default_tree" ] || broken "cargo tree -p auths-cli produced nothing — cannot measure additivity" - feature_tree="$( ( cd "$AUTHS_SRC" && cargo tree -p auths-cli --features witness-node 2>/dev/null ) || true )" -else - default_tree="$(cat "$DEFAULT_TREE_SOURCE")" - feature_tree="auths-witness-node" # the trap only constrains the DEFAULT tree -fi - -if printf '%s\n' "$default_tree" | grep -q 'auths-witness-node'; then - red "default auths-cli build pulls auths-witness-node — the witness-node feature is NOT additive; the lean install stopped being lean (WIT-B2)" -fi -printf '%s\n' "$feature_tree" | grep -q 'auths-witness-node' \ - || red "--features witness-node does NOT pull auths-witness-node — the feature is wired wrong" - -green "skeleton builds: bin/auths is the feature-enabled build (witness up/down/status/register/logs present); auths-witness-node composes auths-witness/auths-keri/auths-verifier and is additive (absent from the default cargo tree, present under --features witness-node)" diff --git a/.recurve/claims/network/probes/boot-2.trap/README.md b/.recurve/claims/network/probes/boot-2.trap/README.md deleted file mode 100644 index d28c865..0000000 --- a/.recurve/claims/network/probes/boot-2.trap/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# boot-2 trap — the witness-node feature stopped being additive - -`node-in-default-tree/default-tree.txt` is a `cargo tree -p auths-cli` of the -**default** build in which `auths-witness-node` appears as a dependency. That is -the WIT-B2 regression: a default `auths` build dragging the node crate (and its -heavy deps) into the lean install. The BOOT-2 probe MUST turn RED on this -fixture — a green here would mean the additivity guard blessed its own -counterexample, the highest-order gate failure. - -The probe consumes this fixture via `TRAP_FIXTURE`: it reads `default-tree.txt` -in place of the live `cargo tree`, then runs the same additivity assertion. diff --git a/.recurve/claims/network/probes/boot-2.trap/node-in-default-tree/default-tree.txt b/.recurve/claims/network/probes/boot-2.trap/node-in-default-tree/default-tree.txt deleted file mode 100644 index af86947..0000000 --- a/.recurve/claims/network/probes/boot-2.trap/node-in-default-tree/default-tree.txt +++ /dev/null @@ -1,9 +0,0 @@ -auths-cli v0.1.3 (/auths/crates/auths-cli) -├── auths-keri v0.1.3 (/auths/crates/auths-keri) -├── auths-sdk v0.1.3 (/auths/crates/auths-sdk) -│ └── auths-core v0.1.3 (/auths/crates/auths-core) -├── auths-verifier v0.1.3 (/auths/crates/auths-verifier) -└── auths-witness-node v0.1.3 (/auths/crates/auths-witness-node) - ├── auths-witness v0.1.3 (/auths/crates/auths-witness) - ├── auths-keri v0.1.3 (/auths/crates/auths-keri) - └── auths-verifier v0.1.3 (/auths/crates/auths-verifier) diff --git a/.recurve/claims/network/probes/boot-3.sh b/.recurve/claims/network/probes/boot-3.sh deleted file mode 100755 index 5979f0d..0000000 --- a/.recurve/claims/network/probes/boot-3.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# BOOT-3 — every authored probe can run: the full probe set executes on the -# built tree and EVERY verdict is a decision (RED or GREEN), never BROKEN. -# -# Why this is a real claim, not bookkeeping: a probe that exits 2 (BROKEN) — or -# times out, or crashes — did not measure its claim. The runner maps all of -# those to BROKEN and the matrix counts them, but a suite whose baseline is full -# of BROKEN has no baseline at all: the burndown cannot start, because "is this -# behavior present?" has no answer for those gaps. This probe is the gate that a -# fresh agent inherits a CLEAN baseline — every other authored probe decided, -# so the first sculpt cycle faces RED/GREEN, never a wall of "could not measure". -# -# Behavioral, end to end. GREEN means: each authored sibling probe -# (probes/*.sh, minus this meta-probe and the sourced _contract.sh) ran against -# the already-built tree and exited 0 or 1 — a real verdict. RED means at least -# one came back BROKEN (exit 2 / timeout / crash / 127), naming the offender and -# the prerequisite it is missing, which is exactly the env/fixture gap this -# claim exists to close. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. This probe is hermetic — it runs -# already-authored sibling probes against already-built artifacts; it builds -# nothing itself. (The siblings are hermetic too, by the same contract, so the -# meta-run stays in seconds.) -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -# env.sh sets `errexit`; this probe DELIBERATELY runs sibling probes that are -# expected to exit non-zero (RED is exit 1, and the trap's sibling exits 2) and -# inspects their codes by hand. errexit would abort the meta-run on the first -# such exit — exactly the verdicts we exist to count. Turn it off. -set +e - -PROBES_DIR="./probes" -SELF="$(basename "$0")" # boot-3.sh — never run ourselves (infinite recursion) -# Anything other than 0/1 is "could not decide". The runner coerces timeout -# (124), signals, and 127 to BROKEN the same way; we treat them identically. -is_decision() { [ "$1" -eq 0 ] || [ "$1" -eq 1 ]; } - -# A bound on each hermetic sibling so one hung probe can't wedge the meta-run. -# Portable: GNU `timeout` / Homebrew `gtimeout` where present (Linux CI), and a -# no-op passthrough on a bare macOS box — the siblings are hermetic by contract -# (seconds), and the recurve runner's own outer timeout is the backstop. -if command -v timeout >/dev/null 2>&1; then - run_bounded() { timeout 110 "$@"; } -elif command -v gtimeout >/dev/null 2>&1; then - run_bounded() { gtimeout 110 "$@"; } -else - run_bounded() { "$@"; } -fi - -# ── Assemble the probe set under test ──────────────────────────────────────── -# Live: every authored sibling probe on disk. Trap: the KNOWN-BAD probe set the -# runner hands us via TRAP_FIXTURE — a probe that cannot decide. A clean -# baseline that swallowed a BROKEN sibling would be a baseline built on a lie; -# the trap proves this probe still turns RED when a sibling cannot measure. -declare -a probe_set=() -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -d "${TRAP_FIXTURE}" ] || broken "trap fixture dir absent: ${TRAP_FIXTURE}" - while IFS= read -r p; do probe_set+=("$p"); done \ - < <(find "${TRAP_FIXTURE}" -maxdepth 1 -name '*.sh' | sort) - [ "${#probe_set[@]}" -gt 0 ] \ - || broken "trap fixture holds no *.sh probe: ${TRAP_FIXTURE}" -else - while IFS= read -r p; do - base="$(basename "$p")" - [ "$base" = "$SELF" ] && continue # no self-recursion - [ "$base" = "_contract.sh" ] && continue # sourced helper, not a probe - probe_set+=("$p") - done < <(find "$PROBES_DIR" -maxdepth 1 -name '*.sh' | sort) - # If the only authored probe is this one, there is nothing to attest to — - # that is an un-set-up suite, not a clean baseline. Fail closed. - [ "${#probe_set[@]}" -gt 0 ] \ - || broken "no sibling probes found under $PROBES_DIR — nothing to attest a clean baseline over" -fi - -# ── Run each probe; a verdict that is not a decision is the RED line ────────── -# We run WITHOUT TRAP_FIXTURE in the child env so each sibling exercises its -# real (live) path — we are measuring whether the authored probes can decide on -# the built tree, not re-running their traps. -declare -a broken_probes=() -for probe in "${probe_set[@]}"; do - # Run each sibling in a clean subshell with its real (live) path: no - # TRAP_FIXTURE (we are measuring the live verdict, not re-running its trap), - # NO_COLOR for stable output. `run_bounded` caps a hung probe where a - # timeout tool exists. - ( unset TRAP_FIXTURE RECURVE_PROBE; export NO_COLOR=1 - run_bounded bash "$probe" >/dev/null 2>&1 ) - code=$? - is_decision "$code" || broken_probes+=("$(basename "$probe"):exit=$code") -done - -if [ "${#broken_probes[@]}" -gt 0 ]; then - red "ours=BROKEN baseline expected=all-decide — $(( ${#broken_probes[@]} )) authored probe(s) could not measure on the built tree (exit≠0,1): ${broken_probes[*]} — fix the missing env/fixture prerequisite so the baseline is RED/GREEN, never BROKEN" -fi - -green "clean baseline: ${#probe_set[@]} authored probe(s) ran on the built tree and every one returned a decision (RED or GREEN), zero BROKEN — the burndown can start" diff --git a/.recurve/claims/network/probes/boot-3.trap/README.md b/.recurve/claims/network/probes/boot-3.trap/README.md deleted file mode 100644 index efaba5f..0000000 --- a/.recurve/claims/network/probes/boot-3.trap/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# boot-3 trap — a baseline that swallowed a BROKEN probe - -A probe that has never been seen RED is not yet evidence. This fixture feeds the -BOOT-3 meta-probe a known-bad probe set (via `TRAP_FIXTURE`) that it MUST -reject. - -- `broken-sibling/cannot-measure.sh` — a sibling probe that exits **2 - (BROKEN)**: it announces it could not measure (missing oracle/fixture/build) - and returns the undecidable verdict. This is exactly the verdict BOOT-3 exists - to forbid in a clean baseline. - -BOOT-3 consumes the fixture by running every `*.sh` under `$TRAP_FIXTURE` -instead of its live sibling set, then applies the same decision check: a probe -that did not exit 0 or 1 is a BROKEN baseline. So it exits **RED** here. - -If this ever went GREEN, BOOT-3 would be certifying a baseline as clean while a -sibling silently could-not-measure — the burndown would start on a lie, the one -failure this claim exists to prevent. diff --git a/.recurve/claims/network/probes/boot-3.trap/broken-sibling/cannot-measure.sh b/.recurve/claims/network/probes/boot-3.trap/broken-sibling/cannot-measure.sh deleted file mode 100755 index 86870b7..0000000 --- a/.recurve/claims/network/probes/boot-3.trap/broken-sibling/cannot-measure.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# A KNOWN-BAD sibling: a probe that cannot decide. It exits 2 (BROKEN) — the -# exact verdict BOOT-3 exists to forbid in a clean baseline. A probe missing its -# oracle/fixture/build looks exactly like this from the runner's side: it -# announces it could not measure and exits 2. -# -# BOOT-3, handed this fixture via TRAP_FIXTURE, must NOTICE the BROKEN exit and -# turn RED. If BOOT-3 ever went GREEN here, it would be certifying a baseline as -# clean while a sibling silently could-not-measure — the burndown would start on -# a lie. That green is the highest-order gate failure this trap guards against. -echo "could not measure: oracle/fixture/build prerequisite is absent (this is the BROKEN a clean baseline must not contain)" -exit 2 diff --git a/.recurve/claims/network/probes/fixtures/icp-event.json b/.recurve/claims/network/probes/fixtures/icp-event.json deleted file mode 100644 index e1aee2b..0000000 --- a/.recurve/claims/network/probes/fixtures/icp-event.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"KERI10JSON000000_","t":"icp","d":"EPVOwKzgOeQ2rD5nv_fXzD036LBYcIgyaD3AgD0ghToU","i":"EProbeControllerWITN2000000000000000000000000","s":0,"k":["DBIN0lwDJqlOtNlK3iZu-9_Y5fTNAUl7OPaQSZvLoGSn"],"x":"3f0c7b577d015f4aa3fdd0077e485f05ad3cf0c71a0221d6ca2400cc5dac4d6e01aff1b1f828cffb5ae44c9c20bfb7be4dc5d03bf1bfb999458e8cf39d3a2e01"} diff --git a/.recurve/claims/network/probes/fixtures/keri-icp.json b/.recurve/claims/network/probes/fixtures/keri-icp.json deleted file mode 100644 index 2e41f65..0000000 --- a/.recurve/claims/network/probes/fixtures/keri-icp.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"KERI10JSON000000_","t":"icp","d":"EEo2wEOllS9yyvdmbdMayh5v1EUlG_FcgUQ74Og3KIVO","i":"EEo2wEOllS9yyvdmbdMayh5v1EUlG_FcgUQ74Og3KIVO","s":"0","kt":"1","k":["DB7_u05LWr6ubmkc_MFHEdTsvC4NPaGOBXJA1VUUhhky"],"nt":"1","n":["EChIyzn-jbMOdiLm78-cahnwaN59g9B55GIKpSI8MnD_"],"bt":"0","b":[],"c":[],"a":[],"x":"c0b57d238ab089de198d6e18a8acf6c041404b8ee59cff0b1914ab69783d9bc47b4482eb8a14c06ae80965039d0c66e05a3539bd12cbd09ec22c88398dacfb05"} diff --git a/.recurve/claims/network/probes/fixtures/receipt-bundle.json b/.recurve/claims/network/probes/fixtures/receipt-bundle.json deleted file mode 100644 index c4ad073..0000000 --- a/.recurve/claims/network/probes/fixtures/receipt-bundle.json +++ /dev/null @@ -1 +0,0 @@ -{"receipt":{"receipt":{"v":"KERI10JSON000000_","t":"rct","d":"EPVOwKzgOeQ2rD5nv_fXzD036LBYcIgyaD3AgD0ghToU","i":"EProbeControllerWITN2000000000000000000000000","s":"0"},"signature":"3828f4b8c9156f3603060e3c71f38324bc968ea6547fe3144ee059f3219879e9dd824d49db705478d71c0b597d5bc30ff53f79841266ded9275782259f5b270c"},"witness":"did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S"} diff --git a/.recurve/claims/network/probes/wit-n1.sh b/.recurve/claims/network/probes/wit-n1.sh deleted file mode 100755 index bcad8d6..0000000 --- a/.recurve/claims/network/probes/wit-n1.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env bash -# WIT-N1 — one command, one witness: `auths witness up` takes a clean box to a -# HEALTHY node and tells the truth about it. The headline standup claim. -# -# Behavioral, end to end. GREEN means: `auths witness up` brought a witness node -# up, printed a health URL, and exited 0 — AND that URL actually answers (a real -# node is serving), with zero protocol vocabulary in the happy-path output. -# RED means the standup did not reach a healthy node (the behavior is absent). -# BROKEN means we could not even attempt the measurement (no bin/auths). -# -# The load-bearing distinction this probe enforces: `up` exiting 0 is NOT -# success — a node answering its health URL is. An `up` that prints a health URL -# and exits 0 while nothing listens there is the lie WIT-N1 exists to forbid; -# this probe reconciles the two and is RED until they agree. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The probe drives `auths witness up` -# against a throwaway data dir + free port and reads the result; it tears its -# own node down on exit (hermetic — leaves no fixture behind). -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -set +e # we inspect exit codes of commands expected to fail; errexit would abort - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# A trap fixture supplies a KNOWN-BAD captured `witness up` run at -# probes/wit-n1.trap//{up.out,up.code}: an `up` that exited 0 and -# printed a health URL while the port was already taken — a partial-state lie -# (it claimed success without standing a fresh node up). The probe MUST turn RED -# on it: a printed health URL with a non-zero exit OR a stale answerer is not a -# successful standup. This is the occupied-port adversarial twin, frozen. -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -f "${TRAP_FIXTURE}/up.out" ] && [ -f "${TRAP_FIXTURE}/up.code" ] \ - || broken "trap fixture missing up.out/up.code: ${TRAP_FIXTURE}" - up_out="$(cat "${TRAP_FIXTURE}/up.out")" - up_code="$(cat "${TRAP_FIXTURE}/up.code")" - # The trap encodes an occupied-port standup. Acceptable behavior is a - # non-zero exit with a single actionable line. A zero exit (claimed success - # on an occupied port) is the partial-state lie — RED. - if [ "$up_code" -eq 0 ]; then - red "ours=exit0-occupied-port expected=actionable-refusal — \`witness up\` claimed success while the port was already taken; that is partial state masquerading as a standup" - fi - lines="$(printf '%s\n' "$up_out" | grep -c .)" - [ "$lines" -le 3 ] \ - || red "ours=${lines}-line-error expected=one-line — occupied-port \`witness up\` must fail with a single actionable line: ${up_out}" - green "occupied-port standup refused cleanly (exit $up_code, ${lines} line(s)) — the adversarial twin holds" -fi - -AUTHS_BIN="./bin/auths" -[ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" -"$AUTHS_BIN" --version >/dev/null 2>&1 \ - || broken "bin/auths does not run as an auths binary — cannot attempt standup" - -# A free port + throwaway data dir for this probe's own ephemeral node. -PROBE_PORT="${WIT_N1_PORT:-3340}" -DATA_DIR="$(mktemp -d "${TMPDIR:-/tmp}/wit-n1.XXXXXX")" -cleanup() { - "$AUTHS_BIN" witness down --port "$PROBE_PORT" --data-dir "$DATA_DIR" >/dev/null 2>&1 - rm -rf "$DATA_DIR" 2>/dev/null -} -trap cleanup EXIT - -# Standup runs a RELEASED image (`image:`, never `build:`) — obtaining that image -# is the harness's job, not the probe's and not `up`'s. Ensure it is present -# (build-once from the platform's canonical deployment Dockerfile), so on a clean -# box `up` finds a real node image and the one-command-to-healthy behavior is -# measurable. If the engine is genuinely absent, ensure-image fails — the -# no-engine branch below still decides (the clean adversarial refusal). -if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then - bash ./harness/ensure-image.sh >/dev/null 2>&1 \ - || broken "the witness node image could not be made present (harness/ensure-image.sh failed) — cannot attempt the standup; this is a fixture prerequisite, not a verdict on \`up\`" -fi - -# ── 1. Run the one command ─────────────────────────────────────────────────── -# `up` runs the released image the harness just made present. The image is the -# operation's input, supplied by the operator/harness; `up` never builds it. -up_out="$("$AUTHS_BIN" witness up --port "$PROBE_PORT" --data-dir "$DATA_DIR" --image "$WITNESS_IMAGE" 2>&1)" -up_code=$? - -# Standup needs a container engine. If it is genuinely absent, `up` must say so -# in one actionable line and exit non-zero (the adversarial twin) — that is a -# DECISION (RED), not an inability to measure. We only go BROKEN if bin/auths -# itself can't run (handled above). -if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then - # No engine: the ONLY acceptable behavior is a clean, single-line refusal and - # a non-zero exit, with NO node left behind. An `up` that exits 0 here is the - # lie under test. - if [ "$up_code" -eq 0 ]; then - red "ours=exit0-no-engine expected=actionable-refusal — \`witness up\` claimed success with no container engine available; it must fail with one actionable line and stand nothing up" - fi - lines="$(printf '%s\n' "$up_out" | grep -c .)" - [ "$lines" -le 3 ] \ - || red "ours=${lines}-line-error expected=one-line — \`witness up\` without an engine must fail with a single actionable line, not a wall of text: ${up_out}" - green "no container engine present: \`witness up\` refused cleanly (exit $up_code, ${lines} line(s)) and stood nothing up — the adversarial twin holds; happy-path standup is unverifiable here but the behavior decided" -fi - -# ── 2. With an engine, exit 0 AND a health URL that actually answers ────────── -if [ "$up_code" -ne 0 ]; then - red "ours=exit${up_code} expected=exit0 — \`witness up\` did not complete a standup: $(printf '%s' "$up_out" | tail -1)" -fi - -# Extract the printed health URL — operators are told where to look; that URL is -# the contract. No URL printed is a standup that hid its result. -health_url="$(printf '%s\n' "$up_out" | grep -oE 'https?://[^[:space:]]+/health' | head -1)" -[ -n "$health_url" ] \ - || red "ours=no-health-url expected=printed-url — \`witness up\` exited 0 but printed no health URL for the operator to open: ${up_out}" - -# The lie gate, and the measurability gate. `up` exited 0 and named a health -# URL, so it is CLAIMING a standup succeeded. Poll that URL: if it answers, the -# claim is true (GREEN below). If it does NOT answer, `up` reported a success -# reality contradicts — the probe cannot decide whether the standup capability -# is real-but-slow or simply unfinished/faked, so the measurement is corrupted: -# BROKEN, not a verdict. (An `up` that cannot stand a node up must say so by -# exiting non-zero — handled above as a clean RED — never by claiming success.) -answered="" -deadline=$(( $(date +%s) + 20 )) -while [ "$(date +%s)" -lt "$deadline" ]; do - if curl -fsS --max-time 3 "$health_url" >/dev/null 2>&1; then answered="yes"; break; fi - sleep 1 -done -[ -n "$answered" ] \ - || broken "\`witness up\` exited 0 and printed ${health_url} but nothing answers there — the command claimed a standup that did not happen; cannot decide WIT-N1 against a build whose success is a lie (a real \`up\` must exit non-zero when it cannot stand a node up)" - -# ── 3. Zero protocol vocabulary in the happy path (carried from TTV/US-001) ─── -leak="$(printf '%s\n' "$up_out" | grep -ioE '\b(keri|kel|ksn|said|cesr|oobi)\b' | head -1 || true)" -[ -z "$leak" ] \ - || red "ours=leak:${leak} expected=no-protocol-vocab — happy-path \`witness up\` output leaked protocol vocabulary; operators must never see it" - -green "one command stood up a healthy witness: \`witness up\` exited 0, printed ${health_url}, the node answers there, and the happy path carries zero protocol vocabulary" diff --git a/.recurve/claims/network/probes/wit-n1.trap/README.md b/.recurve/claims/network/probes/wit-n1.trap/README.md deleted file mode 100644 index d5572dc..0000000 --- a/.recurve/claims/network/probes/wit-n1.trap/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# wit-n1 trap — `up` claimed success on an occupied port - -A probe that has never been seen RED is not yet evidence. This fixture feeds the -WIT-N1 probe a known-bad captured standup (via `TRAP_FIXTURE`) that it MUST -reject. - -- `occupied-port/` — a captured `witness up` run that **printed a health URL and - exited 0 while the port was already taken** (`up.out` + `up.code=0`). That is - the partial-state lie WIT-N1 forbids: `up` exiting 0 is not success; a fresh - node answering is. On an occupied port the only acceptable behavior is a - single actionable line and a non-zero exit, standing nothing up. - -The probe consumes the fixture by reading `up.out`/`up.code` in place of running -`up` live, then applies the same rule: a zero exit on an occupied port is RED. - -If this ever went GREEN, WIT-N1 would be blessing an `up` that reports success -without a healthy node — the exact failure the headline standup claim exists to -prevent. diff --git a/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.code b/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.code deleted file mode 100644 index 573541a..0000000 --- a/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.code +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.out b/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.out deleted file mode 100644 index 5173176..0000000 --- a/.recurve/claims/network/probes/wit-n1.trap/occupied-port/up.out +++ /dev/null @@ -1 +0,0 @@ -health: http://127.0.0.1:3333/health diff --git a/.recurve/claims/network/probes/wit-n2.sh b/.recurve/claims/network/probes/wit-n2.sh deleted file mode 100755 index d7de90c..0000000 --- a/.recurve/claims/network/probes/wit-n2.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# WIT-N2 — receipts verify offline on a stranger's machine; tampered receipts -# rejected. The corroboration claim: a witness receipt is worth nothing unless a -# third party who does NOT trust the node can check it alone — on a clean -# machine, with no network and no registry — and a tampered one fails closed. -# -# Behavioral, end to end. GREEN means: a real witness node receipted an event; -# the receipt + the node's published identity were carried into an isolated, -# no-network, no-registry context; `auths witness verify-receipt` verified the -# bundle there (exit 0); and flipping a single byte of the signature made the -# SAME command reject it (non-zero, distinct reason). RED means either the -# genuine receipt did not verify offline, or a tampered one was NOT rejected — -# both break the claim. BROKEN means we could not even attempt (no bin/auths). -# -# The load-bearing distinction: the witness's published identity is the ONLY -# trust input. It is a did:key that EMBEDS the witness's verification key, so the -# bundle is self-contained — no directory, no lookup, no second party. We prove -# "no registry" by pointing the verifier's home at an empty dir, and "no node -# needed" by tearing the fixture down before the offline verify runs. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The probe gets a real receipt from the -# harness fixture when Docker is up; with no engine it falls back to a captured -# real bundle so the offline-verify + tamper claim still DECIDES (the bundle is -# the node's product, verified the same way either path). Hermetic otherwise. -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -set +e # we inspect exit codes of commands expected to fail; errexit would abort - -AUTHS_BIN="./bin/auths" -FIXTURES="./probes/fixtures" - -# verify_bundle — run the offline verify in an ISOLATED context: -# HOME and the auths home redirected to an empty throwaway dir (no registry, no -# pinned witnesses), so a verify that succeeds proves the bundle is -# self-contained — the published identity inside it is the only trust input. -# The command is handed only a file path; it is given no node URL and reaches no -# network. Echoes the command's combined output; returns its exit code. -verify_bundle() { - local bundle="$1" - local empty_home - empty_home="$(mktemp -d "${TMPDIR:-/tmp}/wit-n2-home.XXXXXX")" - HOME="$empty_home" AUTHS_HOME="$empty_home" \ - "$AUTHS_BIN" witness verify-receipt --receipt "$bundle" 2>&1 - local code=$? - rm -rf "$empty_home" 2>/dev/null - return $code -} - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# A trap fixture supplies a KNOWN-BAD receipt bundle at -# probes/wit-n2.trap//bundle.json: a genuine receipt whose signature has -# been bit-flipped (a forged receipt). The runner feeds this as the bundle the -# probe's GREEN path treats as genuine — and the probe MUST turn RED, because a -# forged receipt does not verify. This proves the GREEN verdict genuinely -# depends on a valid signature: a probe that blessed this counterexample (called -# it "verified") would be one that never actually checks signatures. The trap -# stays RED forever. -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" - [ -f "${TRAP_FIXTURE}/bundle.json" ] \ - || broken "trap fixture missing bundle.json: ${TRAP_FIXTURE}" - out="$(verify_bundle "${TRAP_FIXTURE}/bundle.json")" - code=$? - # A trap MUST be RED. The forged bundle is fed where the GREEN path treats it - # as the genuine bundle; the probe's GREEN gate (exit 0 AND a "verified" line) - # must NOT pass on it. Either way the verdict is RED: the only question the - # trap settles is WHY — a probe that called this "verified" is broken (it does - # not check signatures), and one that rejected it is correct but the - # counterexample is still known-bad, so RED stands. - if [ "$code" -eq 0 ] && printf '%s\n' "$out" | grep -qi 'verified'; then - red "ours=verified-forged DANGER — the offline verify accepted a bit-flipped receipt as genuine; signatures are not actually being checked: ${out}" - fi - red "ours=forged-receipt expected=RED — a bit-flipped receipt is the known-bad counterexample; the offline verify correctly refused to bless it (exit $code), so this trap stays RED: ${out}" -fi - -[ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" -"$AUTHS_BIN" --version >/dev/null 2>&1 \ - || broken "bin/auths does not run as an auths binary — cannot attempt offline verify" - -# ── 1. Obtain a REAL receipt bundle ────────────────────────────────────────── -# The genuine bundle = a witness's signed receipt + the witness's PUBLISHED -# identity. With Docker, we have a live fixture node receipt the event and read -# its identity from /health — provenance is real. With no engine, we use the -# captured real bundle (the node's own product), so the offline-verify claim -# still decides on this box. -BUNDLE="$(mktemp "${TMPDIR:-/tmp}/wit-n2-bundle.XXXXXX.json")" -cleanup() { rm -f "$BUNDLE" 2>/dev/null; } -trap cleanup EXIT - -PROVENANCE="captured" -if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then - bash ./harness/up.sh >/dev/null 2>&1 \ - || broken "the 3-witness fixture could not be brought up (harness/up.sh failed) — cannot receipt an event against a live node; fixture prerequisite, not a verdict on the claim" - - EVENT_FILE="$FIXTURES/icp-event.json" - [ -f "$EVENT_FILE" ] || broken "missing event fixture $EVENT_FILE — cannot submit an event to receipt" - PREFIX="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["i"])' "$EVENT_FILE")" - PORT="${NODE_PORTS[0]}" - - # The live node receipts the event (idempotent: same event → same SAID → - # a fresh signed receipt every time) and advertises its identity at /health. - receipt_json="$(curl -fsS --max-time 5 -X POST -H 'content-type: application/json' \ - --data-binary "@$EVENT_FILE" "http://127.0.0.1:${PORT}/witness/${PREFIX}/event")" - [ -n "$receipt_json" ] \ - || red "ours=no-receipt expected=signed-receipt — a live node did not return a receipt for a valid event; receipting is the precondition for corroboration" - witness_did="$(curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/health" \ - | python3 -c 'import json,sys;print(json.load(sys.stdin)["witness_did"])' 2>/dev/null)" - [ -n "$witness_did" ] \ - || red "ours=no-identity expected=published-did — the node did not advertise its identity at /health; a receipt with no published identity cannot be checked by a stranger" - - python3 -c ' -import json, sys -receipt = json.loads(sys.argv[1]) -bundle = {"receipt": receipt, "witness": sys.argv[2]} -open(sys.argv[3], "w").write(json.dumps(bundle)) -' "$receipt_json" "$witness_did" "$BUNDLE" \ - || broken "could not assemble the receipt bundle from the live receipt + identity" - PROVENANCE="live node :${PORT} (${witness_did})" - # The fixture is the harness's to own (up/down); this probe only READS it and - # leaves it standing — the BOOT-1 probe and the rest of the suite depend on - # it being up. "No node needed" is proven below by the offline verify making - # NO contact with it, not by killing the shared network. -else - cp "$FIXTURES/receipt-bundle.json" "$BUNDLE" 2>/dev/null \ - || broken "no container engine AND no captured bundle fixture ($FIXTURES/receipt-bundle.json) — cannot obtain a receipt to verify" -fi - -# ── 2. The genuine receipt verifies OFFLINE, in an isolated context ────────── -out="$(verify_bundle "$BUNDLE")" -code=$? -[ "$code" -eq 0 ] \ - || red "ours=exit${code} expected=verified — a genuine receipt + the witness's published identity did NOT verify offline (no network, no registry); receipts are only corroboration if a stranger can check them alone [provenance: ${PROVENANCE}]: ${out}" -printf '%s\n' "$out" | grep -qi 'verified' \ - || red "ours=no-verified-line expected=verified — \`verify-receipt\` exited 0 but did not report the receipt verified: ${out}" - -# ── 3. A bit-flipped receipt is REJECTED, with a distinct reason ───────────── -TAMPERED="$(mktemp "${TMPDIR:-/tmp}/wit-n2-tampered.XXXXXX.json")" -python3 -c ' -import json, sys -b = json.load(open(sys.argv[1])) -sig = b["receipt"]["signature"] -# Flip the first nibble of the hex signature — one byte changed, nothing else. -first = "0" if sig[0] != "0" else "1" -b["receipt"]["signature"] = first + sig[1:] -open(sys.argv[2], "w").write(json.dumps(b)) -' "$BUNDLE" "$TAMPERED" \ - || { rm -f "$TAMPERED"; broken "could not synthesize the tampered receipt"; } - -tout="$(verify_bundle "$TAMPERED")" -tcode=$? -rm -f "$TAMPERED" 2>/dev/null -if [ "$tcode" -eq 0 ]; then - red "ours=verified-tampered expected=rejected — a bit-flipped receipt verified offline; a tampered receipt MUST fail closed or corroboration is worthless: ${tout}" -fi -printf '%s\n' "$tout" | grep -qiE 'reject|does not verify|altered' \ - || red "ours=opaque-rejection expected=distinct-reason — the tampered receipt was rejected (exit $tcode) but with no distinct reason a verifier can act on: ${tout}" - -green "receipts verify offline on a stranger's machine: a real witness receipt + the node's published identity verified with no network and no registry [provenance: ${PROVENANCE}], and a bit-flipped receipt was rejected with a distinct reason — the corroboration claim holds end to end" diff --git a/.recurve/claims/network/probes/wit-n2.trap/README.md b/.recurve/claims/network/probes/wit-n2.trap/README.md deleted file mode 100644 index 73e777a..0000000 --- a/.recurve/claims/network/probes/wit-n2.trap/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# wit-n2 trap — a forged receipt that must stay RED - -A probe that has never been seen RED is not yet evidence. This fixture feeds the -WIT-N2 probe a known-bad receipt bundle (via `TRAP_FIXTURE`) that the offline -verify MUST reject. - -- `forged-signature/bundle.json` — a **genuine** witness receipt and the - witness's real published identity, but with **one byte of the signature - flipped** (the first hex nibble). Everything else is authentic: the receipt - body, the `did:key` identity, the rest of the signature. This is a tampered - receipt — the kind a thief produces by editing a real one. - -The probe runs `auths witness verify-receipt` on the bundle in the same isolated -no-network, no-registry context it uses for the genuine path, and asserts a -non-zero exit. A zero exit here means the verifier accepted a forged receipt as -corroboration — exactly the failure WIT-N2 exists to forbid. If this ever goes -GREEN, receipts stop being worth anything: a stranger could be handed an altered -receipt and told it verified. diff --git a/.recurve/claims/network/probes/wit-n2.trap/forged-signature/bundle.json b/.recurve/claims/network/probes/wit-n2.trap/forged-signature/bundle.json deleted file mode 100644 index be45583..0000000 --- a/.recurve/claims/network/probes/wit-n2.trap/forged-signature/bundle.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "receipt": { - "receipt": { - "v": "KERI10JSON000000_", - "t": "rct", - "d": "EPVOwKzgOeQ2rD5nv_fXzD036LBYcIgyaD3AgD0ghToU", - "i": "EProbeControllerWITN2000000000000000000000000", - "s": "0" - }, - "signature": "0828f4b8c9156f3603060e3c71f38324bc968ea6547fe3144ee059f3219879e9dd824d49db705478d71c0b597d5bc30ff53f79841266ded9275782259f5b270c" - }, - "witness": "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S" -} \ No newline at end of file diff --git a/.recurve/claims/network/probes/wit-n3.sh b/.recurve/claims/network/probes/wit-n3.sh deleted file mode 100755 index ed3cd39..0000000 --- a/.recurve/claims/network/probes/wit-n3.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash -# WIT-N3 — the node serves a conformant key-state notice (KSN), cross-verified -# against the pinned keripy oracle, both directions. The wire-conformance claim: -# a thin client can trust an identity's current key-state from the node's notice -# alone, and an implementation we do NOT control (keripy 1.3.4) agrees that -# notice IS a key-state notice — and accepts ours / publishes one we accept. -# -# Behavioral, end to end. GREEN means: a live node, having witnessed a real -# controller inception, serves a KERI-conformant key-state record at a stable -# endpoint; that record reconstructs byte-for-byte inside the keripy oracle -# (node → oracle); a record the keripy oracle publishes ingests on the node -# (oracle → node); AND a stale notice (a lower sequence than a newer state the -# verifier holds) is detected as stale, not silently accepted. RED means any of -# those fails — the wire shape diverged, the oracle rejected it, or staleness -# went undetected. BROKEN means we could not even attempt (no bin/auths, no -# keripy oracle, or the fixture could not stand up). -# -# This extends interop's IOP-L3c (the KSN wire shape, owned and cross-verified -# there) to the RUNNING node: the same oracle, the same versions.lock pin, now -# against a notice a live witness actually served from events it corroborated. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The probe reuses the harness 3-witness -# fixture (up.sh) and leaves it standing for the rest of the suite — it only -# READS the running node, submitting one throwaway controller's events. -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -set +e # we inspect exit codes of commands expected to fail; errexit would abort - -AUTHS_BIN="./bin/auths" -FIXTURES="./probes/fixtures" -ORACLE="./harness/ksn_oracle.py" -EMIT="./harness/ksn_emit.py" - -# oracle_present — 0 if the pinned keripy oracle is importable, else non-zero. -# The conformance claim is meaningless without the implementation we compare to. -oracle_present() { python3 -c 'import keri' >/dev/null 2>&1; } - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# A trap fixture supplies a KNOWN-BAD stale key-state record at -# probes/wit-n3.trap//{ksn.json,last_seen}: a notice at sequence S fed -# to a verifier that already trusts a NEWER state (last_seen > S). The node's -# staleness gate MUST reject it. The probe's GREEN path treats `--reject-stale- -# below` accepting a notice as "fresh"; here it must instead fail closed, so the -# probe turns RED — proving the freshness check genuinely depends on the -# sequence, not a rubber stamp. A verifier that accepted this stale notice would -# trust a rewound, replayed view of the identity. The trap stays RED forever. -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" - [ -f "${TRAP_FIXTURE}/ksn.json" ] && [ -f "${TRAP_FIXTURE}/last_seen" ] \ - || broken "trap fixture missing ksn.json/last_seen: ${TRAP_FIXTURE}" - last_seen="$(cat "${TRAP_FIXTURE}/last_seen")" - out="$("$AUTHS_BIN" key-state --ingest "${TRAP_FIXTURE}/ksn.json" --reject-stale-below "$last_seen" 2>&1)" - code=$? - if [ "$code" -eq 0 ]; then - red "ours=accepted-stale DANGER — the node ingested a stale key-state notice (seq below last-seen ${last_seen}) as fresh; a rewound/replayed view was trusted: ${out}" - fi - printf '%s\n' "$out" | grep -qiE 'stale|reject' \ - || red "ours=opaque-stale-rejection expected=distinct-reason — the stale notice was refused (exit $code) but without a reason a verifier can act on: ${out}" - red "ours=stale-ksn expected=RED — a stale key-state notice (seq below last-seen ${last_seen}) is the known-bad counterexample; the freshness gate correctly refused it (exit $code), so this trap stays RED: ${out}" -fi - -[ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" -"$AUTHS_BIN" --version >/dev/null 2>&1 \ - || broken "bin/auths does not run as an auths binary — cannot attempt the KSN conformance check" -oracle_present \ - || broken "the pinned keripy oracle ($(oracle_version)) is not importable (python3 -c 'import keri') — the KSN conformance claim has no oracle to compare against; install it (see harness/versions.lock)" - -# ── 1. A live node witnesses a real controller inception ───────────────────── -# The node can only notice a key-state it actually saw. We need a live fixture -# node and a full, conformant inception for it to witness, then serve. -command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 \ - || broken "no container engine — the KSN claim is about a RUNNING node serving a notice; without a live node there is nothing to cross-verify (fixture prerequisite, not a verdict)" - -bash ./harness/up.sh >/dev/null 2>&1 \ - || broken "the 3-witness fixture could not be brought up (harness/up.sh failed) — cannot have a live node witness an event; fixture prerequisite, not a verdict on the claim" - -EVENT_FILE="$FIXTURES/keri-icp.json" -[ -f "$EVENT_FILE" ] || broken "missing event fixture $EVENT_FILE — cannot submit a conformant inception to witness" -PREFIX="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["i"])' "$EVENT_FILE")" -PORT="${NODE_PORTS[0]}" -NODE="http://127.0.0.1:${PORT}" - -# The live node witnesses the inception (idempotent — first-seen wins; re-runs of -# this probe re-submit the same event and get the same receipt). -receipt_json="$(curl -fsS --max-time 5 -X POST -H 'content-type: application/json' \ - --data-binary "@$EVENT_FILE" "${NODE}/witness/${PREFIX}/event")" -[ -n "$receipt_json" ] \ - || red "ours=no-receipt expected=witnessed — a live node did not witness a valid conformant inception; it cannot notice a key-state it never saw" - -# ── 2. The node serves a key-state notice for the identity it witnessed ────── -ksn="$(curl -fsS --max-time 5 "${NODE}/witness/${PREFIX}/key-state")" -ksn_code=$? -[ "$ksn_code" -eq 0 ] && [ -n "$ksn" ] \ - || red "ours=no-ksn-endpoint expected=served-notice — the node did not serve a key-state notice at /witness/${PREFIX}/key-state (curl exit $ksn_code); a thin client has nothing to trust" - -# The served record must be the KERI ksn wire shape, not the auths-internal -# envelope — labels and order are the conformance surface. -fields="$(printf '%s' "$ksn" | python3 -c 'import json,sys;print(",".join(json.load(sys.stdin).keys()))' 2>/dev/null)" -expected="vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di" -[ "$fields" = "$expected" ] \ - || red "ours=fields:[${fields}] expected=[${expected}] — the served notice is not the KERI ksn wire record shape; a peer cannot read it" - -# ── 3. node → oracle: keripy agrees the node's notice IS a key-state notice ── -oracle_out="$(printf '%s' "$ksn" | python3 "$ORACLE" 2>&1)" -oracle_code=$? -[ "$oracle_code" -eq 0 ] && printf '%s\n' "$oracle_out" | grep -q 'ORACLE-OK' \ - || red "ours=oracle-rejected expected=ORACLE-OK — the keripy oracle ($(oracle_version)) did not reconstruct the node's served key-state record (the node's notice is not conformant): ${oracle_out}" - -# ── 4. oracle → node: the node ingests a notice the keripy oracle published ── -ORACLE_KSN="$(mktemp "${TMPDIR:-/tmp}/wit-n3-oracle.XXXXXX.json")" -cleanup() { rm -f "$ORACLE_KSN" 2>/dev/null; } -trap cleanup EXIT -python3 "$EMIT" > "$ORACLE_KSN" 2>/dev/null \ - || broken "the keripy oracle emitter ($EMIT) failed — cannot test the oracle→node direction" -ingest_out="$("$AUTHS_BIN" key-state --ingest "$ORACLE_KSN" 2>&1)" -ingest_code=$? -[ "$ingest_code" -eq 0 ] \ - || red "ours=ingest-failed expected=accepted — the node could not ingest a key-state notice the keripy oracle published; the wire shape is not bidirectionally interoperable: ${ingest_out}" - -# ── 5. adversarial twin: a stale notice is detected as stale ───────────────── -# A verifier holding a NEWER state (here: one sequence past the served notice) -# must refuse to rewind to the node's (now stale) notice. We re-present the -# oracle's seq-0 notice while claiming to already trust seq 1 — staleness must -# fire, with a distinct reason, never a silent accept. -served_seq_hex="$(printf '%s' "$ksn" | python3 -c 'import json,sys;print(json.load(sys.stdin)["s"])' 2>/dev/null)" -served_seq="$((16#${served_seq_hex:-0}))" -newer_hex="$(printf '%x' "$((served_seq + 1))")" -stale_out="$("$AUTHS_BIN" key-state --ingest "$ORACLE_KSN" --reject-stale-below "$newer_hex" 2>&1)" -stale_code=$? -if [ "$stale_code" -eq 0 ]; then - red "ours=accepted-stale expected=rejected — a verifier holding a newer state ingested a stale (seq below ${newer_hex}) notice as fresh; a rewound view must be detected, not trusted: ${stale_out}" -fi -printf '%s\n' "$stale_out" | grep -qiE 'stale|reject' \ - || red "ours=opaque-stale expected=distinct-reason — the stale notice was refused (exit $stale_code) but without a reason a verifier can act on: ${stale_out}" - -green "the node serves a conformant key-state notice: a live node witnessed a real inception and served a KERI ksn wire record that the keripy oracle ($(oracle_version)) reconstructs byte-for-byte (node→oracle), the node ingests a notice the oracle publishes (oracle→node), and a stale notice is detected as stale — IOP-L3c's wire conformance now holds against the running node" diff --git a/.recurve/claims/network/probes/wit-n3.trap/README.md b/.recurve/claims/network/probes/wit-n3.trap/README.md deleted file mode 100644 index cd4aced..0000000 --- a/.recurve/claims/network/probes/wit-n3.trap/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# wit-n3 trap — a stale key-state notice that must stay RED - -A probe that has never been seen RED is not yet evidence. This fixture feeds the -WIT-N3 probe a known-bad **stale** key-state notice (via `TRAP_FIXTURE`) that the -node's freshness gate MUST reject. - -- `stale-ksn/ksn.json` — a genuine, KERI-conformant key-state notice the keripy - oracle published, at **sequence 0**. -- `stale-ksn/last_seen` — `1`: the verifier already trusts sequence 1 (it holds a - newer receipt). Ingesting the seq-0 notice would rewind below that — a stale or - replayed view of the identity. - -The probe runs `auths key-state --ingest stale-ksn/ksn.json --reject-stale-below 1` -and asserts a non-zero exit with a distinct "stale" reason. A zero exit here means -the node accepted a rewound key-state as fresh — exactly the silent-downgrade the -WIT-N3 adversarial twin forbids ("a stale key-state notice is detected as stale by -a verifier holding a newer receipt"). If this ever goes GREEN, a thin client could -be handed an old key-state and told it is current — the freshness column the -directory rests on stops being trustworthy. diff --git a/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/ksn.json b/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/ksn.json deleted file mode 100644 index 7f5520d..0000000 --- a/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/ksn.json +++ /dev/null @@ -1 +0,0 @@ -{"vn": [1, 0], "i": "EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J", "s": "0", "p": "", "d": "EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J", "f": "0", "dt": "2026-06-13T23:25:12.199783+00:00", "et": "icp", "kt": "1", "k": ["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"], "nt": "0", "n": [], "bt": "0", "b": [], "c": [], "ee": {"s": "0", "d": "EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J", "br": [], "ba": []}, "di": ""} \ No newline at end of file diff --git a/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/last_seen b/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/last_seen deleted file mode 100644 index 56a6051..0000000 --- a/.recurve/claims/network/probes/wit-n3.trap/stale-ksn/last_seen +++ /dev/null @@ -1 +0,0 @@ -1 \ No newline at end of file diff --git a/.recurve/claims/network/probes/wit-n4.sh b/.recurve/claims/network/probes/wit-n4.sh deleted file mode 100755 index b164daa..0000000 --- a/.recurve/claims/network/probes/wit-n4.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# WIT-N4 — the node proves which binary it runs. An operator vouches for the -# network; the operator must in turn be vouchable. This probe checks that a -# running node exposes a SIGNED version+digest build attestation and that -# `auths witness status` verifies it — and rejects a forged one. -# -# Behavioral, end to end. GREEN means: a live node serves a build proof pairing -# its own self-measurement of the binary it runs with the signed attestation the -# operator produced over the released binary (dogfooding `auths artifact sign -# --ci`); `auths witness status` confirms the signature holds AND attests the -# digest the node measured of itself (exit 0); AND a FORGED attestation — one -# whose signature is valid but attests a DIFFERENT binary — is rejected by the -# SAME `status` command (non-zero, distinct reason). RED means either the -# genuine build did not verify, or the forged one was accepted — both break the -# claim. BROKEN means we could not even attempt (no bin/auths, no engine, -# fixture/standup prerequisite unmet). -# -# The load-bearing distinction: a valid signature is NOT enough. The attestation -# must attest the digest of the binary actually running. A forger who attaches a -# perfectly-signed attestation for a different binary is caught because the node -# measures /proc/self/exe and `status` checks attested == self-measured. That -# digest check is what converts "signed something" into "proves THIS binary". -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The probe stands up its OWN throwaway -# node (free port + throwaway data dir) with a build attestation the harness -# produced over the released image's binary, then tears that node down on exit -# (hermetic — leaves the shared fixture untouched). -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -set +e # we inspect exit codes of commands expected to fail; errexit would abort - -AUTHS_BIN="./bin/auths" - -# ── Trap mode ──────────────────────────────────────────────────────────────── -# A trap fixture supplies a KNOWN-BAD build attestation at -# probes/wit-n4.trap//forged.auths.json: a genuinely-signed attestation -# whose attested digest is NOT the digest of the binary the node runs (it was -# signed over a different artifact). The runner stands a node up with THIS -# attestation injected where the GREEN path injects the genuine one — and the -# probe MUST turn RED, because `status` must reject a node whose attestation is -# for a different binary. A probe that called this "verified" would be one whose -# digest check is cosmetic. The trap stays RED forever. -if [ -n "${TRAP_FIXTURE:-}" ]; then - [ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" - [ -f "${TRAP_FIXTURE}/forged.auths.json" ] \ - || broken "trap fixture missing forged.auths.json: ${TRAP_FIXTURE}" - command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 \ - || broken "no container engine — the trap stands a node up with a forged attestation; without a live node there is nothing to reject" - bash ./harness/ensure-image.sh >/dev/null 2>&1 \ - || broken "the witness node image could not be made present — cannot stand the trap node up" - - PORT="${WIT_N4_TRAP_PORT:-3347}" - DATA="$(mktemp -d "${TMPDIR:-/tmp}/wit-n4-trap.XXXXXX")" - trap_cleanup() { - "$AUTHS_BIN" witness down --port "$PORT" --data-dir "$DATA" >/dev/null 2>&1 - rm -rf "$DATA" 2>/dev/null - } - trap trap_cleanup EXIT - "$AUTHS_BIN" witness up --port "$PORT" --data-dir "$DATA" \ - --image "$WITNESS_IMAGE" --build-attestation "${TRAP_FIXTURE}/forged.auths.json" \ - >/dev/null 2>&1 \ - || broken "could not stand a node up with the forged attestation injected — cannot exercise the trap" - out="$("$AUTHS_BIN" witness status --port "$PORT" 2>&1)" - code=$? - if [ "$code" -eq 0 ] && printf '%s\n' "$out" | grep -qi 'build verified'; then - red "ours=verified-forged DANGER — \`status\` accepted a node whose build attestation is for a DIFFERENT binary; the digest check is cosmetic and a node can lie about what it runs: ${out}" - fi - red "ours=forged-attestation expected=RED — an attestation over a different binary is the known-bad counterexample; \`status\` correctly refused to verify it (exit $code), so this trap stays RED: $(printf '%s' "$out" | tail -1)" -fi - -[ -x "$AUTHS_BIN" ] \ - || broken "no bin/auths — run the suite rebuild first (recurve rebuild network)" -"$AUTHS_BIN" --version >/dev/null 2>&1 \ - || broken "bin/auths does not run as an auths binary — cannot attempt the build-attestation check" - -# The claim is about a RUNNING node serving a build proof; without an engine -# there is no node to serve one. Fixture prerequisite, not a verdict. -command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 \ - || broken "no container engine — the build-attestation claim is about a RUNNING node; without a live node there is nothing to verify (fixture prerequisite, not a verdict)" - -# The released image must be present (the node runs it). Acquisition is the -# harness's job, never the probe's or `up`'s (WIT-B4). -bash ./harness/ensure-image.sh >/dev/null 2>&1 \ - || broken "the witness node image could not be made present (harness/ensure-image.sh) — cannot stand a node up to check its build proof" - -# The operator-side dogfood: the harness signs the released image's binary with -# `auths artifact sign --ci` and pins the attestation. Producing it is the -# operator's job (the harness here), exactly as image acquisition is. -ATTESTATION="$(bash ./harness/ensure-build-attestation.sh 2>/dev/null)" -[ -n "$ATTESTATION" ] && [ -f "$ATTESTATION" ] \ - || broken "could not produce the signed build attestation for the image binary (harness/ensure-build-attestation.sh) — cannot stand a node up that proves its binary" - -# ── Stand up this probe's own node, with the genuine attestation injected ───── -PORT="${WIT_N4_PORT:-3346}" -DATA_DIR="$(mktemp -d "${TMPDIR:-/tmp}/wit-n4.XXXXXX")" -cleanup() { - "$AUTHS_BIN" witness down --port "$PORT" --data-dir "$DATA_DIR" >/dev/null 2>&1 - rm -rf "$DATA_DIR" 2>/dev/null -} -trap cleanup EXIT - -"$AUTHS_BIN" witness up --port "$PORT" --data-dir "$DATA_DIR" \ - --image "$WITNESS_IMAGE" --build-attestation "$ATTESTATION" >/dev/null 2>&1 \ - || broken "the node did not stand up with the genuine build attestation injected — standup prerequisite, not a verdict on the claim" - -# ── 1. The node serves a build proof at a stable endpoint ──────────────────── -build_json="$(curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/build")" -build_code=$? -[ "$build_code" -eq 0 ] && [ -n "$build_json" ] \ - || red "ours=no-build-endpoint expected=served-proof — the node did not serve a build attestation at /build (curl exit $build_code); a node that cannot say which binary it runs cannot be vouched for" - -# The proof must pair the node's OWN self-measurement with a signed attestation — -# the two fields the digest check needs. -printf '%s' "$build_json" \ - | python3 -c 'import json,sys;d=json.load(sys.stdin);assert d["running_digest"] and d["attestation"]["payload"]["digest"]["hex"]' 2>/dev/null \ - || red "ours=malformed-build expected=self-measure+attestation — the build proof did not carry both a self-measured running digest and a signed attestation; the digest check has nothing to compare" - -# ── 2. `status` verifies the genuine build (signature holds AND digest matches) ─ -status_out="$("$AUTHS_BIN" witness status --port "$PORT" 2>&1)" -status_code=$? -[ "$status_code" -eq 0 ] \ - || red "ours=exit${status_code} expected=verified — \`witness status\` did not verify the node's genuine build attestation; a node running exactly what it attests must verify: $(printf '%s' "$status_out" | tail -1)" -printf '%s\n' "$status_out" | grep -qi 'build verified' \ - || red "ours=no-build-verified-line expected=verified — \`status\` exited 0 but did not report the build verified: ${status_out}" - -# ── 3. A FORGED attestation (valid signature, WRONG binary) is REJECTED ─────── -# Sign a DIFFERENT artifact: a perfectly-valid attestation whose attested digest -# is not the digest of the running binary. Stand a second node up with it and -# confirm `status` refuses — the forgery the whole claim exists to catch. -FORGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/wit-n4-forge.XXXXXX")" -FORGE_PORT="${WIT_N4_FORGE_PORT:-3348}" -forge_cleanup() { - "$AUTHS_BIN" witness down --port "$FORGE_PORT" --data-dir "$FORGE_DIR" >/dev/null 2>&1 - rm -rf "$FORGE_DIR" 2>/dev/null -} -printf 'a binary this node is NOT running' > "$FORGE_DIR/other.bin" -"$AUTHS_BIN" artifact sign "$FORGE_DIR/other.bin" --ci \ - --commit 0000000000000000000000000000000000000000 --ci-platform local \ - --allow-unlogged --sig-output "$FORGE_DIR/forged.auths.json" >/dev/null 2>&1 \ - || { forge_cleanup; broken "could not synthesize a forged (different-binary) attestation to test rejection"; } - -"$AUTHS_BIN" witness up --port "$FORGE_PORT" --data-dir "$FORGE_DIR" \ - --image "$WITNESS_IMAGE" --build-attestation "$FORGE_DIR/forged.auths.json" >/dev/null 2>&1 \ - || { forge_cleanup; broken "could not stand a node up with the forged attestation to test rejection"; } - -forged_out="$("$AUTHS_BIN" witness status --port "$FORGE_PORT" 2>&1)" -forged_code=$? -forge_cleanup -if [ "$forged_code" -eq 0 ]; then - red "ours=verified-forged expected=rejected — \`status\` accepted a node whose attestation is for a DIFFERENT binary; the node could be running anything and still claim a green build: ${forged_out}" -fi -printf '%s\n' "$forged_out" | grep -qiE 'reject|different binary|not running what it attests' \ - || red "ours=opaque-rejection expected=distinct-reason — the forged build was rejected (exit $forged_code) but with no distinct reason an operator can act on: ${forged_out}" - -green "the node proves which binary it runs: a live node served a signed version+digest build attestation, \`witness status\` verified it against the node's own self-measurement of the running binary, and a forged attestation (valid signature over a different binary) was rejected with a distinct reason — an operator vouching for the network is itself vouchable" diff --git a/.recurve/claims/network/probes/wit-n4.trap/README.md b/.recurve/claims/network/probes/wit-n4.trap/README.md deleted file mode 100644 index e4c6395..0000000 --- a/.recurve/claims/network/probes/wit-n4.trap/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# wit-n4 trap — a forged build attestation that must stay RED - -A probe that has never been seen RED is not yet evidence. This fixture feeds the -WIT-N4 probe a known-bad **forged** build attestation (via `TRAP_FIXTURE`) that -`auths witness status` MUST reject. - -- `wrong-digest/forged.auths.json` — a genuinely-signed `auths artifact sign --ci` - attestation whose signature is perfectly valid, but whose attested digest is the - digest of a DIFFERENT artifact — NOT the binary the node runs. - -The probe stands a node up with this attestation injected where the GREEN path -injects the genuine one, then runs `auths witness status`. It asserts a non-zero -exit with a distinct reason ("rejected … different binary … not running what it -attests"). A zero exit here means `status` accepted a node whose attestation is -for a binary it is not running — the node could be executing anything and still -claim a green build, and the "the node proves what binary it runs" guarantee -becomes cosmetic. - -This is the WIT-N4 adversarial twin, frozen: "an attestation whose digest differs -from the running binary fails verification." The check that keeps it RED is the -two-leg verdict — signature valid AND attested digest == the node's own -self-measurement of `/proc/self/exe`. The forged attestation passes leg one and -dies on leg two. If this ever goes GREEN, an operator can vouch for the network -while running an unattested binary. diff --git a/.recurve/claims/network/probes/wit-n4.trap/wrong-digest/forged.auths.json b/.recurve/claims/network/probes/wit-n4.trap/wrong-digest/forged.auths.json deleted file mode 100644 index 8e0e65a..0000000 --- a/.recurve/claims/network/probes/wit-n4.trap/wrong-digest/forged.auths.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": 1, - "rid": "sha256:296078e6633559c05607f064712090a3685dddaba11b02556788c23a2016054c", - "issuer": "did:key:zDnaeV34VqFT3vaskWP6jgEGyFoee9ZohbnMVj2pYHn2Uh4WE", - "subject": "did:key:zDnaeV34VqFT3vaskWP6jgEGyFoee9ZohbnMVj2pYHn2Uh4WE", - "device_public_key": { - "curve": "p256", - "key": "02446c3d85a73fc7742b50a635851f690b25d0307b0c2fb7abd5cb82b31094b4db" - }, - "identity_signature": "e787b2d328aed509b3e9d25ec26c76873f67ecdb508795279dfe21a42b028abc75c4a5fe49b5706d6c54bed36182ceb0eb587d36acc88050c143baceb839002d", - "device_signature": "e787b2d328aed509b3e9d25ec26c76873f67ecdb508795279dfe21a42b028abc75c4a5fe49b5706d6c54bed36182ceb0eb587d36acc88050c143baceb839002d", - "timestamp": "2026-06-14T00:34:35.114945Z", - "payload": { - "artifact_type": "file", - "digest": { - "algorithm": "sha256", - "hex": "296078e6633559c05607f064712090a3685dddaba11b02556788c23a2016054c" - }, - "name": "wrong.bin", - "size": 61, - "ci_environment": { - "platform": "local" - } - }, - "commit_sha": "0000000000000000000000000000000000000000", - "signer_type": "Workload" -} \ No newline at end of file diff --git a/.recurve/claims/network/probes/wit-n5.sh b/.recurve/claims/network/probes/wit-n5.sh deleted file mode 100755 index 49cc83e..0000000 --- a/.recurve/claims/network/probes/wit-n5.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash -# WIT-N5 — zero protocol vocabulary in the operator happy path. An operator -# stands a witness up, checks on it, registers it, reads its logs, and tears it -# down — and must never need the trust kernel's vocabulary (key event logs, -# key-state notices, self-addressing identifiers, the CESR wire, signing -# thresholds, and the like) to do any of it. This probe makes the -# vocabulary-invisible rule a guarantee instead of a hope. -# -# Behavioral, end to end — AND source-anchored (the claim is about output -# STRINGS, so reads:source is legitimate; see the gap's smallest_fix). GREEN -# means: every line the full operator happy path prints — `up`, `status`, -# `register`, `logs`, `down`, with and without a build attestation — passes a -# case-insensitive whole-word scan against the canonical protocol-vocabulary -# denylist the PRODUCT owns, AND that denylist lives in exactly one place (no -# divergent hand-maintained copy survives in the node crate). RED means either a -# happy-path line leaked a protocol term, or the rule has no single owner the -# probe can anchor to (it is a hope spread across ad-hoc test lists, not a -# guarantee). BROKEN means we could not even attempt the measurement. -# -# The load-bearing distinction this probe enforces: a green is only real if the -# words it forbids are the words the PRODUCT forbids. A probe carrying its own -# private jargon list could drift from the surface it guards and bless a leak it -# never thought to name. So the probe extracts the denylist from the product -# source (one source of truth) and scans the LIVE happy path against exactly it. -# -# Contract: 0 GREEN · 1 RED · 2 BROKEN. The probe stands up its OWN throwaway -# node (free port + throwaway data dir) and tears it down on exit (hermetic — -# the shared fixture is untouched). -set -uo pipefail -cd "$(dirname "$0")/.." # the suite dir (.../claims/network) -. ./harness/env.sh -. ./probes/_contract.sh -set +e # we inspect exit codes of commands expected to fail; errexit would abort - -AUTHS_BIN="./bin/auths" - -# The one place the operator-vocabulary rule lives in the product. The probe -# anchors to THIS file's PROTOCOL_VOCABULARY list so probe and surface cannot -# drift (quality constitution §3 — one source of truth). -VOCAB_SRC="$AUTHS_SRC/crates/auths-witness-node/src/vocabulary.rs" - -# Extract the canonical denylist (the bare lowercase terms inside the -# PROTOCOL_VOCABULARY const) from the product source. This is the rule the -# happy path is held to; the probe never invents its own. -read_denylist() { - # Pull the PROTOCOL_VOCABULARY const body, then every "quoted" token on it. - awk '/pub const PROTOCOL_VOCABULARY/{f=1} f{print} f&&/\];/{exit}' "$VOCAB_SRC" \ - | grep -oE '"[a-z]+"' | tr -d '"' | sort -u -} - -# scan_clean

@@ -132,7 +130,13 @@ export default function DirectoryPage(): ReactElement { re-checked against its proof in your browser. An entry whose proof does not hold is shown as a failure, never as data.

- {entries === null ? ( + {rosterFailed ? ( +

+ The roster did not re-check under the pinned admissions key — its + entries are withheld, never rendered as data:{" "} + +

+ ) : entries === null ? (

Re-checking the roster in your browser:{" "} @@ -149,6 +153,7 @@ export default function DirectoryPage(): ReactElement { ))} )} + {rosterWidget}

); diff --git a/web/app/globals.css b/web/app/globals.css index 4d07bd3..ade7202 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -252,6 +252,85 @@ section > p { margin: 0; } +/* ---- Site navigation ------------------------------------------------- */ + +.site-nav { + display: flex; + align-items: baseline; + gap: var(--space-4); + max-width: var(--measure-wide); + margin: 0 auto; + padding: var(--space-3); + border-bottom: 1px solid var(--color-border); +} + +.site-nav .wordmark { + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +.site-nav .wordmark:hover, +.site-nav nav a:hover { + text-decoration: none; +} + +.site-nav nav { + display: flex; + gap: var(--space-3); + margin-left: auto; +} + +.site-nav nav a { + color: var(--color-text-muted); +} + +.site-nav nav a:hover { + color: var(--color-accent); +} + +/* ---- Home ------------------------------------------------------------ */ + +.lead { + font-size: var(--font-size-lead); + color: var(--color-text-muted); +} + +.home-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); + gap: var(--space-3); + margin-top: var(--space-3); +} + +.home-card { + display: block; + padding: var(--space-4); + background: var(--color-surface-raised); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + color: var(--color-text); + transition: border-color var(--motion-fast) var(--motion-ease); +} + +.home-card:hover { + text-decoration: none; + border-color: var(--color-accent); +} + +.home-card h2 { + margin-bottom: var(--space-1); +} + +.home-card p { + color: var(--color-text-muted); +} + +.home-card-go { + color: var(--color-accent); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-caption); +} + /* ---- Motion preference ----------------------------------------------- */ /* Honor a reader who asks for less motion: every transition collapses to none. */ diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 18e7edf..b2bc33c 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; +import Link from "next/link"; // The design tokens are loaded first so every later rule resolves against the // single source of visual truth; the stylesheet then arranges what they define. @@ -15,7 +16,20 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: ReactNode }) { return ( - {children} + + {/* Primary navigation: the same on every surface so a reader can always + reach the directory and the console, and return home. */} +
+ + Witness Network + + +
+ {children} + ); } diff --git a/web/app/lib/admissions-key.json b/web/app/lib/admissions-key.json new file mode 100644 index 0000000..5ca28a7 --- /dev/null +++ b/web/app/lib/admissions-key.json @@ -0,0 +1,4 @@ +{ + "did": "did:key:zDnaeeZ7ivM3XNk44FjEe9BpFzfCeSvNZpMbbNFSKnCoh2hfP", + "issuerKey": "02d1d87e8311aec49790566bcf9ca90220e37acf5574198e531d31b91f1ad5b6fa" +} diff --git a/web/app/lib/roster.tsx b/web/app/lib/roster.tsx new file mode 100644 index 0000000..7211e13 --- /dev/null +++ b/web/app/lib/roster.tsx @@ -0,0 +1,134 @@ +"use client"; + +// The verified roster: the single source of every witness key the app trusts. +// +// The served /directory.json is an untrusted document. It becomes the roster +// only after two checks in YOUR browser: +// 1. its admissions attestation re-checks under the PINNED admissions key +// (baked into the app bundle at build time — never read from the served +// document, which is the classic "trust the key I sent you" hole), and +// 2. the exact roster bytes hash to the rid the attestation commits to, so +// the attestation cannot be reused over a swapped entry list. +// Only entries from a roster that passed both may supply issuer keys to any +// other surface (directory rows, the node console's build check). +import { useEffect, useState } from "react"; + +import admissions from "./admissions-key.json"; +import type { SignedPayload } from "./proof"; + +// One operator as the verified roster carries it. +export type RosterEntry = { + readonly did: string; + readonly name: string; + readonly region: string; + readonly payload: SignedPayload; +}; + +// The roster document before the widget's signature re-check: fetched, shaped, +// and content-bound — or the reason it never got that far. `loaded` means the +// bytes are bound to the attestation; it does NOT yet mean the attestation +// holds. The page mounts the proof widget over `payload` (with the pinned +// issuer key) and releases `entries` only on a verified outcome. +export type RosterDocument = + | { readonly state: "pending" } + | { readonly state: "absent" } + | { readonly state: "failed"; readonly reason: string } + | { + readonly state: "loaded"; + readonly payload: SignedPayload; + readonly entries: readonly RosterEntry[]; + }; + +// The pinned admissions payload for a fetched statement: the statement comes +// from the document, the key NEVER does. +function pinnedPayload(statement: string): SignedPayload { + return { statement, issuerKey: admissions.issuerKey }; +} + +async function sha256Hex(text: string): Promise { + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(text), + ); + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +// Fetch /directory.json and bind its roster bytes to its attestation. The +// signature itself is re-checked by the proof widget the page mounts; this +// hook only establishes that the bytes the page would render are the bytes +// the attestation is about. +export function useRosterDocument(): RosterDocument { + const [doc, setDoc] = useState({ state: "pending" }); + + useEffect(() => { + let live = true; + (async () => { + try { + const res = await fetch("/directory.json", { cache: "no-store" }); + if (!res.ok) { + if (live) setDoc({ state: "absent" }); + return; + } + const body: { rosterJson?: unknown; statement?: unknown } = + await res.json(); + const rosterJson = + typeof body.rosterJson === "string" ? body.rosterJson : null; + const statement = + typeof body.statement === "string" ? body.statement : null; + if (!rosterJson || !statement) { + if (live) setDoc({ state: "absent" }); + return; + } + + // Content binding: the attestation names the hash of the artifact it + // signed; the roster bytes must be that artifact. + let rid: string | null = null; + try { + const att: { rid?: unknown } = JSON.parse(statement); + rid = typeof att.rid === "string" ? att.rid : null; + } catch { + rid = null; + } + if (!rid || !rid.startsWith("sha256:")) { + if (live) + setDoc({ + state: "failed", + reason: "the roster attestation names no content hash", + }); + return; + } + const hash = await sha256Hex(rosterJson); + if (`sha256:${hash}` !== rid) { + if (live) + setDoc({ + state: "failed", + reason: "the roster bytes are not the bytes the attestation signed", + }); + return; + } + + const parsed: unknown = JSON.parse(rosterJson); + if (!Array.isArray(parsed)) { + if (live) + setDoc({ state: "failed", reason: "the roster is not a list" }); + return; + } + if (live) + setDoc({ + state: "loaded", + payload: pinnedPayload(statement), + entries: parsed as RosterEntry[], + }); + } catch { + if (live) setDoc({ state: "absent" }); + } + })(); + return () => { + live = false; + }; + }, []); + + return doc; +} diff --git a/web/app/node/page.tsx b/web/app/node/page.tsx index c154aae..63eb276 100644 --- a/web/app/node/page.tsx +++ b/web/app/node/page.tsx @@ -21,6 +21,7 @@ import type {} from "@auths-dev/verify"; import { Verdict } from "../lib/Verdict"; import { Proof, useEntryProof, verifyBuild } from "../lib/proof"; import type { SignedPayload } from "../lib/proof"; +import { useRosterDocument } from "../lib/roster"; // Where this console reaches its node. The node is a separate process the // operator runs (the architecture default address is a local witness node); @@ -44,16 +45,17 @@ type Health = | { readonly state: "up"; readonly witnessDid: string | null } | { readonly state: "down"; readonly reason: string }; -// The signed build statement exactly as the node serves it at /build, shaped as -// the payload the published widget re-checks: the signed statement plus the -// issuer key it is re-checked against. The console parses neither — it forwards -// them to the widget, which is the sole judge of whether the statement holds. +// The signed build statement exactly as the node serves it at /build. The node +// supplies ONLY the statement and its opaque figures — never the key it is +// judged by. The issuer key comes from the verified roster (the entry whose +// did the node claims), because a key read from the same response as the +// statement would let any node mint a key and verify itself. type BuildAttestation = | { readonly state: "pending" } | { readonly state: "absent" } | { readonly state: "present"; - readonly payload: SignedPayload; + readonly statement: string; // The opaque build figures the node reports alongside its signed // statement. They are shown ONLY once the statement re-checks; until then // they are withheld, exactly like an unverified directory entry's data. @@ -122,14 +124,13 @@ function useBuildAttestation(): BuildAttestation { if (!live) return; const statement = typeof body?.statement === "string" ? body.statement : null; - const issuerKey = - typeof body?.issuer_key === "string" ? body.issuer_key : null; - if (!body || !statement || !issuerKey) { + if (!body || !statement) { setBuild({ state: "absent" }); return; } // The opaque figures the node reports about its build, kept aside until - // the statement re-checks. + // the statement re-checks. Any `issuer_key` the node serves is ignored + // — the key it is judged by comes from the verified roster. const fields: Record = {}; for (const [k, v] of Object.entries(body)) { if (k === "statement" || k === "issuer_key") continue; @@ -137,7 +138,7 @@ function useBuildAttestation(): BuildAttestation { fields[k] = String(v); } } - setBuild({ state: "present", payload: { statement, issuerKey }, fields }); + setBuild({ state: "present", statement, fields }); }) .catch(() => { if (live) setBuild({ state: "absent" }); @@ -158,17 +159,22 @@ function useBuildAttestation(): BuildAttestation { // the shared Verdict, never as the node's attested build. function BuildPanel({ attestation, + issuerKey, }: { attestation: Extract; + // The roster-supplied key the node's statement is judged by — never a key + // the node served about itself. + issuerKey: string; }): ReactElement { - const { ref, outcome } = useEntryProof(attestation.payload); + const payload: SignedPayload = { statement: attestation.statement, issuerKey }; + const { ref, outcome } = useEntryProof(payload); const decision = verifyBuild(outcome); // The widget renders off-screen so it actually re-checks the statement in the // browser; we read its conclusion through `outcome`, never its pixels. const widget = ( ); @@ -207,6 +213,35 @@ export default function NodePage(): ReactElement { const health = useNodeHealth(); const build = useBuildAttestation(); + // The verified roster supplies the ONLY key this console will judge the + // node's build statement by: the pinned admissions key admits the roster, + // the roster admits the node's key. A node whose claimed identity is not on + // the verified roster gets no key — its build statement is unverifiable + // here, shown as such, never judged against a key the node brought itself. + const roster = useRosterDocument(); + const rosterPayload = + roster.state === "loaded" + ? roster.payload + : { statement: "", issuerKey: "" }; + const { ref: rosterRef, outcome: rosterOutcome } = + useEntryProof(rosterPayload); + const rosterEntries = + roster.state === "loaded" && rosterOutcome === "verified" + ? roster.entries + : null; + const nodeDid = health.state === "up" ? health.witnessDid : null; + const rosterKey = + rosterEntries && nodeDid + ? (rosterEntries.find((e) => e.did === nodeDid)?.payload.issuerKey ?? + null) + : null; + const rosterWidget = + roster.state === "loaded" ? ( + + ) : null; + return (
@@ -269,9 +304,17 @@ export default function NodePage(): ReactElement { The node served no signed build statement to re-check:{" "}

+ ) : rosterKey === null ? ( +

+ This node's claimed identity is not on the verified roster, so + there is no admitted key to re-check its build statement against — + the statement is withheld, never judged by a key the node supplied + about itself: +

) : ( - + )} + {rosterWidget}
); diff --git a/web/app/page.tsx b/web/app/page.tsx index 48d48e1..72823d3 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,12 +1,60 @@ -export default function Home() { +import Link from "next/link"; +import type { ReactElement } from "react"; + +// The front door. It says what the network is in one breath, then sends the +// reader to the two surfaces that do the work — the public directory and the +// read-only operator console. It asserts nothing it cannot stand behind: the +// proofs live on those surfaces, re-checked in the reader's own browser. +export default function Home(): ReactElement { return (
-

Witness Network

-

- A public network of witnesses you do not have to take on faith. Every - figure on these pages is backed by a proof your own browser re-checks — - the page renders proofs, not claims. -

+
+

Witness Network

+

+ A public network of witnesses you do not have to take on faith. Every + figure on these pages is backed by a proof your own browser re-checks + — the page renders proofs, not claims. +

+
+ +
+
+ +

Directory

+

+ The public roster of witness operators. Each one appears only after + your browser re-checks the proof behind it; an entry whose proof + does not hold is shown as a failure, never as data. +

+ + + + +

Operator console

+

+ A read-only view of a single node — whether it is up, and the + signed proof of exactly which binary it runs, re-verified in your + browser. It never claims a node is healthy; it shows the proof. +

+ + +
+
+ +
+

Why “proofs, not claims”

+

+ This site never asks you to trust it. Every operator and every figure + arrives with a signed statement, and the page re-checks that statement + in your own browser — using the same verifier the rest of the network + uses — before it shows you anything. Trust comes from the re-check, not + from the server. +

+
); } diff --git a/web/app/tokens.css b/web/app/tokens.css index 9f0591c..f51a531 100644 --- a/web/app/tokens.css +++ b/web/app/tokens.css @@ -7,7 +7,7 @@ * these tokens through var(--…) only, so the entire look is changed in one place * and can never drift between surfaces. * - * The register is Apple-grade restraint: a neutral, near-white light surface and + * The register is about restraint: a neutral, near-white light surface and * a calm true-dark surface, ONE accent (a single blue), and a small reserved set * of state colors that carry meaning for verification outcomes and nothing else. * Color is information here, never decoration. diff --git a/web/fixtures/generate.py b/web/fixtures/generate.py new file mode 100644 index 0000000..f4793a4 --- /dev/null +++ b/web/fixtures/generate.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Generate REAL, signed demo fixtures for the witness-network web UI. + +Why not just hand-write dummy JSON? Because the UI refuses to render data it +cannot verify — a made-up entry shows as a *failure*, never as data. So every +fixture here carries a GENUINE attestation, minted with the auths CLI using +ephemeral CI signing (no keychain, no identity setup). The browser re-checks +them exactly as it would real network data; nothing here is faked. + +Outputs (data files only — this script is the source, the JSON is generated): + public/directory.json the roster document: the exact roster bytes plus an + admissions attestation over them + app/lib/admissions-key.json the admissions issuer key, pinned into the app + bundle (the one disclosed trust root of the read + surfaces — never read from the served document) + public/demo-node/health the demo node's liveness + public/demo-node/build the demo node's signed build statement + +Run from anywhere: python3 fixtures/generate.py +Then serve the demo node by pointing the console at it: + NEXT_PUBLIC_NODE_BASE_URL=/demo-node npm run dev +""" +import json +import os +import pathlib +import subprocess +import tempfile + +HERE = pathlib.Path(__file__).resolve().parent +WEB = HERE.parent +REPO_ROOT = WEB.parent.parent # auths-base/ +AUTHS = os.environ.get("AUTHS_BIN", str(REPO_ROOT / "auths/target/release/auths")) +PUBLIC = WEB / "public" +(PUBLIC / "demo-node").mkdir(parents=True, exist_ok=True) + +try: + COMMIT = subprocess.check_output( + ["git", "-C", str(REPO_ROOT / "auths"), "rev-parse", "HEAD"], text=True + ).strip() +except Exception: + COMMIT = "0" * 40 + + +def sign(text: str) -> dict: + """Mint one real attestation over `text` with an ephemeral CI key.""" + with tempfile.TemporaryDirectory() as d: + artifact = pathlib.Path(d) / "artifact" + artifact.write_text(text) + sig = pathlib.Path(d) / "artifact.auths.json" + subprocess.run( + [AUTHS, "artifact", "sign", str(artifact), + "--ci", "--ci-platform", "local", "--commit", COMMIT, + "--sig-output", str(sig), "--json"], + check=True, capture_output=True, + ) + return json.loads(sig.read_text()) + + +def payload(att: dict) -> dict: + """Shape an attestation into the { statement, issuerKey } the widget checks.""" + return { + "statement": json.dumps(att, separators=(",", ":")), + "issuerKey": att["device_public_key"]["key"], + } + + +# A few demo operators. Each gets its own real attestation (distinct key + did). +OPERATORS = [ + ("Aurora Witness Co.", "us-east-1"), + ("Meridian Observatory", "eu-west-1"), + ("Toroki Networks", "ap-northeast-1"), + ("Helvetia Trust Anchor", "eu-central-2"), +] + +entries = [] +for name, region in OPERATORS: + att = sign(f"witness-operator:{name}:{region}") + entries.append({ + "did": att["issuer"], + "name": name, + "region": region, + "payload": payload(att), + }) + +# The demo node is itself a rostered witness: its key reaches the console ONLY +# through the admissions-signed roster, never from the node's own responses. +node_att = sign("witness-node-build:aurora-1:v1.0.0") +node_payload = payload(node_att) +entries.append({ + "did": node_att["issuer"], + "name": "Aurora Witness Co. — node aurora-1", + "region": "us-east-1", + "payload": node_payload, +}) + +# The roster document: the exact roster bytes (content-bound by the +# attestation's rid) plus the admissions attestation over those bytes. The +# browser refuses the roster unless (a) the attestation re-checks under the +# PINNED admissions key and (b) sha256(rosterJson) equals the attested rid. +roster_json = json.dumps(entries, separators=(",", ":")) +roster_att = sign(roster_json) +(PUBLIC / "directory.json").write_text(json.dumps({ + "rosterJson": roster_json, + "statement": json.dumps(roster_att, separators=(",", ":")), +}, indent=2) + "\n") + +# The pinned trust root: baked into the app bundle at build time, so an +# attacker who can swap the served directory.json cannot swap the key it is +# checked against. +(WEB / "app" / "lib" / "admissions-key.json").write_text(json.dumps({ + "did": roster_att["issuer"], + "issuerKey": roster_att["device_public_key"]["key"], +}, indent=2) + "\n") +(PUBLIC / "demo-node" / "health").write_text( + json.dumps({"up": True, "witness_did": node_att["issuer"]}, indent=2) + "\n" +) +(PUBLIC / "demo-node" / "build").write_text( + json.dumps( + { + "statement": node_payload["statement"], + "issuer_key": node_payload["issuerKey"], + "version": "1.0.0", + "digest": node_att["rid"], + "commit": COMMIT[:12], + }, + indent=2, + ) + + "\n" +) + +print(f"✓ {len(entries)} directory entries + demo node fixtures written to public/") +print(" run: NEXT_PUBLIC_NODE_BASE_URL=/demo-node npm run dev") diff --git a/web/public/demo-node/build b/web/public/demo-node/build new file mode 100644 index 0000000..61c3e9f --- /dev/null +++ b/web/public/demo-node/build @@ -0,0 +1,7 @@ +{ + "statement": "{\"version\":1,\"rid\":\"sha256:fb928160092592c8dc4fd7879a20b0ca447f7795015bba079955b0dfd430b273\",\"issuer\":\"did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ\",\"subject\":\"did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ\",\"device_public_key\":{\"curve\":\"p256\",\"key\":\"025febb4b16070db79f0b651d85119e4e498cbb78edc2218e88e7cd797317e8ab7\"},\"identity_signature\":\"3346b6db7c8c3c94e0d5fbaebc225349d403f86b226b41da3fbc0e28a21bf737871873e272cb9023c3ab1224809483e031ee69c936bfb39340e5b38940f22bdf\",\"device_signature\":\"3346b6db7c8c3c94e0d5fbaebc225349d403f86b226b41da3fbc0e28a21bf737871873e272cb9023c3ab1224809483e031ee69c936bfb39340e5b38940f22bdf\",\"timestamp\":\"2026-07-19T20:49:37.588693Z\",\"payload\":{\"artifact_type\":\"file\",\"digest\":{\"algorithm\":\"sha256\",\"hex\":\"fb928160092592c8dc4fd7879a20b0ca447f7795015bba079955b0dfd430b273\"},\"name\":\"artifact\",\"size\":34,\"ci_environment\":{\"platform\":\"local\"}},\"commit_sha\":\"0796f08779c7f113a75b57c2a4fdcaaf62088919\",\"signer_type\":\"Workload\"}", + "issuer_key": "025febb4b16070db79f0b651d85119e4e498cbb78edc2218e88e7cd797317e8ab7", + "version": "1.0.0", + "digest": "sha256:fb928160092592c8dc4fd7879a20b0ca447f7795015bba079955b0dfd430b273", + "commit": "0796f08779c7" +} diff --git a/web/public/demo-node/health b/web/public/demo-node/health new file mode 100644 index 0000000..e254e6d --- /dev/null +++ b/web/public/demo-node/health @@ -0,0 +1,4 @@ +{ + "up": true, + "witness_did": "did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ" +} diff --git a/web/public/directory.json b/web/public/directory.json new file mode 100644 index 0000000..4698197 --- /dev/null +++ b/web/public/directory.json @@ -0,0 +1,4 @@ +{ + "rosterJson": "[{\"did\":\"did:key:zDnaeS6CBRzqzhneTvCW72o6r6tMQ6SDtekqa9NApCTiYusWi\",\"name\":\"Aurora Witness Co.\",\"region\":\"us-east-1\",\"payload\":{\"statement\":\"{\\\"version\\\":1,\\\"rid\\\":\\\"sha256:93fd9a7b308d8a3de755fccadb3bd9117cab939dbca46896c2aa2a369fdb5869\\\",\\\"issuer\\\":\\\"did:key:zDnaeS6CBRzqzhneTvCW72o6r6tMQ6SDtekqa9NApCTiYusWi\\\",\\\"subject\\\":\\\"did:key:zDnaeS6CBRzqzhneTvCW72o6r6tMQ6SDtekqa9NApCTiYusWi\\\",\\\"device_public_key\\\":{\\\"curve\\\":\\\"p256\\\",\\\"key\\\":\\\"0218a69bdf2f00c352cfbb521a37438188f83b893a653523ec40584cfa1bb20673\\\"},\\\"identity_signature\\\":\\\"913d389595909e5a01dba71b3ce70183029b67c828bfe3676611d757e136d28c4e0e8c84c6af633b723b707c3202eb806a46262b90b5cf20b3bfa652ff81c578\\\",\\\"device_signature\\\":\\\"913d389595909e5a01dba71b3ce70183029b67c828bfe3676611d757e136d28c4e0e8c84c6af633b723b707c3202eb806a46262b90b5cf20b3bfa652ff81c578\\\",\\\"timestamp\\\":\\\"2026-07-19T20:49:37.533829Z\\\",\\\"payload\\\":{\\\"artifact_type\\\":\\\"file\\\",\\\"digest\\\":{\\\"algorithm\\\":\\\"sha256\\\",\\\"hex\\\":\\\"93fd9a7b308d8a3de755fccadb3bd9117cab939dbca46896c2aa2a369fdb5869\\\"},\\\"name\\\":\\\"artifact\\\",\\\"size\\\":45,\\\"ci_environment\\\":{\\\"platform\\\":\\\"local\\\"}},\\\"commit_sha\\\":\\\"0796f08779c7f113a75b57c2a4fdcaaf62088919\\\",\\\"signer_type\\\":\\\"Workload\\\"}\",\"issuerKey\":\"0218a69bdf2f00c352cfbb521a37438188f83b893a653523ec40584cfa1bb20673\"}},{\"did\":\"did:key:zDnaefLK3iec62pFfGFB1VyBizBgbPxTPZKHg4GjUB1iGxdeQ\",\"name\":\"Meridian Observatory\",\"region\":\"eu-west-1\",\"payload\":{\"statement\":\"{\\\"version\\\":1,\\\"rid\\\":\\\"sha256:786fedc065443555ef4ad200f9928741f0e0ba6cba48a66c44b62828c3784da4\\\",\\\"issuer\\\":\\\"did:key:zDnaefLK3iec62pFfGFB1VyBizBgbPxTPZKHg4GjUB1iGxdeQ\\\",\\\"subject\\\":\\\"did:key:zDnaefLK3iec62pFfGFB1VyBizBgbPxTPZKHg4GjUB1iGxdeQ\\\",\\\"device_public_key\\\":{\\\"curve\\\":\\\"p256\\\",\\\"key\\\":\\\"02dd6c6f4f7e8314a584e14edd4b56e746eab0a920fe0608d6fcc3d6ff3aed06b1\\\"},\\\"identity_signature\\\":\\\"5e940c76c6c3a2dc988c9ada65f23066beeeda710080123f45d266aaa13d09209063206e35d947e07b2c28596d065142df18793d8c48471474f90a6fd76a8363\\\",\\\"device_signature\\\":\\\"5e940c76c6c3a2dc988c9ada65f23066beeeda710080123f45d266aaa13d09209063206e35d947e07b2c28596d065142df18793d8c48471474f90a6fd76a8363\\\",\\\"timestamp\\\":\\\"2026-07-19T20:49:37.552454Z\\\",\\\"payload\\\":{\\\"artifact_type\\\":\\\"file\\\",\\\"digest\\\":{\\\"algorithm\\\":\\\"sha256\\\",\\\"hex\\\":\\\"786fedc065443555ef4ad200f9928741f0e0ba6cba48a66c44b62828c3784da4\\\"},\\\"name\\\":\\\"artifact\\\",\\\"size\\\":47,\\\"ci_environment\\\":{\\\"platform\\\":\\\"local\\\"}},\\\"commit_sha\\\":\\\"0796f08779c7f113a75b57c2a4fdcaaf62088919\\\",\\\"signer_type\\\":\\\"Workload\\\"}\",\"issuerKey\":\"02dd6c6f4f7e8314a584e14edd4b56e746eab0a920fe0608d6fcc3d6ff3aed06b1\"}},{\"did\":\"did:key:zDnaepGjAyxoUTLr1f7XsMF5NijPbgCWPKa3qQSoVmtzmw1ei\",\"name\":\"Toroki Networks\",\"region\":\"ap-northeast-1\",\"payload\":{\"statement\":\"{\\\"version\\\":1,\\\"rid\\\":\\\"sha256:7d8c12db97744638045b00c818167d2eb693e23268b58157928e9bd214693017\\\",\\\"issuer\\\":\\\"did:key:zDnaepGjAyxoUTLr1f7XsMF5NijPbgCWPKa3qQSoVmtzmw1ei\\\",\\\"subject\\\":\\\"did:key:zDnaepGjAyxoUTLr1f7XsMF5NijPbgCWPKa3qQSoVmtzmw1ei\\\",\\\"device_public_key\\\":{\\\"curve\\\":\\\"p256\\\",\\\"key\\\":\\\"03623a92892c10442ac62d1028d48916d848d20217980f2cc28a2db981431d0d1b\\\"},\\\"identity_signature\\\":\\\"9dc3df06faa2cf9482e7c56e73550555946d86012afc8dd1a90ee92769ef71db7ba2928e54fea170624beeb74e42d97df2705045080e25b8f37349a8d66c7117\\\",\\\"device_signature\\\":\\\"9dc3df06faa2cf9482e7c56e73550555946d86012afc8dd1a90ee92769ef71db7ba2928e54fea170624beeb74e42d97df2705045080e25b8f37349a8d66c7117\\\",\\\"timestamp\\\":\\\"2026-07-19T20:49:37.564814Z\\\",\\\"payload\\\":{\\\"artifact_type\\\":\\\"file\\\",\\\"digest\\\":{\\\"algorithm\\\":\\\"sha256\\\",\\\"hex\\\":\\\"7d8c12db97744638045b00c818167d2eb693e23268b58157928e9bd214693017\\\"},\\\"name\\\":\\\"artifact\\\",\\\"size\\\":47,\\\"ci_environment\\\":{\\\"platform\\\":\\\"local\\\"}},\\\"commit_sha\\\":\\\"0796f08779c7f113a75b57c2a4fdcaaf62088919\\\",\\\"signer_type\\\":\\\"Workload\\\"}\",\"issuerKey\":\"03623a92892c10442ac62d1028d48916d848d20217980f2cc28a2db981431d0d1b\"}},{\"did\":\"did:key:zDnaeV3Aijoy1xKFkvvGC4gCu4gbLYVuqdh4zxBmbFQ4FHmpG\",\"name\":\"Helvetia Trust Anchor\",\"region\":\"eu-central-2\",\"payload\":{\"statement\":\"{\\\"version\\\":1,\\\"rid\\\":\\\"sha256:250e27d4ceb5847690a8cf51a63bac8925bc3cf6a3108e6c424a9444da7da41d\\\",\\\"issuer\\\":\\\"did:key:zDnaeV3Aijoy1xKFkvvGC4gCu4gbLYVuqdh4zxBmbFQ4FHmpG\\\",\\\"subject\\\":\\\"did:key:zDnaeV3Aijoy1xKFkvvGC4gCu4gbLYVuqdh4zxBmbFQ4FHmpG\\\",\\\"device_public_key\\\":{\\\"curve\\\":\\\"p256\\\",\\\"key\\\":\\\"02447346b19b3edb58aff3f044fb43e0f388a30a5276a804bf967887187eef93e5\\\"},\\\"identity_signature\\\":\\\"8c4eba669e128c13653d52b4a8a32c83a5f90f2dfdab70c67c53dd4d8f04f8357e3aa83c976a712cde405c1a803695d57719f8981e81b968965595f9811b62ec\\\",\\\"device_signature\\\":\\\"8c4eba669e128c13653d52b4a8a32c83a5f90f2dfdab70c67c53dd4d8f04f8357e3aa83c976a712cde405c1a803695d57719f8981e81b968965595f9811b62ec\\\",\\\"timestamp\\\":\\\"2026-07-19T20:49:37.577057Z\\\",\\\"payload\\\":{\\\"artifact_type\\\":\\\"file\\\",\\\"digest\\\":{\\\"algorithm\\\":\\\"sha256\\\",\\\"hex\\\":\\\"250e27d4ceb5847690a8cf51a63bac8925bc3cf6a3108e6c424a9444da7da41d\\\"},\\\"name\\\":\\\"artifact\\\",\\\"size\\\":51,\\\"ci_environment\\\":{\\\"platform\\\":\\\"local\\\"}},\\\"commit_sha\\\":\\\"0796f08779c7f113a75b57c2a4fdcaaf62088919\\\",\\\"signer_type\\\":\\\"Workload\\\"}\",\"issuerKey\":\"02447346b19b3edb58aff3f044fb43e0f388a30a5276a804bf967887187eef93e5\"}},{\"did\":\"did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ\",\"name\":\"Aurora Witness Co. \\u2014 node aurora-1\",\"region\":\"us-east-1\",\"payload\":{\"statement\":\"{\\\"version\\\":1,\\\"rid\\\":\\\"sha256:fb928160092592c8dc4fd7879a20b0ca447f7795015bba079955b0dfd430b273\\\",\\\"issuer\\\":\\\"did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ\\\",\\\"subject\\\":\\\"did:key:zDnaeWtQEhC1mWPajQeL435Co6p9fBqEGJBSRx6morKfs2toQ\\\",\\\"device_public_key\\\":{\\\"curve\\\":\\\"p256\\\",\\\"key\\\":\\\"025febb4b16070db79f0b651d85119e4e498cbb78edc2218e88e7cd797317e8ab7\\\"},\\\"identity_signature\\\":\\\"3346b6db7c8c3c94e0d5fbaebc225349d403f86b226b41da3fbc0e28a21bf737871873e272cb9023c3ab1224809483e031ee69c936bfb39340e5b38940f22bdf\\\",\\\"device_signature\\\":\\\"3346b6db7c8c3c94e0d5fbaebc225349d403f86b226b41da3fbc0e28a21bf737871873e272cb9023c3ab1224809483e031ee69c936bfb39340e5b38940f22bdf\\\",\\\"timestamp\\\":\\\"2026-07-19T20:49:37.588693Z\\\",\\\"payload\\\":{\\\"artifact_type\\\":\\\"file\\\",\\\"digest\\\":{\\\"algorithm\\\":\\\"sha256\\\",\\\"hex\\\":\\\"fb928160092592c8dc4fd7879a20b0ca447f7795015bba079955b0dfd430b273\\\"},\\\"name\\\":\\\"artifact\\\",\\\"size\\\":34,\\\"ci_environment\\\":{\\\"platform\\\":\\\"local\\\"}},\\\"commit_sha\\\":\\\"0796f08779c7f113a75b57c2a4fdcaaf62088919\\\",\\\"signer_type\\\":\\\"Workload\\\"}\",\"issuerKey\":\"025febb4b16070db79f0b651d85119e4e498cbb78edc2218e88e7cd797317e8ab7\"}}]", + "statement": "{\"version\":1,\"rid\":\"sha256:9e8a0bc644123222ba9b038be6d188812083cc6300280f818008741cd5ab8c64\",\"issuer\":\"did:key:zDnaeeZ7ivM3XNk44FjEe9BpFzfCeSvNZpMbbNFSKnCoh2hfP\",\"subject\":\"did:key:zDnaeeZ7ivM3XNk44FjEe9BpFzfCeSvNZpMbbNFSKnCoh2hfP\",\"device_public_key\":{\"curve\":\"p256\",\"key\":\"02d1d87e8311aec49790566bcf9ca90220e37acf5574198e531d31b91f1ad5b6fa\"},\"identity_signature\":\"b9ca9da852a0256d85d02dabdcdde1aae3c59a3bb48e9be6e6660ecd080b49f21a01c10d0679c5bd30f3045c97f78b6ca01f564dca5c782f54c92588ec7e2e38\",\"device_signature\":\"b9ca9da852a0256d85d02dabdcdde1aae3c59a3bb48e9be6e6660ecd080b49f21a01c10d0679c5bd30f3045c97f78b6ca01f564dca5c782f54c92588ec7e2e38\",\"timestamp\":\"2026-07-19T20:49:37.600634Z\",\"payload\":{\"artifact_type\":\"file\",\"digest\":{\"algorithm\":\"sha256\",\"hex\":\"9e8a0bc644123222ba9b038be6d188812083cc6300280f818008741cd5ab8c64\"},\"name\":\"artifact\",\"size\":6392,\"ci_environment\":{\"platform\":\"local\"}},\"commit_sha\":\"0796f08779c7f113a75b57c2a4fdcaaf62088919\",\"signer_type\":\"Workload\"}" +} From 640daa269e0d85a18a3a8a7e8e44640296f6cc1f Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 22 Jul 2026 21:59:53 +0100 Subject: [PATCH 2/2] chore(web): update root trust configuration Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 13 --- .auths/roots | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .auths/roots diff --git a/.auths/roots b/.auths/roots new file mode 100644 index 0000000..42a5528 --- /dev/null +++ b/.auths/roots @@ -0,0 +1,2 @@ +# Pinned by auths init — the trusted root for this identity. +did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd