Identities for entities did.bot
agent llm did
didbot docs web-surfaces.md
8.3 kB
Markdown
at main

The web surfaces #

Five websites sit around this project, and three of them are dashboards. They look similar from the outside and they are not interchangeable: each has a different origin, a different thing serving it, a different audience, and a different reason it may not be merged with the others. An agent asked to "work on the dashboard" or "the site" has to say which one, and the answer changes what language it is written in, which host it runs on, and who may operate it.

The three dashboards, in one line each:

  • Onboarding and policy — the friendlier form of the operator's command line. Never served by a personal data server. Surface 2.
  • Server operations — health, certificates, sessions, refusals, the emergency stop. Served by the server it is about; operable only by its signed-in operator, and never public. Surface 3.
  • Analytics — what this deployment's agents are doing. Served by the server it is about; read-only, and the operator may make it public. Surface 4.

Surface 1 — the marketing site, did.bot #

A static site. Marketing and launch pages, the prose documentation from docs/, and the compiled Rust API documentation. Built from site/ and published to an object store behind a CDN; nothing about it is served by this project's own binaries, and it holds no state.

It is the only one of the five whose audience is people who have not yet decided anything.

Surface 2 — the onboarding and policy dashboard, policy.did.bot #

A static page with no backend, and it may not be served by the server it governs. It is the friendlier form of didbot operate: the operator signs in against their own personal data server as a public OAuth client, walks a fresh server through the onboarding steps, writes the operator record, and writes policy records with their own key.

The reason is in policy-dashboard and it is worth repeating here, because it is the constraint most likely to be accidentally violated by someone consolidating pages: if the server this policy governs also served this page, a compromised server would serve modified JavaScript, the operator would sign the attacker's policy with their own key, and every downstream check would pass. Hosting it elsewhere removes that path.

It is served at https://policy.did.bot. policy-site/ is the page, and infra/policy-site/ gives it its own bucket and CloudFront distribution, apart from did.bot's, so a mistake in one site's deploy cannot reach the other.

The page uses no cookies: it keeps its OAuth session in IndexedDB, which is scoped by origin. So the cookie-scoping problem in agent-sites, where a cookie set by one host under a shared domain is sent to its siblings, cannot touch that session.

Surface 3 — the server operations dashboard, served by the PDS #

Served by this project's own binary, out of didbot-serve, about the server serving it, for the one person who operates it. Live operational state — health, certificates, sessions, grants, refusals and what refused them — and it carries the emergency stop. Every control it holds is reached only by the signed-in operator, and the page is never public: nothing on it is safe for a stranger to operate, and a stranger reading it learns which agents are sessions on which hosts.

This is the one that has to interoperate with Rust. It is not a static site built elsewhere and copied in: it is served by the running server, reads that server's own state, and its front end talks to Rust either through the server's routes or through wasm compiled from this workspace. Anything built for it belongs in this repository, in didbot-serve and the asset directory beside it.

Two consequences that catch people out:

  • It owns no policy. Everything it can change is operational. Policy is written from surface 2, into the operator's own repository, and only read here.
  • It can lie. A compromised server serves this page and also supplies its numbers, so anything security-relevant has to be checkable against the operator's own records instead. ops-dashboard makes saying so a deliverable of the page rather than a footnote.

There is one of these per deployment, not one for the project.

Surface 4 — the analytics dashboard, served by the PDS #

Also served by didbot-serve, about the same server, and a different page with a different audience. It shows what this deployment's agents are doing: which accounts exist, the lineage between them, what each wrote and into which collections, which apps each signed in to. It is read-only by construction — every route under it is a read, and nothing on it acts on the server — which is what makes it safe for the operator to set public. The default is operator-only, and the setting is the operator's alone.

Everything it draws is either a public record or a number the server already publishes through bot.did.stats, so making it public discloses nothing a stranger could not assemble from the firehose; what it adds is the assembly. The unauthenticated homepage a running deployment answers with — /dashboard and /dashboard/api/about — is this surface's public face. analytics-dashboard is the epic.

Its pages sit under /analytics, apart from the operations dashboard's /dashboard prefix, so "every route under this page is a read" is a thing a test can walk the router and check. GET /analytics is the tree view: the deployment's accounts drawn as the tree they form, from each root down through the accounts it created, with each account's handle, DID, kind, label, operator, the proof its document publishes and the state of the edge above it. The page is rendered by the server, not assembled in the browser, and the values it draws are the ones bot.did.listAccounts answers with, so the page and the wire cannot describe the tree differently. Both are gated on the same [disclosure] setting: closing the roster on the wire closes it here.

An account whose chain a stranger could not walk is drawn as such. An edge reads paused once the vouch above it is gone and the grace window has passed, and every account beneath it reads the same, because that is what the confinement poll cascades. The header carries the deployment's own lifecycle line — what it is, and whether it serves reads, accepts writes and provisions accounts — so a reader learns a halted server is halted without provoking a 503.

It is per deployment, where surface 5 is across deployments: the same kind of picture, one server's own accounts against the whole network's.

Surface 5 — vibescrobble.com, another repository #

A consumer of what this project publishes, developed separately and not in this tree. It reads data served from personal data servers — this project's among them — over the ordinary public interfaces.

It is named here so that nobody looks for it in this repository, and so that a change to a public interface is understood to have a reader outside this codebase. It gets no special access and no private endpoint; if it needs something, that something is a public capability and belongs in a lexicon.

Which one is being asked for #

If the work is… It is surface It lives in
Marketing, docs, API reference 1 site/, static
Onboarding a server or editing policy, as the operator 2 policy-site/ and infra/policy-site/, static
Watching the health of, or stopping, a running server, as its operator 3 didbot-serve, this repo
Seeing what one deployment's agents are doing, as anyone the operator allows 4 didbot-serve, this repo
Reading published records across servers 5 another repository

Surfaces 1 and 2 are static and may be built with whatever a static site is best built with. Surfaces 3 and 4 are part of the server and follow this workspace's conventions; they share a binary, and didbot_serve::surface holds the plumbing under both — the panel envelope, the operator session check, the refusal shape and static asset serving — while each page keeps its own prefix, its own routes and its own file. A control belongs on 3, a picture on 4. Surface 5 is somebody else's build entirely.