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.
A lock is confined to a subtree. Every account is admitted under one parent
(AgentAccount::parent, see didbot_pds::admission), and a tag hung on a
principal is hung on everything live beneath it as Party::Parent — by the
parent, so it comes off below only when it comes off above. A parent's vouch
lapsing hangs quarantined the same way, and the confinement poll lifts it
when the vouch returns. No Actor is the parent, so nothing lifts an
inherited tag at the child.
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.