feat(swarm): GET /swarm/lessons/{id}/proof endpoint — sub-phase 4e (#105)#128
Conversation
) The §4.6 Merkle-inclusion-proof endpoint sits on top of the producer-side substrate (migration 077, sub-phase 4e substrate) and the 4c evidence-merkle module. Receivers can now verify a Lesson's evidence_root commitment without ever seeing raw episode content. mcp-server/src/swarm/endpoints/lesson-proof.ts (new) * buildLessonProofResponse — async handler returning {status, headers, body} triple, dependency-injected (loadSelf, loadLesson, loadLeaves, wasPublishedByThisNode, signRecord) so tests exercise the full path without touching Supabase or the on-disk node key. * Status mapping per §4.6: - 200 with signed envelope when this node IS the lesson's producer and producer-side leaves are recorded - 404 when never held OR held only as a swarm relay (per §4.6 a relay MUST NOT sign a proof envelope on the producer's behalf) - 410 when this node DID publish (lesson_chain row exists) but the content is gone — authoritative, receiver MUST NOT retry this peer - 503 for precondition failures (no self-identity, signing failure, substrate corruption); operator-readable error strings throughout - 400 for malformed lesson_id (UUID precondition catches it before any DB call) * Relay guard fires BEFORE loadLeaves — even if leaves are locally available, a non-origin node never signs an envelope. * Defensive shape pin on every leaf (46-char Qm... multihash) before they enter the response — a malformed substrate row becomes a 503, not an envelope every receiver would drop under rule 18. mcp-server/src/services/evidence-merkle.ts * New exports computeRootFromHashedLeaves and buildInclusionProofFromHashedLeaves — same internal computeRoot / collectAuditPath helpers, exposed without the input hash-and-sort step. Lets the proof endpoint reuse the producer's algorithm verbatim instead of duplicating the Merkle math (which would create a divergence surface — receivers depend on the producer and the endpoint folding leaves identically). mcp-server/src/services/wire-validator.ts * New validateLessonProof — receiver-side §5 rules 17 + 18 plus presence/type, spec_version major (rule 1), envelope signature (rule 5), signed_at future bound (rule 7), and an evidence_count shape pin so a 0/0 envelope cannot slip past rule 17. * Separate entry point (not folded into validateWireRecord's WireKind discriminator) because a proof envelope is the answer to a §4.6 challenge, not a wire record that gets ingested into swarm_lessons. Rules 17/18 are still §5 rules — same ledger, different surface. Tests * lesson-proof-endpoint.test.ts (new): 1-/2-/5-/7-leaf round-trip against a real node:http server (assertions on status, headers, body shape, Ed25519 envelope verification, every proof reconstructs the root); 404 (not held); 410 (published-then-forgotten); 404 (relay guard); 503 (no self-identity, missing substrate, malformed leaf); 400 (non-UUID id). Privacy contract test scans the response body for any UUID-shaped substring other than lesson_id and for any of the (UUID-shaped) raw experience IDs the fixture used — both forbidden by Designprinzip 2. * wire-validator.test.ts: 10 new tests for validateLessonProof covering every rule (1, 2, 3, 5, 7, 16, 17, 18-tampered-path, 18-wrong-root) plus the happy path. Out of scope for 4e * Reputation decay arithmetic on rule 17/18 failure (4f territory). * Wiring the handler into a top-level HTTP router (no router exists yet in this repo — node-advertisement.ts has the same shape and will be wired together once the router lands). * Caching the per-leaf proof keyed on useful_count increments (perf TODO; spec says MAY). * Cross-reference proof.evidence_root vs the stored lesson row's evidence_root — that lives in the receiver's ingest path. Closes part of #105. 568/569 tests pass; the single failure is the pre-existing migration-074 test that depends on PR #119 landing first (074_lesson_chain.sql is not on main yet). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Cross-PR collision flag — autonomous tick 2026-04-30 (post-#131). This PR and #119 both modify
The The Recommendation: land #119 first (it's been waiting longer and was already rebased after #123). Then rebase this PR; the resolution is mechanical:
Verified via |
* docs(swarm): refresh §9 implementation status table Six sub-phases (4c, 4d-substrate, 4e-substrate, 4f, 4g, 4h, 4i.1) have landed on main since the last status pass; the table still labelled most of them "spec-only" or "PR pending". Refresh entries with their actual merged commit hashes, mark the three remaining open PRs (#119 / #128 / #129) as such, and split the omnibus 4i row into 4i.1–4i.4 to mirror the sub-phase decomposition called out in PR #129's body. The trailing "Phases 4b–9 are spec-only after this revision lands" paragraph was outright wrong now that most of 4b–4i has landed — rewritten to describe the actual state and the residual 4i.3/4i.4 work. No spec changes; documentation snapshot only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(swarm): include 4i.3 PR #131 in §9 status table PR #130 was filed before #131 was opened, so its 4i.3 row still said "spec-only". Update the row to point at the open PR and the migration 082 number, and adjust the trailing summary so 4i.4 is the only residual spec-only sub-phase under 4i. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-phases 4d (#119 → 09c64b8), 4e endpoint (#128 → 170d7cc), 4i.2 (#129 → 69dd87c), and 4i.3 (#131 → dc88488) all landed on main. The status table still showed them as 🟡 PR open — flip to ✅ on main. Also adds a 4j row for the self-broadcast safety triggers (migration 085, c4a545e) that landed via PR #156 as a follow-up to #155, and rewrites the prose paragraph to point at the active wiring PRs (#136 through #143) so a reader scanning §9 sees what comes next. Direct-to-main per the tick-prompt's docs/spec carve-out — pure documentation, no code or migration touched. Note: PR #151 (`agent/swarm-docs-status-refresh-2`) had the same table fix queued, but it was branched before c4a545e landed and would have reverted migration 085 + its test on merge. Refreshing main here means PR #151 now goes to MERGE-CONFLICT and Reed can close it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…) (#154) Pure handler module + 19 contract tests for the §10.6 admission firebreak. All side-effects (loadSelf, getPubkeyForNode, getQuarantinedUntil, insertSwarmLesson, setQuarantine, recordTrustEdgeChange, runContradictionGate) are injected via deps so tests run with no DB / HTTP. Status mapping per issue #138 acceptance: 400 plain — non-quarantine validator failures (rules 2/3/4/7/8/9/11/12/13) 400 + Q — single-strike PoK rules 16 + 20 (§5/§10.2) 401 + Q — bad signature (rule 5) 401 + Q — broken commitment chain (rule 19) 401 plain — unknown peer (no edge to quarantine) 403 — origin currently quarantined (pre-check before sig work) 503 — dependency threw (operator-readable detail) 201 — admitted at lesson_tier='B', trust +0.05 'admitted', §10.3 contradiction gate run, findings count returned No HTTP wiring, no migration, no dashboard-server.mjs touch — wiring lands as a follow-up PR (mirror of the #128 → #152 split that worked for the proof endpoint). Test plan - [x] `npm test` — 665 pass (was 646; this PR adds 19 contract tests) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#105) PR #128 shipped buildLessonProofResponse with full unit + integration coverage but never wired the handler into an HTTP host, so the SWARM_SPEC §4.6 inclusion-proof endpoint has been unreachable since the merge. This mounts the route on scripts/dashboard-server.mjs alongside the existing /.well-known/mycelium-node handler — same DI shape, same shared dist. The loaders read `lesson_evidence_origin` and `swarm_lessons` directly via PostgREST instead of the get_lesson_evidence RPC, since that RPC fails to be created on PG16 until migration 084 lands (issue #135 / PR up to use the RPC" refactor cannot regress without flipping a test red. The route regex anchors on a v1–v8 UUID so a malformed id falls through to the static handler as a clean 404 rather than reaching the proof handler. UUID parsing is also enforced inside buildLessonProofResponse; the regex is defense-in-depth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#105) (#152) PR #128 shipped buildLessonProofResponse with full unit + integration coverage but never wired the handler into an HTTP host, so the SWARM_SPEC §4.6 inclusion-proof endpoint has been unreachable since the merge. This mounts the route on scripts/dashboard-server.mjs alongside the existing /.well-known/mycelium-node handler — same DI shape, same shared dist. The loaders read `lesson_evidence_origin` and `swarm_lessons` directly via PostgREST instead of the get_lesson_evidence RPC, since that RPC fails to be created on PG16 until migration 084 lands (issue #135 / PR up to use the RPC" refactor cannot regress without flipping a test red. The route regex anchors on a v1–v8 UUID so a malformed id falls through to the static handler as a clean 404 rather than reaching the proof handler. UUID parsing is also enforced inside buildLessonProofResponse; the regex is defense-in-depth. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#138) (#163) PR #154 shipped the pure handler module `lesson-admission.ts` with 19 contract tests but explicitly deferred the HTTP wiring to mirror the #128 → #152 substrate-then-wiring split. Until this lands, a peer posting a signed v1.1 envelope hits the static fall-through and gets 404'd — the receiver pipeline is conceptually closed but operationally unreachable. This wires admitSwarmLesson onto scripts/dashboard-server.mjs so `curl -X POST /swarm/lessons -d <envelope>` returns the 201/4xx/503 mapping issue #138 specifies. Adapters lean on existing primitives: - getPubkeyForNode → pgSelect nodes.pubkey_b64url, decode b64url - getQuarantinedUntil → pgSelect nodes.quarantined_until - insertSwarmLesson → POST /swarm_lessons (lesson_tier hard-pinned 'B' per §10.6) - recordTrustEdgeChange → record_trust_edge_change RPC (read-modify-write so the +0.05 delta becomes an absolute new_weight the audit row records) - setQuarantine → PATCH /nodes?node_id=eq.X (quarantine_origin RPC enforces p_days > 0 and can't represent the §10.2 1-hour single-strike window) Three deps deliberately undefined and documented at the call site: 1. getExpectedPrevLessonHash — receiver-side rule 19 needs a per-origin received_lesson_chain table (lesson_chain is producer-only). Follow-up. 2. runContradictionGate — gate lives in the MCP server services tree, not the dashboard host; §10.5 REM self-audit catches the same contradicts asynchronously. 3. (No third — the two above plus setQuarantine's PATCH-vs-RPC trade are the conscious gaps; everything else is wired.) Tests: contract test pins the wiring at the source-text level (7 tests: import shape, POST+exact-URL guard, registration before /api/ proxy, table/RPC/PATCH targets). Same shape-pin discipline as dashboard-server-lesson-proof-wiring.test.ts. cd mcp-server && rm -rf dist && npm run build # clean npm test # 835/835 (was 828) Refs #138. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nly Vererbung+federation-transport are deferred Roadmap line 194 lumped "Schwarm + Vererbung + Föderation" as deferred, but §10 mechanics (anti-echo, contradicts gate, diversity, REM-self-audit, inbound POST /swarm/lessons admission) have been wired into active code on main since PRs #91, #121, #128, #131, #148, #154, #155. Issues #137 and #138 carry status snapshots from earlier ticks confirming end-to-end wiring against migrations 080 + 082. Splits item 5 into two distinct lines (Paarung/Vererbung — migrations.deferred 033–036, Cross-Host-Föderation — 038/041) and adds a clarifying paragraph that points to the still-active §10 substrate plus docs/waves.md for the cross-wave view. The closing paragraph now also names what specifically the single brain doesn't know (cross-host trust transport), instead of the catch-all "Schwarm/Föderation". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Lands the §4.6 Merkle-inclusion-proof endpoint and the receiver-side rules 17 + 18 validator. Sits on top of the producer-side substrate (migration 077, sub-phase 4e substrate) and the 4c evidence-merkle module — receivers can now verify a Lesson's
evidence_rootcommitment without ever seeing raw episode content.mcp-server/src/swarm/endpoints/lesson-proof.ts—buildLessonProofResponseasync handler with the full 200/404/410/503/400 status mapping per §4.6, dependency-injected so tests exercise the full path without Supabase or the on-disk node key. Relay guard fires beforeloadLeaves— even if leaves are locally cached, a non-origin node never signs an envelope.mcp-server/src/services/evidence-merkle.ts— two new exports (computeRootFromHashedLeaves,buildInclusionProofFromHashedLeaves) that reuse the producer's algorithm verbatim instead of duplicating the Merkle math. Avoids the divergence surface that would let producer-vs-endpoint drift silently break every receiver's rule 18 check.mcp-server/src/services/wire-validator.ts— newvalidateLessonProofcovering rules 17 + 18 plus presence/type, spec_version major (rule 1), envelope signature against the producer's Ed25519 key (rule 5),signed_atfuture bound (rule 7), and anevidence_countshape pin so a 0/0 envelope cannot slip past rule 17. Separate entry point fromvalidateWireRecordbecause a proof is the answer to a §4.6 challenge, not a wire record that gets ingested.Privacy invariant (Designprinzip 2)
Substrate stores only hashed leaves, so the response naturally contains no raw experience IDs. The contract test scans the response body for any UUID-shaped substring other than
lesson_idAND for any of the (UUID-shaped) raw experience IDs the fixture used to build the tree — both are forbidden by Designprinzip 2.Out of scope for 4e
useful_countincrements → perf TODO; spec says MAYTest plan
npm run build— TypeScript cleannpm test— 568/569 tests pass; the single failure is the pre-existingmigration-074test that depends on PR feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119 landing first (074_lesson_chain.sqlis not on main yet, unrelated to this PR)lesson-proof-endpoint.test.ts— 1-/2-/5-/7-leaf round-trip against a realnode:httpserver with full envelope verification; 404 (not held); 410 (published-then-forgotten); 404 (relay guard); 503 (no self-identity, missing substrate, malformed leaf); 400 (non-UUID id); privacy contract scanwire-validator.test.ts— 10 new tests forvalidateLessonProofcovering rules 1, 2, 3, 5, 7, 16, 17, 18-tampered-path, 18-wrong-root + happy pathCloses part of #105.
🤖 Generated with Claude Code