Federation: replicate content, re-derive judgement (UR-008)
CI / fmt, clippy, test (push) Failing after 1m20s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 25s

Implements §9a. The replication surface is four reads and no writes: a change
feed, fetch by content_id, a batch have, and a human-facing peer directory —
plus a capabilities endpoint carrying the accepted envelope versions, which
lets a client discover a schema mismatch in one request instead of a 400 per
manifest across a library sweep.

Pull, never push: a pulling server chooses what it ingests and when. Push would
let any peer inject work into the validation queue — the same abuse surface as
anonymous upload, at higher volume.

Nothing inherits a peer's judgement. A pulled manifest runs the full §6 stage 1
and 2 validation and this server's own cast check, and the fetched body must
hash to the content_id that was asked for — the check that stops an
intermediary or a misbehaving peer substituting content under a trusted id.
A peer's retraction flags for review rather than delisting, because
auto-delisting would hand every peer a remote delete primitive; only the opt-in
per-peer abuse channel delists, because a takedown propagating at the speed of
manual review is the wrong failure mode for that one case.

A test caught a real bug in the first cut: the feed cursor was a ULID, and
ULIDs are only monotonic *between* milliseconds — two generated in the same
millisecond carry independent random components and can sort opposite to write
order. A peer resuming from `seq > cursor` would then silently skip an entry:
replication losing manifests with no error anywhere. The cursor is now an
AUTOINCREMENT integer, and the test asserts strict monotonicity rather than
merely sortedness.

Peer administration is deliberately not an API. §9a requires that a peering
exist only because an operator typed a URL, so nothing a remote server returns
can establish or widen one; there_is_no_endpoint_that_creates_a_peering asserts
that absence rather than trusting it.

212 tests. Coverage 25/32 (78%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-008 | PR-006
This commit is contained in:
2026-07-31 09:28:32 +02:00
co-authored by Claude Opus 5
parent 88c7264094
commit 545c7d92a2
18 changed files with 1640 additions and 44 deletions
+9 -7
View File
@@ -38,6 +38,11 @@ Implemented:
- §7 relational storage, no JSON blobs on the write path
- §8 Rust + Axum + SQLite, single serialized writer, in-process job queue
- §9a content addressing (`content_id`), computed on upload
- §9a federation — change feed, fetch by `content_id`, batch `have`, peer
directory, capabilities, and a pull worker that **re-derives judgement rather
than inheriting it**: every pulled manifest runs the full §6 validation and
this server's own cast check, and the body is verified to hash to the
`content_id` requested before it is stored
**Schema version 2** (SR-003). `jmanifest_version` moved to 2 in lockstep with
the truth file's `schema_version` — breaking changes are batched and ship
@@ -75,9 +80,7 @@ Deferred:
`POST /manifests/search` are not wired up. This follows §3's own recommended
sequencing: ship the plugin-side computation first, let signatures accumulate,
then enable matching once coverage is useful.
- §9a federation endpoints (`/federation/*`) and the pull worker. The schema
columns (`content_id`, `origin`, `ingested_from`, `peers`) are in place, and
`ingest::persist` is already the shared path a pull would reuse.
(Federation landed — see below.)
## Running
@@ -124,7 +127,7 @@ curl -sX POST -H 'content-type: application/json' -d '{}' \
## Tests
```sh
cargo test # 191 tests
cargo test # 212 tests
cargo deny check # advisories, licences, bans, sources
scripts/traceability-gate.sh # requirement coverage
```
@@ -137,9 +140,8 @@ git submodule update --init --recursive
It reports coverage against [`docs/requirements.md`](docs/requirements.md),
flags orphan tags (an ID no register defines), and fails on a >100% ratio — the
signal that the computation itself is broken. Currently **24/32 (75%)**; the
untraced remainder is UR-007 (plugin-side) and UR-008 (federation), neither of
which is implemented here yet.
signal that the computation itself is broken. Currently **25/32 (78%)**; the
untraced remainder is UR-007, which is plugin-side.
Unit tests per module, plus two integration suites: