--- id: federation title: An off-the-shelf relay and an off-the-shelf app, not just the protocol status: open crates: [didbot-serve] dependsOn: [pds-xrpc] exitCriterion: > Bluesky's official client and Tangled both read an agent's repository through this server with no configuration naming this project, and a relay has crawled it at least once without help. --- # federation [pds-xrpc](pds-xrpc.md)'s exit criterion is "an off-the-shelf atproto client reads and writes an agent's repository through `com.atproto.*` knowing nothing about this project," and its own conformance harness checks every response against the *upstream* lexicon documents rather than a type written here. That is protocol conformance, checked in isolation. It is not the same claim as two specific, prominent applications actually working against a live deployment, and the two have already diverged once: `pds-xrpc.md`'s own conformance note says a missing `cid` "survived here for months" under the in-process harness and "was caught by somebody else's client instead." Nothing here says which somebody, or on what schedule that check runs again. - [ ] **Test against Bluesky's own client and against Tangled, by name.** Not a hand-rolled script exercising the same endpoints the conformance suite already drives — a real client, pointed at a real deployment, doing what its own users do: log in, browse a profile, read a repository's records. `docs/running-locally.md` already exercises `goat`, indigo's reference client, for `ls`-listing a repository; that is closer to this epic's shape than the in-process harness is, and is worth building out from rather than starting over. - [ ] **Say what "works" means for each app**, since the two exercise different surfaces. Bluesky's client is built for `app.bsky.*` records and expects an actor's profile and feed shape; this project's agents write `bot.did.*` records instead, so "Bluesky reads an agent" may mean no more than the client not crashing on an account with no posts it recognizes, rather than a populated timeline. Tangled is closer to this project's own shape (git-adjacent, atproto-native) and may exercise more of the wire surface meaningfully. Neither should be reported as "works" without saying what was actually checked. ## Where the sync half stands `com.atproto.sync.subscribeRepos` is served — `crates/didbot-serve/src/routes/mod.rs` routes it to `subscribe_repos`, and `pds-xrpc.md`'s Done list has the frame shape. A relay discovers new commits by subscribing to that endpoint, so the thing that used to make federation impossible no matter how conformant the rest of the wire surface was is no longer the blocker. What is left is whether the apps this epic is about read what a relay crawled. - [ ] **Test against Bluesky's own client and against Tangled with a repository a real relay has actually crawled**, not merely served over HTTP. An unmodified `indigo` `cmd/relay` has now crawled two live instances, opened `subscribeRepos` and advanced past the initial cursor, verifying 101 commit signatures with no failures — but every non-first commit failed MST inversion, which is what the covering-proof work addresses. That confirms the wire as far as the relay; nothing yet confirms a crawled repository is reachable through the apps this epic is about. ## What is not the risk `did:web` resolution is confirmed supported by `@atproto/identity`, the identity library both the Bluesky client and Tangled's tooling depend on, and by Go's `indigo` — the same reference implementation `docs/running-locally.md` already drives against this server with `goat`. The identity half of federation is not where this is expected to break. The sync half — `subscribeRepos`, and whatever a relay does with the stream once it can open one — is the part with no working implementation to point at yet, and is where this epic's actual risk sits. - [ ] **Point `relay_hostname` at a relay that will accept this deployment, and run `ANNOUNCE` against it.** The wiring below is checked against a loopback stand-in. What is left is a real relay's reply, and a record of which relay gave it. ## Done - [x] **The deployment names a relay.** `infra/pds/`'s `relay_hostname` variable puts `--relay-hostname` on the systemd unit's `ExecStart`. Boot dials nothing, so a relay that is down while an instance comes up costs the instance nothing, and one that refuses or has gone away is reported to the operator who ran `ANNOUNCE` as `refused` or `unreachable` rather than to a log nobody reads. A test renders the unit for both settings of the variable and checks every flag against this binary's own parser. See `docs/deployment.md`'s "Announcing to a relay". - [x] **Announce a deletion on the stream.** `deleteRecord` puts a `#commit` on `subscribeRepos` carrying a `delete` op — null `cid`, the removed record's CID as `prev` — over a covering proof taken the same way a write's is. A relay that was disconnected across it reads it from its cursor like any other commit, so it cannot diverge from this server without also being told its cursor is unreachable. `prev` was missing from every op before this, so a real relay could invert a creation and not an update or a removal, so the live-relay item above should be cheaper to satisfy now. See [pds-xrpc](pds-xrpc.md)'s Done entry and `docs/conformance.md`'s "Removals". - [x] **Every commit passes a strict relay's checks.** `crates/didbot/tests/subscribe_repos.rs` takes each commit an account makes off the socket and checks it as a strict relay does: its commit is signed by the `#atproto` key the account's DID document publishes, its `since` and `prevData` name the commit before it, its frame carries every record it writes, and its ops undo to `prevData` from its own blocks. The writes cover creates, updates, deletes that merge nodes, and one `applyWrites` batch undone in every order. A write or batch that changes no record makes no commit, a batch's frame names each key once, and a batch that deletes a record that is not there is refused. See `docs/conformance.md`'s "What a `#commit` carries". - [x] **The crawl-side announcement, as an explicit operator command.** `didbot_serve::relay` calls `com.atproto.sync.requestCrawl` and the deprecated `notifyOfUpdate` against a configured relay when an operator asks. Nothing calls either automatically — naming a relay in `--relay-hostname`/`[relay]` only says which relay an operator can announce to, not that this server does so on its own. See `docs/conformance.md`'s own section on it for what each command requires, what its reply distinguishes, and what is verified against an ephemeral loopback stand-in rather than a real relay.