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.