Federation: replicate content, re-derive judgement (UR-008)
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:
+10
-2
@@ -34,7 +34,7 @@ requirement and no fixture-generation step, unlike `scene-actor-extraction`.
|
||||
| UR-005 | Trust without accounts: not usable as a content store, nor for prank manifests | SR-004 | High | Done |
|
||||
| UR-006 | Serve and accept a whole series in one operation | PR-006 | High | Done |
|
||||
| UR-007 | Plugin queries an ordered, configurable list of servers | PR-005 | High | In Progress |
|
||||
| UR-008 | Servers replicate manifests between each other | PR-006 | Medium | Planned |
|
||||
| UR-008 | Servers replicate manifests between each other | PR-006 | Medium | Done |
|
||||
| UR-009 | Store an audio spectral-peak signature for content-based identification | SR-003 | Medium | In Progress |
|
||||
| UR-010 | Identity crossing the API boundary is TMDB/IMDB ids, never a name alone | SR-001 | High | Done |
|
||||
| UR-011 | Reject any field capable of carrying binary or attacker-chosen content | SR-004 | High | Done |
|
||||
@@ -52,6 +52,13 @@ requirement and no fixture-generation step, unlike `scene-actor-extraction`.
|
||||
server list and its per-server trust settings, with the community instance
|
||||
pre-configured but disabled. The fetch path that consumes it does not exist yet.
|
||||
|
||||
**UR-008 is `Done` for the replication surface**: change feed, fetch by
|
||||
`content_id`, batch `have`, the peer directory, and a pull worker that
|
||||
re-validates everything it ingests. Peer *administration* — adding and enabling
|
||||
a peer — is deliberately not an API: §9a requires that a peering exist only
|
||||
because an operator typed a URL, so it is a database action, and
|
||||
`there_is_no_endpoint_that_creates_a_peering` asserts the absence.
|
||||
|
||||
**UR-009 is `In Progress`.** The server accepts, validates and stores
|
||||
`cut.audio_signature`, and `content_id` correctly excludes it (§9a). What is
|
||||
absent is `audio`-tier matching and `POST /manifests/search`. This is the
|
||||
@@ -118,7 +125,8 @@ topology is the point, so this is a deliberate choice rather than an oversight.
|
||||
| UR-004 | T1 + T2 | Limits engage and carry the documented headers | Window reset; a rejected request does not extend its own lockout; surfaces have independent budgets |
|
||||
| UR-005 | T1 + T2 | Prank manifests rejected; no free-text channel | Uncredited cast rejected; name-only matches capped; automatic revocation needs a minimum sample |
|
||||
| UR-006 | T2 | Bundle accepted per-episode, non-atomically | One bad episode rejected while its neighbours are accepted; envelope errors are whole-request `400` |
|
||||
| UR-007 | — | *No server-side test.* Plugin-side; the register there will carry it | — |
|
||||
| UR-007 | — | *No server-side test.* Plugin-side (`jRay` JR-025); the register there carries it | — |
|
||||
| UR-008 | T1 + T2 | Feed, fetch-by-hash, batch have, peer directory | **Cursor is strictly monotonic** — a ULID would sort out of write order within a millisecond and silently skip entries; a peer retraction flags rather than delists; only the opt-in abuse channel delists; `pending` is never replicated; **no endpoint can create a peering** |
|
||||
| UR-009 | T1 | Signature structurally validated | Fixed length; reserved high bit; **media < 120 s must send no signature at all** |
|
||||
| UR-010 | T1 + T2 | Actors persist as TMDB person ids | A name the upload invented does not round-trip |
|
||||
| UR-011 | T2 | Every payload-shaped field rejected | base64, hex, markup, control characters, bidi overrides, compatibility homoglyphs |
|
||||
|
||||
Reference in New Issue
Block a user