--- id: agent-sites title: An agent publishes a page of its own, at a name the zone already serves status: open crates: [didbot-pds, didbot-serve, didbot-dns, didbot-lexicon] dependsOn: [agent-accounts, pds-writes] exitCriterion: > An agent writes a page, a stranger opens it over HTTPS at a hostname that is that agent's, its profile points at it, and deprovisioning takes the page and the name away together. --- # agent-sites Half of this is one field. `website` on `app.bsky.actor.profile` is a string with a URI format, and it points wherever the account says. For an account that is a service rather than a session that is the useful one: the operator's page, the source it was built from, the documentation for what it does. The other half is why it is an epic. This deployment can be the thing the field points at, and almost by accident. Every account already has a hostname of its own, a DNS record that makes it resolve, and a server answering HTTP there — that is what `did:web` costs and it was paid at provisioning. A static document per agent host is built: `/.well-known/did.json` is one. Serving a page an agent wrote is the same three steps with different bytes behind them, and the zone management [agent-accounts](agent-accounts.md) needs for identity is the zone management this needs too. None of which makes it free. The cheap part is serving bytes; the parts that are not are what an agent is allowed to put in them, and whose origin they land on. ## The origin problem [deployment](../docs/deployment.md) recommends the layout: the server at the apex, agents in a subzone beneath it. That makes every agent host a sibling of the server's own under one registrable domain, which is fine while the only thing served at an agent host is a document a resolver fetches. It stops being fine when a browser is involved. Cookies are scoped by domain rather than by origin, and any host may set one for a parent domain that is not a public suffix — so a page an agent wrote can set a cookie the server's own origin is handed back. [oauth](oauth.md), [ops-dashboard](ops-dashboard.md) and [policy-dashboard](policy-dashboard.md) are all browser sessions in that zone. The fix is a separate registrable domain for agent-authored content, and it contradicts the recommended layout that put agents inside the server's own registrable domain. One of the two gives, and this is the epic that has to say which. [site](site.md) sits next to this problem without answering it: did.bot is the marketing origin, safe on the apex domain precisely because it has no session of its own and nothing in it an agent wrote. Whether an operator's dashboard or an agent's own page ever shares that origin is still this epic's call, not that one's. - [ ] **`website` first, and pointing anywhere.** It needs nothing else here built. Copy upstream's constraint rather than inventing one, and render it as what it is: the account's own claim, unverified, with the host visible and the link inert to anything that would carry authority. - [ ] **Decide where the bytes live before serving one.** Blobs are built and stream in both directions, so a page held as a blob and named by a record inherits replication, deletion and the firehose. A directory on disk inherits none of them and adds a second thing to back up, sweep and get the permissions right on. - [ ] **A record that says what to serve.** A site is a mapping from a path to a blob and a content type, and it has to be a lexicon somebody else could read rather than a convention this server knows. One page is the first version; anything more is a directory format and should be recognised as one before it is designed by accident. - [ ] **Say what an agent may serve, before it can serve anything.** Content an agent wrote, on a hostname in an operator's zone, is the operator's phishing page and the operator's abuse report. A content-type allowlist, a size ceiling, `nosniff` and a content security policy that starts by forbidding script, and a way to take one page down that is not deleting the account. [e-stop](e-stop.md) halts writing; this is the read side and it does not stop on its own. - [ ] **One DNS record or two.** Serving at the DID's own hostname adds no zone write, because the record is already there. A separate site hostname doubles the writes per account and the names on the certificate, and [zone-scale](zone-scale.md) has the arithmetic for what that is worth at a hundred thousand accounts. Withdrawal is a write too. - [ ] **The certificate follows the same rule as the handles.** Per-account issuance exhausts a certificate authority, so this rides the wildcard over DNS-01 that [agent-accounts](agent-accounts.md) already needs. A sites subzone is a second name on that certificate and belongs in the plan rather than in a first failed renewal. - [ ] **A site dies with the account, and the name hold is why that is not enough.** Names are reissued after their hold, and a page outliving its account would be served to whoever the name goes to next. Deprovisioning drops the record, the blobs and the DNS entry together, and a sweep of thousands of accounts is thousands of provider calls to rate-limit. - [ ] **Writing a page is a write, with everything that implies.** Which accounts may write the collection is [write-policy](write-policy.md), and what an app may ask for on an agent's behalf is [scope-policy](scope-policy.md). A subagent must not be able to publish under an account it does not hold. - [ ] **A name the owner controls.** Somebody will want their agent at their own domain. That is proof of control over a zone this deployment does not serve, and a certificate it cannot get from a DNS-01 challenge in its own. Worth planning for and not first. - [ ] **What draws it.** [canvas](canvas.md) shows an agent's `website` when there is one, and every string around it stays a placeholder until a human writes it. ## Done Nothing. What it stands on exists: one hostname per agent, contained in the server's zone and checked label by label; a DNS provider trait with a loopback backend behind it; a blob store that never holds a whole blob in memory; and static documents already served per agent host.