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
+8 -1
View File
@@ -14,7 +14,7 @@ use axum::Router;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use crate::api::{exists, fetch, report, upload};
use crate::api::{exists, federation, fetch, report, upload};
use crate::state::AppState;
use crate::validate::limits;
@@ -49,6 +49,13 @@ pub fn router(state: AppState) -> Router {
)
// §5a — anonymous bearer capability, not an account.
.route("/tokens", post(upload::post_token))
// §9a federation. Pull-based: a peer chooses what it ingests and when,
// so every route here is a read. There is deliberately no push endpoint.
.route("/federation/changes", get(federation::get_changes))
.route("/federation/manifests/{content_id}", get(federation::get_manifest_by_content_id))
.route("/federation/have", post(federation::post_have))
.route("/federation/peers", get(federation::get_peers))
.route("/federation/capabilities", get(federation::get_capabilities))
.layer(DefaultBodyLimit::max(SMALL_BODY_LIMIT));
Router::new()