Identities for entities did.bot
agent llm did
didbot docs ownership-verification.md
7.3 kB
Markdown
at commit 18ba4fe0

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.