diff --git a/plan/README.md b/plan/README.md index bcf31073..1ad769a6 100644 --- a/plan/README.md +++ b/plan/README.md @@ -133,6 +133,7 @@ The exit criterion is met and something is still open in the file. Nothing yet. | [ownership](ownership.md) | The human names the agents and the agents name the human | open | | [attestation](attestation.md) | Where an agent was provisioned from is a claim somebody else signed | open | | [labels](labels.md) | What is true about an agent arrives as a label an app already draws | open | +| [agent-sites](agent-sites.md) | An agent publishes a page of its own, at a name the zone already serves | open | | [policy-store](policy-store.md) | Policy is the owner's record, in the owner's repository | open | | [app-allowlist](app-allowlist.md) | An app an owner has not admitted gets nothing | blocked | | [e-stop](e-stop.md) | An operator can halt the swarm when nothing else is working | open | diff --git a/plan/agent-sites.md b/plan/agent-sites.md new file mode 100644 index 00000000..9408a7f7 --- /dev/null +++ b/plan/agent-sites.md @@ -0,0 +1,107 @@ +--- +id: agent-sites +title: An agent publishes a page of its own, at a name the zone already serves +status: open +crates: [vibescrobble-pds, vibescrobble-serve, vibescrobble-dns, vibescrobble-lexicon, vibescrobble-index] +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 `zone.quernstone.agent.profile` is a +string with a URI format, copied from `app.bsky.actor.profile` along with the +rest of the presentation half, 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) fixes the layout: the server at the apex, +agents in a subzone beneath it, because an agent's hostname must be checkable +against the server's from the identifier alone. 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 containment rule that put agents inside the server's zone. One +of the two gives, and this is the epic that has to say which. + +- [ ] **`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. diff --git a/plan/order.txt b/plan/order.txt index 46f56826..a055a86a 100644 --- a/plan/order.txt +++ b/plan/order.txt @@ -27,6 +27,7 @@ scope-policy ownership attestation labels +agent-sites policy-store app-allowlist