Identities for entities did.bot
agent llm did
didbot docs architecture.md
35 kB
Markdown
at main

The shape of the system #

Four pictures of where things run and what talks to what. This page draws the intended shape. Each picture is a decision, and the decision lives in the epic under plan/ that owns it; plan/README.md is the register of what each epic has reached.

Every route declares which credential it requires, and crates/didbot-serve/src/auth.rs holds that table: public reads, an account's session or a DPoP-bound OAuth token on the com.atproto.repo.* write surface, and the operator's own sign-in on the operations dashboard. auth-types is the epic that owns it.

The trust model says what the system can and cannot prove.

Hosts, and what a compromise of each one reaches #

Four kinds of host. The one thing that is not a service is drawn as one, because the human is the only party touching both sides: they load the operations page from the agent server and stay on it, approving the sign-in in a popup against their own server. That is why the agent server needs no write scope on the operator's account in order to authenticate anybody.

Policy is pulled. There is one arrow between the two servers and it is a poll, because there is no per-repository subscription to a foreign server to have instead. A compromise inside any dashed boundary takes that boundary and stops there, and the rule the server would be breaking stays legible on a host it cannot write to.

The agent host at the bottom is one of several, and the daemon on an agent host draws what runs inside it: the socket the hooks reach, the process behind it, and how the host proves itself to the server.

The layout of the zone is a security property rather than a naming convention. An agent's hostname must sit under a zone this deployment serves, and off that zone's apex so a wildcard certificate cannot shadow the server's own name. A TLS wildcard covers exactly one label, so a name deeper than one needs a zone and a certificate of its own.

STATIC HOST policy dashboard no backend the human operator a person at a browser · not a service OPERATOR HOST operator pds policy records the authority only the operator's key writes here third-party app any client_id, anywhere dns zone pds.did.bot delegated · acme dns-01 for the wildcard cert PDS HOST · pds.did.bot accounts under *.pds.did.bot — one label below its own name a compromise here reaches keys, accounts and records — not policy HTTP SURFACE oauth server par · authorize · token xrpc + firehose what agents write to ops dashboard state · e-stop control DOMAIN identity + names did docs · handles attestation proofs against documents policy engine pull sync · evaluator floor STORAGE repository store + write-ahead log accounts · custodial keys · records · check, append, apply disk fsync par acme dns-01 writes policy operator's key opens approves the sign-in, in a popup loads the ops page and stays on it polls for policy no write scopes AGENT HOST · reaches this host's sessions hook · didbot-agentd reports contexts, creates, collects agents session and subagent did:web accounts AGENT HOST · one of several same again · one agent pds serves many a compromise here does not reach the others provision · collect · write

Provisioning, and the tree inside one process #

Three accounts, one process. Drawing the subagents inside the harness box is the honest picture: distinct DIDs separate attribution and policy, not custody, because nothing stops code in that process from reaching the parent's credential. The hook reports each context to the daemon on the host, which asks the server once per context.

On the right, the zone is drawn beside the phase that names the account, and nothing crosses to it: both of an account's names resolve through the zone's wildcard record the moment they are chosen, so provisioning makes no external call — the names an agent has works one account through the whole path. Record writes go to the XRPC surface and are not on this path.

AGENT HOST ONE HARNESS PROCESS — subagents share it session agent did:web:7f3a9c.pds.did.bot handle: basalt-otter.pds.did.bot subagent did:web:1d4e02.pds.did.bot slate-heron · reviewer subagent did:web:9b77af.pds.did.bot amber-vole · writer SubagentStart · narrowed scope set PreToolUse stamps the acting DID onto every tool call made in here hook · didbot-agentd the harness supplies identity AGENT PDS · PROVISIONING 1 · verify the creator its proof, or nothing below runs 2 · claim a name one atomic claim, never check-then-take 3 · the names resolve two hostnames, one wildcard record 4 · mint the keypair custodial, held here 5 · serve the documents did.json · atproto-did · the records 6 · issue the credential session-scoped, back to the host dns zone pds.did.bot *.pds.did.bot, held by terraform resolves SessionStart · SubagentStart

Signing in to a third-party app #

An agent signs in to somebody else's app without a browser and without ever holding a key. The app is an ordinary OAuth client: it binds a loopback listener and pushes an authorization request naming the agent's account. The agent server is the authorization server, so what that request becomes is ours — a decision record, saying which client asked, what it asked for, and what the operator's policy made of it.

The problem that shape creates is knowing which agent is answering. A cookie is only as good as a browser profile on a machine that runs agents, and handing the agent a key puts a credential inside a model's context. Neither is necessary, because the daemon on that host already holds one credential per context. It follows the records for the accounts it issued, hands each one to the context it belongs to, and carries the answer back presenting that account's own token. The agent holds nothing.

Answering a record rather than a URL is what makes the binding exact. The agent is shown what was asked and what policy would grant, and replies with a one-time token — not an account, not a page, not an address. Which account signs in is read off the record the daemon is holding that token in, so there is no identity field on the wire for a caller to fill in with another agent's, and the server checks the credential presented rather than a name in a body. A token relayed to another agent's daemon matches no record it holds.

The honest limit has moved but not gone. The daemon's socket is guarded by the directory, the file mode and the peer's uid, so anything running as this user can connect to it and approve what is waiting. That is user-level custody, which is real, and not per-agent custody, which no arrangement on a shared machine provides. What it no longer is, is a claim: there is nothing to assert and therefore nothing to assert falsely.

agent the model hook · didbot-oauth short-lived, one per event or command didbot-agentd holds the account tokens the app third party, on this host agent pds authorization server start the client, name its own handle PAR: client id, scopes, the agent's account a decision record, pending no browser is ever opened listPendingAuthorizations, held open who asked, what for, what policy did to it outbound only: nothing here is reachable report and what is waiting for this context the agent reads it didbot oauth approve <token> a token, not an account approve(token) the record names the account approveAuthorization, as that account is the token live and unused? does the credential authenticate as the account? what does the operator's policy allow? the granted scopes, and the client's redirect the code, fetched on loopback the code, for a token and the granted scopes

Why a stop is local #

A policy change is authored in a browser, lands in the operator's repository and waits for the next poll before it binds. That is correct for a rule and useless for an emergency, which is the argument for keeping an emergency stop in local state: it needs no network, no operator and no poll to wait for.

policy path · every hop can stall policy dashboard operator's repo next poll validate whole evaluator swaps stop path · local, immediate e-stop thrown next request refused no network, no operator, no poll to wait for