Identities for entities did.bot
agent llm did
didbot docs account-lifecycle.md
6.6 kB

The account lifecycle #

didbot_pds::AccountState is whether an account's data exists, and whether it is being created or destroyed right now. It answers nothing about whether the account serves: that is a set of locks, didbot_pds::Locks, hung on the account by named parties, and the account serves when the hasp is empty. didbot_pds::ServerState (see server-lifecycle.md) is the deployment's own standing, which is a third thing.

Everything below the marker is generated from the types by didbot_pds::account_lifecycle::generated_docs and checked against them by the_committed_diagram_matches_the_types. Regenerate it with:

UPDATE_DOCS=1 cargo test -p didbot-pds account_lifecycle
stateDiagram-v2
    direction LR
    [*] --> reserved
    reserved --> provisioning: hostname published
    provisioning --> active: records landed, announced
    active --> decommissioning: erase
    decommissioning --> decommissioned: erased
    decommissioned --> [*]: hard delete
state serves the document serves the verification method serves the repository accepts external writes on #account
reserved no no no no nothing announced
provisioning yes yes no no nothing announced
active yes yes yes yes active: true
decommissioning yes yes no no active: false, status: deleted
decommissioned yes yes no no active: false, status: deleted
lock removes reads removes writes on #account while active
frozen no yes active: true
quarantined no yes active: true
suspended yes yes active: false, status: suspended
deactivated yes yes active: false, status: deactivated
hold refuses
prevent-data-deletion active --> decommissioning, whoever asks

The state is data existence, and it moves one way #

Data can always be served again, and can never be un-erased. That is why the axis is a state machine with no edge backwards, and why a reason to stop serving is never a state: frozen as a state would have to be left again, and the state that answers "does the data exist" cannot be the state that answers "who says it may not serve".

An -ing state is work that is running, so each one names what advances it and what finishes it when nothing else does. Registry::provision moves an account from reserved through provisioning to active in one call; Registry::delete moves it from active through decommissioning to decommissioned. A process that dies between two durable steps leaves the -ing state behind, and Provisioner::sweep_stale is what acts on it: a reserved or provisioning row is reaped, and a decommissioning one is resumed and finished. tests/erasure.rs kills the process at each step.

reserved is the first durable step of provisioning and comes before the hostname is published, so the zone never holds a record no row names — tests/route53_seam.rs proves it against a Route53 double.

decommissioned keeps the DNS record, the zone, the certificate and did.json: a did:web identity cannot leave the domain that hosts it without breaking every signature it ever made. The row leaves on a hard delete, which is Registry::hard_delete with the DID confirmed by an operator, or the retention sweep once the account's retention has run out.

Locks are lockout/tagout #

Any party may hang a lock; each tag is lifted only by the party that hung it; the account serves when nothing hangs. Two parties hanging the same kind of lock are two tags, so lifting one never lifts the other. The parties are didbot_pds::Party, and Registry::lock/unlock take an Actor that names which one is acting and, for an operator, who.

One recorded exception: an operator lifts a policy's quarantined, and the ledger says which operator overrode it. plan/policy.md's rule that an evaluator can never unfreeze holds — the policy's tag comes off by an operator's hand, never by a later verdict.

Subtraction always wins. A lock that removes reads removes writes too, and AgentAccount::policy() — the one call an enforcement point makes — is the state's baseline with every tag subtracted. No lock touches the document.

Over HTTP the account's own credential is the only one this server authenticates, so bot.did.freezeAgent/unfreezeAgent and bot.did.deactivateAgent/activateAgent hang and lift the account's own tags. An operator's tags, the policy's tag, and every hold are in-process calls on Registry, the way Registry::hard_delete is.

Self-service erasure respects every lock. Registry::delete is refused while anything hangs: an account that is frozen, quarantined or suspended does not get to destroy what the lock was hung to keep. An operator's Registry::hard_delete ignores the locks.

A hold refuses a destructive edge #

PreventDataDeletion is set and cleared by an operator, both recorded in the account's ledger, and every eraser — the account's own, an operator's confirmed one, the retention sweep — is refused while it is set. The store refuses the row's removal again on its own, so a caller that skipped the provisioner's check still cannot get past it. It adjusts nothing about service: stopping an account is a lock.

A pin is not a hold. It is Retention::Forever, a value on the retention axis, and it keeps the row — the sweep passes a pinned account by and a hard delete is refused — while the account's own erasure goes through it.

What a relay sees #

#account on com.atproto.sync.subscribeRepos is this host's answer to can you fetch this repository from me, and if not, why. active is fetchability; status is the reason when it is false. The generated tables above are the mapping, computed by AccountState::sync_status over the state and the locks, and the same function decides whether a lock change is announced at all: a tag that changes the answer is, one that does not is not.

The write axis has no wire representation, so a consumer of #account alone cannot learn that an account is frozen or quarantined; bot.did.listAgents carries every tag with the party that hung it. takendown, desynchronized and throttled are values this deployment has no position for. See conformance.md.