# Verifying who an agent belongs to A stranger meets an agent and wants one thing: is there a human answerable for this, and can that human withdraw it. Answering it takes two records written by two parties, checked against each other by somebody who trusts neither. Every check below is a read of a public record or a DID document, and the section after them says where each one runs. ## The records | Record | Lives in | Written by | Says | | --- | --- | --- | --- | | `bot.did.registration` | the account's repository, at `self` | the hosting server, at creation, once | `operator`: the human at the top of the tree. `lineage`: every hosted account between, root first, ending at the parent. `kind`: what it is. | | `bot.did.operator` | the operator's repository, keyed by the hostname of the account it names; a hosted account's repository, written by the server as that account when it creates beneath itself | the operator with their own key, or the server as the creator | `subject`: the account operated. `creates`: what may be created beneath it, read only from the operator's own repository. | | `bot.did.credential` | the account's repository, keyed by a TID | the hosting server, when the credential is admitted | `credential`: a key the account signs with, or an OpenID Connect identity a token must match. | | `bot.did.policy` | the operator's repository | the operator, with their own key | `actions`: which requests it is consulted on. `document`: what it refuses, in the engine its `$type` names. | | `bot.did.policyBinding` | the operator's repository | the operator, with their own key | `policies`: which, by AT-URI. `subjects`, `includes`, `excludes`: whom. | The first two are what the verifier checks: an account's registration names the human and the chain between, and the human's repository holds the record naming the first account of that chain. The operator record also carries what the server acts on for a hosted account: what it may create beneath itself. What logs in as it is its own `bot.did.credential` records, which the server writes and the account cannot; [who operates an account](ownership.md) is the model. The last two are how the operator governs what their accounts may do once they exist: a policy only ever denies, and a binding applies this repository's policies to its subjects, to every account created beneath them, or both, minus the excluded accounts and everything beneath those. The policies a binding names must currently be in the same repository as the binding; a reference into another repository is not supported yet. A server builds the whole set from one reading of both collections, and a set any record spoils is refused whole, so a server that cannot read or build its policies keeps the last set it built rather than widening. It publishes a digest of the records it enforces as `policy.enforced` on `bot.did.stats`, so whether it is obeying the operator is a string comparison against the same digest computed over the operator's repository. Neither party can write the other's. An account cannot write its own registration or the operator records in its repository — `bot.did.registration` and `bot.did.operator` are server-authored and every write path refuses them — and the server cannot write into the operator's repository, because it holds no credential there and never has. That asymmetry is the whole design: a compromised server can stop being claimed, and cannot claim itself. Every record in the operator's repository is the operator's own: nothing in this project writes there. A binding is how the operator speaks about an agent, and it is still the operator's record, not the agent's. ## The checks 1. The account's DID document, resolved directly, says where its repository is through its `#atproto_pds` endpoint. Where the repository is, not where anyone says it is. 2. `bot.did.registration`, read from that repository, names the human as `operator` and every hosted account between as `lineage`. This is the server's statement, and on its own it proves nothing. The human's repository is then read for the record naming the lineage's first entry, or the account itself when there is none; `didbot operate --check` also walks each hosted edge, reading the parent's `bot.did.operator/` record the same way. 3. The server's DID document, resolved directly, gives its endpoint and its current `#atproto` signing key. `com.atproto.server.describeServer` on that endpoint has to name the same DID the document does. 4. The operator's DID document, resolved directly, says where the operator's repository is. A repository served by the same server the claim is about proves nothing: one party is then serving both halves, and their agreement is not evidence. 5. `bot.did.operator`, read from the *operator's* repository at the record key that is the server's hostname. The key is derived from the server's own DID (`AccountDid::authority`), never accepted from anybody. 6. The two agree: the record's `subject` is the DID step 3's document resolved to. A pair only one side states fails, in both directions. **What is never consulted:** the server's own `bot.did.registration` record, whose `operator` field this server writes from the operator it was launched with, and any endpoint that would answer "who operates me". Both are a server's word about itself. A check built on either passes for a server that simply says the right thing. **What sits underneath and is not checked here:** the operator delegated the zone the server's DID resolves under, the server proved control of that zone through ACME DNS-01, and TLS proves the connection reaches the holder of the resulting certificate. The `describeServer` cross-check is a consistency check on top of that, not a substitute for it. ## Where the checks run The reads in steps 3 and 5 — the document, the `describeServer` cross-check, the signing key and the record key — are one crate, `didbot-claim-check`, so every caller asks the same questions and writes the same record: - `didbot operate --check `, on the operator's own machine, runs them over a real network: it resolves the hostname, opens HTTPS to it, reads its `did:web` document and cross-checks `describeServer`, then reports which of those a stranger can do. `didbot operate ` writes the operator record at the derived key once they pass. - The policy site, in a browser over `fetch`, reads the same document and `describeServer` and runs the same checks on them before it writes the operator record. What a browser cannot check — the certificate — it lists as not checked, never as passed. - The server itself reads step 5 on a timer, from the operator's repository at the same derived key, and pauses when the record is gone; see below. `bot.did.registration` is read the way any record is, with `com.atproto.repo.listRecords` against the agent's repository; [running locally](running-locally.md) shows the request. ## Revocation, and how long it takes An operator revokes by deleting their `bot.did.operator` record. There are two readers, with two different bounds. **The server itself** polls the operator's repository every `DEFAULT_POLL_INTERVAL` and pauses once `DEFAULT_GRACE_WINDOW` has elapsed with no claim found. The pause therefore lands no later than the grace window plus one interval after the record stops being readable — six hours and five minutes as this deployment ships. The lapse makes the server `unclaimed`, which refuses writes and creates. The pause refuses every token the server issued as well, on the account routes too, except to freeze or deactivate its own account. It ends none of them: once the record is back, every token works again. A service-auth token is the exception: the pause refuses every new one and every proxied call, but one already minted works at the service it names until its `exp`, at most an hour, or a minute for the ones the proxy mints. The same poll re-reads the operator's record for every account the operator created directly, and quarantines such an account and everything beneath it on the same schedule; a creation beneath it re-reads the record first, so a deleted record stops new accounts within a minute. An account beneath a hosted creator stops when the creator's record for it is deleted, which only the server does, when the account is erased. The grace window is not slack: a server cannot tell "the operator revoked" from "the operator's own PDS is down", so it waits before treating one as the other. A shorter window converts an outage into a stopped fleet; a longer one makes revocation decorative. Only a running server counts the window: one restarted during an outage boots `unclaimed`, and refuses writes and creates until the operator's PDS answers. **A third party** is not polling anything, so there is no window: each read of the two records starts from nothing, and a revocation is visible to the next one. A reader that looks hourly sees it within the hour. DNS is not the mechanism for any of this, and cannot be. A deleted agent's name stays cached for as long as its record's TTL, and a resolver that answers from cache is not evidence of anything. The honest limit is that a verdict is about the instant it was made, which is why every one carries the time it was checked. A verdict already read cannot be un-read. Nothing an operator does afterwards reaches a copy of it, and anyone acting on a week-old confirmation is acting on history. ## Enumeration is a lower bound "Which agents does this operator have" is *derived*, not authoritative. It comes from `bot.did.listAccounts` — one server's account of its own contents — and each DID in it is then checked against the operator's own record. So the count is a floor, and four things push it below the truth: - The server can omit an agent from its listing. Nothing in the operator's repository contradicts it, because the operator writes a record for each account they create themselves and none for what is created beneath those. - A deployment may close the listing. An empty answer then means nothing about how many agents exist. - An operator may run several servers. A survey asks one; there is no directory of the rest. - A listing is a moment, and agents are provisioned and deprovisioned. The count is a floor only for the accounts the operator created. A DID the server invents and lists as one of them has no `bot.did.operator` record in the operator's repository, so it is reported as unconfirmed rather than counted. Beneath those accounts the server writes each creator's `bot.did.operator` record itself ([attestation](attestation.md)), so an account it invents there arrives with the record that confirms it. Containment is the other half: a server cannot mint an agent outside the zones it has been delegated, so the agents it can fabricate are at least confined to names it already answers for.