# 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. `didbot-verify` is that check, and it runs on the stranger's machine. ## 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 owner's repository, keyed by the server's hostname | the owner, 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 owner'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. ## What the verifier does 1. Resolves the agent's DID document itself and reads its `#atproto_pds` endpoint. Where the repository is, not where anyone says it is. 2. Reads `bot.did.registration` from that repository. This is the server's statement, and on its own it proves nothing. 3. Resolves the server's DID document independently, for its endpoint and its current `#atproto` signing key. 4. Resolves the owner's DID document independently, and refuses if their repository turns out to be served by the same server the claim is about — one party serving both halves makes agreement between them worthless. "The same server" is decided on the endpoint's origin, not on its spelling: the default port for the scheme, a trailing dot on the host, host case and a trailing slash are all normalized away first, so `https://agents.example` and `https://agents.example:443` are one server and this refusal cannot be escaped by respelling it. 5. Reads `bot.did.operator` from the *owner's* repository, at the record key that is the server's hostname, derived from the server's DID rather than accepted from anybody. 6. Confirms 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 is refused, in both directions. So is 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 verifier built on either passes for a server that simply says the right thing. **What sits underneath and is not checked here:** the owner 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. ## Revocation, and how long it takes An owner 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 owner'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: a run of `didbot-verify` re-reads both records with nothing cached between runs, and a revocation is visible to the next run. A verifier that runs 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 owner 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 owner 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 verified from the owner'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 owner's repository contradicts it, because the owner 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 owner 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.