Identities for entities did.bot
agent llm did
README.md

policy-site #

The policy dashboard served at https://policy.did.bot: surface 2 in docs/web-surfaces.md. A static page with no backend. An operator signs in against their own PDS and edits the bot.did.policy and bot.did.policyBinding records in their own repository. Their servers poll those records and enforce them.

It is its own package, with its own lockfile, apart from site/.

Commands #

npm ci
npm run dev      # http://127.0.0.1:5173, sending the deployed CSP header
npm run build    # typechecks, then writes dist/
npm test         # every tests/check-*.mjs, against dist/

The checks module is built separately, by scripts/build-policy-wasm.sh, into public/wasm/. Vite copies it into dist/wasm/ untouched. Without it the page loads, and says the checks did not load. scripts/build-policy-anim-wasm.sh builds the background's module beside it. Without that one the page has a plain ground.

What lives where #

  • src/oauth-config.ts: the client's OAuth identity. The served /client-metadata.json is generated from it by a plugin in vite.config.ts, and the page's own scope and redirect come from it.
  • src/copy.ts: every sentence of prose on the page, as ⟦…⟧ filler until a human writes it. index.html names slots with data-copy="key", filled at build time.
  • src/checks.ts: the only door to the wasm checks. Every verdict the page shows about a record comes from there.
  • recommended/: the policies the page offers to start from, one file each, with the cases didbot-pds judges them by. src/recommended.ts reads them at build time. See its README.
  • src/session.ts: sign-in and sign-out, and which third parties see what.
  • src/pds.ts: reads of the operator's repository, and its one write, com.atproto.repo.applyWrites.
  • src/xrpc.ts: the HTTP calls behind them. Writes go through the OAuth session's fetchHandler; every other call carries no credentials.
  • src/servers.ts, src/watch.ts: the operator's servers and whether each enforces the records as they now read.
  • src/directory.ts: the DIDs the typeahead suggests, from those servers only.
  • src/dom.ts: every element is built here, with untrusted text as text nodes.
  • src/routes.ts: every address the page answers, and the flows. Each is one index.html: infra/policy-site/viewer-request.js rewrites the page's own addresses to it, and serving.mjs runs that same function in the dev and preview servers.
  • src/batch.ts: the write batch, every create, update and delete lined up across both collections, kept in sessionStorage for the session and written as one applyWrites guarded by the commit the records were read at. src/flow.ts keeps the operator's place in a flow beside it.
  • src/store.ts: the signed-in state: the records, that commit, the servers, and the batch laid over them. Its deploy() is the page's only write.
  • src/views/: one module per view, and wizard.ts for every flow's steps. main.ts routes between them.
  • src/ui/policy-syntax.ts: what a policy's JSON means to its engine, for colouring it: kinds, regular expressions, DIDs, ⟦…⟧ placeholders. Which fields are which comes from the engine's own grammar, which crates/didbot-policy-check exports. src/ui/regex-parts.ts colours regex syntax.
  • src/ui/cedar-tokens.ts: Cedar text cut into coloured parts and into statements, for the editor and the snippets alike. src/ui/cedar-complete.ts completes Cedar from the schema the server validates it against, which crates/didbot-policy-check exports.
  • src/ui/snippet.ts: read-only JSON, highlighted by walking the parsed value, and Cedar text, highlighted as written. Its JSON text is exactly JSON.stringify(value, null, 2).
  • src/ui/policy-review.ts: a policy at a glance, as My policies and the recommended pages show it: the record's fields, and the document's body highlighted.
  • src/ui/policy-editor.ts: the policy editor. The record's own fields are form fields, and the engine select sets the document's $type. src/ui/policy-document.ts turns a record into those fields and back, losing nothing.
  • src/ui/document-editor.ts: the document's text editor, CodeMirror, in the engine's language: JSON under its grammar, or Cedar. It completes what the engine allows and shows the checks' problems in place. It runs in a shadow root, because CodeMirror styles a document with a <style> element, which the CSP refuses, and a shadow root with a constructable stylesheet. It and CodeMirror load the first time an editor opens.
  • src/style.css: the palette, as custom properties at the top.
  • src/lattice.ts: the background. It is the landing page's Ising lattice from crates/didbot-site-anim, stopped just before the did.bot field switches on, then swept a few times a second. It starts after the checks settle and the page is idle, pauses in a hidden tab, and stays one frame under prefers-reduced-motion.

Against a real server #

scripts/test-policy-e2e.sh starts a didbot-pds to be governed and a stand-in for the operator's own PDS, serves this build beside them, then runs tests/e2e-policy.mjs over it in a Chrome the machine already has — puppeteer-core, which downloads none. With no Chrome the test says which paths it looked in and skips, and the Rust half deploys instead.

The test signs in on the page as the operator, over TLS, and deploys a recommended policy through review, bind and confirm. crates/didbot-swarm/tests/policy_rollout.rs then watches the governed server enforce it. Last, the page's own wasm computes a digest over the two records, and it must equal the one the governed server publishes as policy.enforced. Two surfaces agreeing, rather than one being trusted.

Local sign-in #

Served from 127.0.0.1, the page signs in as the atproto loopback client, with the same scope, redirected back to itself. localhost redirects to 127.0.0.1 first.

A sign-in is redirected back to / whatever address it started from. The path to go on to travels in the OAuth library's own state, which it keeps in its store and hands back on return; src/routes.ts's returnPath follows it only to one of the page's own addresses.