# Attestation Attestation is the check `bot.did.createAccount` makes before it mints an account: the proof the caller presents is verified against the document of the account it claims to be, at the moment of creation. It runs once. What it establishes is written as the two records of one edge — `bot.did.operator/` in the creator's repository and `bot.did.registration/self` in the new account's — and those records, not the proof, are what anyone reads afterwards. [Who operates an account](ownership.md) is the model; this page is the keys each party holds, the check, what the credentials last, and what a lapse does. `crates/didbot-agentd` signs the proofs a host and a registering machine present, `crates/didbot-operator` mints the human's, `crates/didbot-pds` holds the accounts and the policy tree, and `crates/didbot-serve` serves the routes and runs the polls. [The daemon on an agent host](agentd.md) draws the process that signs; [the trust model](trust-model.md) says what a signature is worth. ## The parties, and the keys each one holds | party | key | where it lives | who writes it | | --- | --- | --- | --- | | the human | their own atproto account's | their own machine and their own PDS | the human | | the server | one repository signing key per account it hosts, and its own | the deployment's store | the server | | a host | `#key-1`, secp256k1 | one file on the host, `0600` in a `0700` directory, under a `flock` for the daemon's life | `didbot register host`, once | | a service or pipeline | an OIDC identity, or `#key-1` | the issuer's, or `keys/.key` under `$DIDBOT_STATE` | the issuer, or `didbot register` | | an agent context | none: a session | the daemon's memory | the server issues it | **The human** is a DID with a repository this deployment holds no credential for. `didbot operate` writes their `bot.did.operator` records there — one keyed by the server's hostname, and one per account they create — and proves each create to the server with the operator session the server minted when they signed in to it. **The server** signs every repository it hosts. A hosted account's operator records are written by the server as that account, so every edge below the human is the server's word. **A host** holds its key itself. `didbot register host ` mints it as the daemon's own, in `$XDG_STATE_HOME/didbot/agentd` or wherever `DIDBOT_STATE` names (`crates/didbot-agentd/src/node.rs`), and the server publishes the public half as `#key-1` in the host's document. The private half crosses neither the daemon's socket nor the network; what leaves the machine is a public key and signatures. It is a file, and a copy of it is the host. **A service or pipeline** is an OpenID Connect identity when the human gives `--oidc`: an ID token from that issuer carrying those claims is the account. A machine that runs `didbot register service` or `didbot register pipeline` holds a key under `keys/.key` instead. **An agent context** holds nothing. The daemon creates one account per context and keeps its session in memory. ## The check `bot.did.createAccount` reads the parent off `Authorization: Bearer` and verifies it one way per shape; [who operates an account](ownership.md) tabulates the four. The JWT shape, which `didbot-agentd` signs (`crates/didbot-agentd/src/jwt.rs`), is `ES256K` over `{iss, aud, lxm, exp, iat, jti}`: `iss` the account, `aud` the server's DID, `lxm` the one method the token is for, `exp` five minutes out, `jti` sixteen random bytes the server spends once. `bot.did.createSession` takes the same token with `lxm` `bot.did.createSession` and answers a session. Four gates close the route before the proof is read: `Halted` (the e-stop, which also fires when the server's own operator record has lapsed), `ServerNotReady` (the lifecycle — see [the server lifecycle](server-lifecycle.md)), `RateLimitExceeded` and `AccountCapReached`. The first three close `bot.did.createSession` too, and its calls spend the same per-address budget as `createAccount`'s: reading its proof can also fetch an issuer's keys or a DID document. A proof that fails is refused with the reason in the message: the wrong audience, an expired or spent token, a signature no key in the document verifies, a claim the creator's OIDC identity names with another value. A create no allowance in the operator's repository covers is refused as `NotAllowedToCreate`. A parked key is a public key and a fingerprint held at the server for minutes. `bot.did.parkKey` is rate-limited per address, lists nothing and mints nothing until `didbot operate` from the human's side names it, and a record older than the park does not admit it. Parks are held per key, so several may wait under one name and the operator admits one by fingerprint. ## What the credentials last **The account's session.** `DEFAULT_AGENT_TOKEN_TTL` is 365 days from issue (`crates/didbot-pds/src/credential.rs`). One live token per account; issuing another revokes the first; the server keeps a SHA-256 digest and sends the bytes once. Nothing extends it and nothing shortens it: using it daily and leaving it idle for a year come to the same expiry, after which every route that takes it answers `ExpiredToken`. The daemon holds each context's in memory for as long as the context lasts; `didbot register` and `didbot operate` keep one under `$DIDBOT_STATE/accounts/.token`. An account with a key or an OIDC identity mints a fresh one through `bot.did.createSession` whenever it needs one. **OAuth.** `ACCESS_TTL` is 5 minutes and `REFRESH_TTL` is 14 days (`crates/didbot-serve/src/oauth/token.rs`). Every refresh rotates both tokens and sets the refresh expiry to fourteen days from that refresh (`rotate` in `crates/didbot-pds/src/oauth.rs`), so an app that refreshes at least fortnightly keeps one login indefinitely. Presenting a refresh token that has been rotated away, or one whose fourteen days have passed, ends the whole family. The app then signs in again, which makes a fresh decision record for the agent to answer, and the daemon approves it with that account's own session. Grants are durable across a restart for a `--data` run; the scope ceiling is asked again at every refresh and every write, so a login never outlives what current policy allows. Erasing an account ends every credential it has at once — its session, its OAuth grants and its unredeemed authorization codes. A lock ends none of them; it refuses the write. ## Withdrawals and losses A lock hung on an account is hung on everything live beneath it as `Party::Parent`, and comes off below only when it comes off above — see [the account lifecycle](account-lifecycle.md). An account under any lock mints nothing new beneath itself, and keeps its key. | situation | what happens | who recovers it | | --- | --- | --- | | a lock on a host | creates beneath it are refused; the contexts beneath it keep their sessions and have their writes refused | the party that hung the tag lifts it | | the human deletes an operator record | at the next poll after the grace window the edge lapses, the account and its subtree are quarantined, and every create beneath them is refused | the human writes the record again; the next pass releases the subtree | | the human's repository is unreachable | every edge stays as it is; the server's own record has `DEFAULT_GRACE_WINDOW`, six hours plus one poll interval, before the server pauses itself and every route here answers `Halted` | the human's PDS comes back | | a host's key is lost | the account keeps the key its document names and nothing can sign for it; its contexts keep the sessions they hold | `didbot register host` under a new name and `didbot operate` for it; the human deletes the old host's record to stop what it created | | the server is down longer than a refresh window | proofs are unaffected, each made fresh for one call; sessions are unaffected; every OAuth refresh token last rotated more than fourteen days ago ends its family at the next attempt | apps sign in again | | the server restarts | it comes back `unclaimed`: record writes, blob uploads, creates and `bot.did.createSession` answer `ServerNotReady` until the first poll finds the human's record for the server | the poll, on its own | The confinement poll (`crates/didbot-serve/src/confinement_poll.rs`) re-derives every inherited tag from the tree on the same five-minute clock the operator poll runs on, so a lapsed edge and a lapsed top edge are noticed at the same rate. ## The `.localhost` development exception Under `localhost` and any name beneath `.localhost`, `bot.did.createAccount` mints for a request carrying no proof; a request that does carry a proof is checked exactly as it is anywhere else. The server starts `claimed`, and no operator poll runs while `--operator` is the placeholder, which names no repository. Naming an operator runs the poll against that operator's repository, as anywhere else. [Running locally](running-locally.md) is that stack.