Deferred from #406 (PR #472), which landed federation's serving half: a peer holding a capability this server minted pulls a shared album through GET /v1/sync?album_id= and GET /v1/blob/{hash}, the grant's lifecycle (mint, refresh, revoke) is on the wire, per-peer events-per-hour rides CounterStore, signed federated report intake and the server-level blocklist are in, and both stores have in-memory and Postgres adapters (migration ordinal 6).
What that PR did not build, and why each was left rather than stubbed:
1. The egress pull worker (S-E2 remainder)
Nothing on this server fetches from another. design/federation.md describes a peer pulling "on Capsule's schedule, through Capsule's validation pipeline", and the ownership, schedule and failure policy of that worker are not settled anywhere — it is not a route, so it does not fall out of the API surface. With it come:
- invariant 20 re-validation of everything pulled (all of invariants 1–18 + 25 re-applied to a peer's manifests);
- the per-
(receiving_user, source_peer) storage quota design/quota.md names for cached pulls;
- the breadcrumb index and the soft-fail rejected-hash table (
federation.md), including inbound stale-revival;
- accepting hints on the low-trust channel, which is what would make the pull event-driven rather than polled.
capsule-sdk::federation::FederationPull is the client-side orchestration of exactly this pull and is the shape a server-side worker would drive; it is not a server component.
2. Weighted per-peer budgets, the error budget, the breaker and probation
federation.md names events/hour, bytes/hour and CPU/hour, an error budget with a circuit breaker, and a probation tier for new peers. Only events/hour shipped. The other two need a weighted counter — charge N units in one atomic call — and CounterStore::hit charges one; expressing bytes as N calls is the read-then-write loop that port exists to forbid. Nothing measures per-request CPU at all. error.federation.circuit_open is in the catalog and deliberately unused until this lands.
3. Peer keys are operator-pinned, and no operator command pins them
PeerStore::pin exists and POST /v1/federation/reports verifies against what it holds, but there is no way for an operator to put a key there. capsule-server peer pin|block|unblock was scoped and then dropped, because it cannot work: boot::assemble and boot::assemble_maintenance both refuse the Durable backend outright until #403's adapters land, so such a command would only run under serve --memory — which forgets what it pinned the moment it exits. It should land with or after the durable boot arm.
TOFU-fetching a peer's server-info at intake stays rejected on its own merits: the server has no outbound HTTP client, and making the first report from an unknown server the thing that decides whether to trust it is the wrong moment to decide.
4. Moderation's admin surface (S-C49 remainder)
ModerationStore::pending_reports is the queue and nothing reads it over HTTP. design/moderation.md names an admin throughout and specifies no way for one to authenticate; SLICES.md S-C49 says that question has to be answered first. Blocklist exchange between peers is v2 by the contract and is not this issue.
Where the seams are
capsule-server/src/federation/mod.rs — admit is where a receiving-side budget would be charged; Presentation is where a new presentation kind would go.
capsule-server/src/counter/mod.rs — CounterStore::hit is the port a weighted charge would extend.
capsule-server/src/federation/peers.rs — PeerRecord::signing_key is Option precisely so an unpinned peer is a real state.
capsule-server/src/moderation/mod.rs — pending_reports is the read an admin surface would expose.
Deferred from #406 (PR #472), which landed federation's serving half: a peer holding a capability this server minted pulls a shared album through
GET /v1/sync?album_id=andGET /v1/blob/{hash}, the grant's lifecycle (mint, refresh, revoke) is on the wire, per-peer events-per-hour ridesCounterStore, signed federated report intake and the server-level blocklist are in, and both stores have in-memory and Postgres adapters (migration ordinal 6).What that PR did not build, and why each was left rather than stubbed:
1. The egress pull worker (
S-E2remainder)Nothing on this server fetches from another.
design/federation.mddescribes a peer pulling "on Capsule's schedule, through Capsule's validation pipeline", and the ownership, schedule and failure policy of that worker are not settled anywhere — it is not a route, so it does not fall out of the API surface. With it come:(receiving_user, source_peer)storage quotadesign/quota.mdnames for cached pulls;federation.md), including inbound stale-revival;capsule-sdk::federation::FederationPullis the client-side orchestration of exactly this pull and is the shape a server-side worker would drive; it is not a server component.2. Weighted per-peer budgets, the error budget, the breaker and probation
federation.mdnames events/hour, bytes/hour and CPU/hour, an error budget with a circuit breaker, and a probation tier for new peers. Only events/hour shipped. The other two need a weighted counter — charge N units in one atomic call — andCounterStore::hitcharges one; expressing bytes as N calls is the read-then-write loop that port exists to forbid. Nothing measures per-request CPU at all.error.federation.circuit_openis in the catalog and deliberately unused until this lands.3. Peer keys are operator-pinned, and no operator command pins them
PeerStore::pinexists andPOST /v1/federation/reportsverifies against what it holds, but there is no way for an operator to put a key there.capsule-server peer pin|block|unblockwas scoped and then dropped, because it cannot work:boot::assembleandboot::assemble_maintenanceboth refuse theDurablebackend outright until #403's adapters land, so such a command would only run underserve --memory— which forgets what it pinned the moment it exits. It should land with or after the durable boot arm.TOFU-fetching a peer's
server-infoat intake stays rejected on its own merits: the server has no outbound HTTP client, and making the first report from an unknown server the thing that decides whether to trust it is the wrong moment to decide.4. Moderation's admin surface (
S-C49remainder)ModerationStore::pending_reportsis the queue and nothing reads it over HTTP.design/moderation.mdnames an admin throughout and specifies no way for one to authenticate;SLICES.mdS-C49 says that question has to be answered first. Blocklist exchange between peers is v2 by the contract and is not this issue.Where the seams are
capsule-server/src/federation/mod.rs—admitis where a receiving-side budget would be charged;Presentationis where a new presentation kind would go.capsule-server/src/counter/mod.rs—CounterStore::hitis the port a weightedchargewould extend.capsule-server/src/federation/peers.rs—PeerRecord::signing_keyisOptionprecisely so an unpinned peer is a real state.capsule-server/src/moderation/mod.rs—pending_reportsis the read an admin surface would expose.