Skip to content

feat(swarm): GET /swarm/lessons/{id}/proof endpoint — sub-phase 4e (#105)#128

Merged
Dewinator merged 1 commit into
mainfrom
agent/swarm-4e-proof-endpoint
Apr 30, 2026
Merged

feat(swarm): GET /swarm/lessons/{id}/proof endpoint — sub-phase 4e (#105)#128
Dewinator merged 1 commit into
mainfrom
agent/swarm-4e-proof-endpoint

Conversation

@Dewinator
Copy link
Copy Markdown
Owner

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_root commitment without ever seeing raw episode content.

  • mcp-server/src/swarm/endpoints/lesson-proof.tsbuildLessonProofResponse async 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 before loadLeaves — 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 — new validateLessonProof covering rules 17 + 18 plus presence/type, spec_version major (rule 1), envelope signature against the producer's Ed25519 key (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 from validateWireRecord because 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_id AND 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

  • Reputation decay arithmetic on rule 17/18 failure → 4f territory
  • Top-level HTTP router wiring → no router exists in this repo yet (node-advertisement.ts has the same handler shape; both will be wired together once the router lands)
  • Per-leaf proof caching keyed on useful_count increments → perf TODO; spec says MAY
  • Cross-reference proof.evidence_root vs the stored lesson row's evidence_root → lives in the receiver's ingest path

Test plan

  • npm run build — TypeScript clean
  • npm test — 568/569 tests pass; the single failure is the pre-existing migration-074 test that depends on PR feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119 landing first (074_lesson_chain.sql is not on main yet, unrelated to this PR)
  • lesson-proof-endpoint.test.ts — 1-/2-/5-/7-leaf round-trip against a real node:http server 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 scan
  • wire-validator.test.ts — 10 new tests for validateLessonProof covering rules 1, 2, 3, 5, 7, 16, 17, 18-tampered-path, 18-wrong-root + happy path

Closes part of #105.

🤖 Generated with Claude Code

)

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>
@Dewinator
Copy link
Copy Markdown
Owner Author

Cross-PR collision flag — autonomous tick 2026-04-30 (post-#131).

This PR and #119 both modify mcp-server/src/__tests__/wire-validator.test.ts and mcp-server/src/services/wire-validator.ts. Re-ran the merges in scratch worktrees against current main:

Pair wire-validator.ts wire-validator.test.ts
#119#128 auto-merge clean CONFLICT at line 638 (textual only)
#128#119 auto-merge clean CONFLICT at line 638 (textual only)

The .ts file auto-merges because the two PRs touch disjoint regions of the file: #119 inserts the rule-19 block inside validateWireRecord (before the final return { ok: true }), while this PR appends validateLessonProof after the function close. No semantic overlap.

The .test.ts conflict is a pure mechanical "two appends to the same anchor" — both PRs add a fresh block after the existing line 638 (v1.1 Lesson with prev_lesson_hash = null test). Neither block depends on the other's tests. Resolution = keep both blocks in either order.

Recommendation: land #119 first (it's been waiting longer and was already rebased after #123). Then rebase this PR; the resolution is mechanical:

  1. git rebase main after feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119 lands
  2. On the wire-validator.test.ts conflict, the imports at the top auto-merge (feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119 doesn't touch them, this PR adds validateLessonProof + evidence-merkle helpers). Only the trailing-append region needs a one-line resolution: keep this PR's validateLessonProof block, then either before or after feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119's rule-19 block — both orderings produce identical test runs.
  3. No wire-validator.ts work needed (auto-merges).
  4. Migration order is naturally clean — feat(swarm): prev_lesson_hash chain table — sub-phase 4d (#104) #119 owns 074, no migration here.

Verified via git merge-tree --write-tree: every other pairwise combination of the five open PRs (#119, #128, #129, #130, #131) merges cleanly, so this is the only collision left in the open queue.

Dewinator added a commit that referenced this pull request Apr 30, 2026
* 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>
@Dewinator Dewinator merged commit 170d7cc into main Apr 30, 2026
Dewinator added a commit that referenced this pull request May 1, 2026
Sub-phases 4d (#11909c64b8), 4e endpoint (#128170d7cc), 4i.2 (#12969dd87c), and 4i.3 (#131dc88488) 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)
Dewinator added a commit that referenced this pull request May 1, 2026
…) (#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>
Dewinator added a commit that referenced this pull request May 1, 2026
…#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>
Dewinator added a commit that referenced this pull request May 1, 2026
…#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>
Dewinator added a commit that referenced this pull request May 2, 2026
…#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>
Dewinator added a commit that referenced this pull request May 3, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant