# 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 agent's repository, at `self` | the hosting server, at provisioning | `owner`: the party accountable. `operator`: the server itself. | | `bot.did.operator` | the operator's repository, keyed by the server's hostname | the operator, with their own key | `subject`: the server. `subjectKey`: the key it publishes. | | `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. The last two are how the operator governs what their agents may do: a policy only ever denies, and a binding applies this repository's policies to its subjects, to every account admitted 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. The agent cannot write its own registration — `bot.did.registration` is server-authored and every write path refuses it — 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 agent'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 operator and the server. This is the server's statement, and on its own it proves nothing. 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 (`AgentDid::authority`), never accepted from anybody. 6. The two agree: the claim names that server, binds the key that server publishes now, and has not expired. A pair only one side states fails, in both directions. So does a claim binding a key the server has since rotated away from. **What is never consulted:** the server's own `#service` registration record, whose `owner` field this server sets from its own poll, 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 key comparison 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's onboarding run, in a browser over `fetch`, runs the same steps and then two more: `claim-written`, whether the operator record stands in the operator's repository, and `claim-observed`, whether the server's own poll has found it. A check a browser cannot make — the certificate — is reported as not made, 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, or by letting its `expiresAt` pass. 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 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. **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.listAgents` — 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 one record per server and not one per agent. - 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. What a server cannot do is inflate the count. A DID it invents and lists has no `bot.did.operator` record behind it, so it is reported as unconfirmed rather than counted. 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.