From d7ba1ab0800e20809fcb80732cdc7296bb672b53 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Tue, 1 Sep 2026 22:32:45 -0400 Subject: [PATCH] docs: add the names an agent has One worked account from the zone to the repository: the three records that exist, the two hostnames the server answers on, and the four fields a resolver reads out of the DID document. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: I4843f91437d31f82b66a39732cebce428f2541b8 --- README.md | 2 + crates/didbot/src/lib.rs | 3 + docs/names.md | 177 +++++++++++++++++++++++++++++++++++++++ docs/overview.md | 3 + 4 files changed, 185 insertions(+) create mode 100644 docs/names.md diff --git a/README.md b/README.md index 64349857..8594d2c7 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ To that end, here is a non-exhaustive list of recommendations for a respectful ` is served by what - `docs/trust-model.md` — what the system can and cannot prove about an agent - `docs/deployment.md` — the zone, the hosts, and what a compromise of each reaches +- `docs/names.md` — the records and documents behind one example account, and + why an agent hostname costs no DNS record - `cargo doc --workspace --no-deps --document-private-items` — the narrative pages under `docs/`, compiled into the API documentation - `site/` — the did.bot website: marketing pages, `docs/` rendered as prose, diff --git a/crates/didbot/src/lib.rs b/crates/didbot/src/lib.rs index 2f886a36..d5c7c543 100644 --- a/crates/didbot/src/lib.rs +++ b/crates/didbot/src/lib.rs @@ -20,6 +20,9 @@ pub mod trust_model {} #[doc = include_str!("../../../docs/deployment.md")] pub mod deployment {} +#[doc = include_str!("../../../docs/names.md")] +pub mod names {} + #[doc = include_str!("../../../docs/web-surfaces.md")] pub mod web_surfaces {} diff --git a/docs/names.md b/docs/names.md new file mode 100644 index 00000000..e0dcf522 --- /dev/null +++ b/docs/names.md @@ -0,0 +1,177 @@ +# The names an agent has + +An account is two hostnames, a DID document and a plain-text DID. None of that +is a DNS record. This page works one example all the way through: what exists +in the zone, what the server answers, and what a stranger reads to get from a +name to a repository. + +[Deployment](deployment.md) states the rules this example obeys; +[the shape of the system](architecture.md) draws the hosts around it. + +## The example + +A zone `pds.example`, delegated to one server at `203.0.113.10`. One account, +minted with a namer, so it has both hostnames a deployment can issue: the +opaque one the DID is minted from, and the issued handle. + +| | | +| --- | --- | +| zone | `pds.example` | +| DID | `did:web:7f3a9c.pds.example` | +| handle | `basalt-kestrel.pds.example` | +| PDS endpoint | `https://pds.example` | + +A deployment without a namer has one hostname per account rather than two: the +handle is then the DID's own host, which `Provisioner::issued_handle` reports +as no issued handle at all, because there is nothing extra to resolve or +withdraw. + +## Everything that exists in DNS + +Three names, and none of them is per-account: + +```text +pds.example. NS ns-… (in the parent zone, + added by hand) +pds.example. A 203.0.113.10 +*.pds.example. A 203.0.113.10 +_acme-challenge.pds.example. TXT "" "" (only while a + certificate is + being issued) +``` + +Minting an account writes nothing here. `7f3a9c.pds.example` and +`basalt-kestrel.pds.example` resolve because `*.pds.example` synthesizes an +answer for them, and they do so the instant the name is chosen — there is no +propagation to wait for and no external call on the provisioning path. + +`didbot-dns`'s `WildcardDns` is the provider for this: it keeps the same +per-name bookkeeping every other backend keeps, refuses a host outside its +zone, and publishes nothing, because there is nothing left to publish. + +## Everything the server answers + +
+ + + + + + + + + + DNS ZONE · pds.example · external + pds.example + A · 203.0.113.10 + *.pds.example + A · 203.0.113.10 + _acme-challenge.pds.example + TXT · two values at once, only during issuance + + three records, whatever the account count + provisioning writes none of them + the NS delegation lives in the parent, by hand + + SYNTHESIZED — no node in the zone + 7f3a9c.pds.example + 203.0.113.10 + basalt-kestrel.pds.example + 203.0.113.10 + any depth, while no closer node exists — + publishing one real record here would end that + + + + resolves + + ONE LISTENER · 203.0.113.10 + certificate: pds.example + *.pds.example + a TLS wildcard matches exactly one label — unlike the DNS one + dispatch on the Host header + one process, every account; the name is the whole key + + 7f3a9c.pds.example + the DID's own hostname + GET /.well-known/did.json + application/did+json + id · alsoKnownAs · #atproto · #atproto_pds + 404 for a name no account holds + + basalt-kestrel.pds.example + the issued handle + GET /.well-known/atproto-did + text/plain + did:web:7f3a9c.pds.example + answers only when the document agrees + + WHAT did.json SAYS · the four fields atproto reads + id + did:web:7f3a9c.pds.example + alsoKnownAs + at://basalt-kestrel.pds.example + verificationMethod + #atproto · Multikey + service + #atproto_pds · https://pds.example + the rest of the W3C surface is not emitted, because nothing reads it + + + + + points back at the same account + + WHAT A STRANGER DOES + 1 · handle → GET /.well-known/atproto-did → a DID + 2 · DID → GET /.well-known/did.json on its own host + 3 · check id, and alsoKnownAs back to the handle + 4 · #atproto_pds → the XRPC surface + no step consults DNS for anything but reachability +
+ +Both hostnames land on the same listener, and the `Host` header is the only +thing that says which account a request means. `did_json` and `atproto_did` in +`didbot-serve`'s routes both read it, and both derive their answer from one +registry, so the bidirectional check holds by construction: a resolver +following the handle to the DID and the DID to its document always finds the +handle again in `alsoKnownAs`. + +A name no account holds is a 404 rather than an `NXDOMAIN`, because the +wildcard resolves it and the TLS handshake completes. That is a conformant +answer — a resolver treats any non-200 as unresolvable — and it is the only +one that can express a soft-deleted account, which keeps serving its document +and its verification method while its repository is empty. + +## What the wildcard does not cover + +**The apex.** `*.pds.example` does not answer for `pds.example`. That is a +separate record, and the reason [deployment](deployment.md) keeps agents off +the apex in the first place. + +**Any type that is not at the wildcard node.** A wildcard holding `A` answers +`NODATA` for a `TXT` query at a synthesized name. Nothing wildcards the ACME +DNS-01 challenge: the authority asks for a literal token at +`_acme-challenge.`, so a deployment issuing its own certificates needs a +provider that writes real records, and needs two values present at one name +simultaneously to cover both the apex and the wildcard in one order. + +**Anything below a name that exists.** DNS wildcards synthesize at any depth, +but only while no closer node exists — publish a real record at +`basalt-kestrel.pds.example` and `x.basalt-kestrel.pds.example` stops +resolving, because the lookup now wants `*.basalt-kestrel.pds.example` and +finds nothing. Per-account records and deep names are mutually exclusive; the +TLS certificate rules out the deep names anyway. + +## DNS wildcards and TLS wildcards are not the same rule + +They are easy to conflate and they differ where it matters: + +| | matches | +| --- | --- | +| `*.pds.example` in DNS | any depth — `a.pds.example`, `a.b.pds.example` — while no closer node exists | +| `*.pds.example` in a certificate | exactly one label, and never the bare parent | + +So the constraint on how deep an agent hostname may sit is the certificate, +not the zone. No authority issues a multi-label wildcard, which is why a +deployment with two zones holds two certificates and +`didbot_tls::fleet::CertificateFleet` keeps one manager per zone. diff --git a/docs/overview.md b/docs/overview.md index 1d9c381e..9ef109d6 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -15,6 +15,9 @@ namespace, and the Claude Code hook protocol. prove about an agent, and why those are separate questions. - [The hook flow](../docs/hook-flow.md) — how an agent context becomes an account, and how a status update gets written. +- [The names an agent has](../docs/names.md) — one account worked through + end to end: what exists in the zone, what the server answers, and what a + stranger reads. - [The web surfaces](../docs/web-surfaces.md) — the four websites around this project, which is served by what, and why two of them may not be merged. -- 2.51.2