Identities for entities did.bot
agent llm did
didbot docs account-lifecycle.md
7.2 kB
Markdown
at main

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: row and key stored
    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: the row exists and the name is held, and nothing serves. The hostname already resolves, through the zone's wildcard record.

decommissioned keeps the name, 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.

An account of any kind, in any state from active on, keeps its row however old it is, until the operator deletes it with POST /dashboard/api/accounts/{did}/delete or didbot account delete, which calls that route. That is Registry::hard_delete with the DID confirmed, and it erases the data first when the data is still there. The stale sweep takes only a reserved or provisioning row, once it is a week old.

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 created beneath one operator (see who operates an account), 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. An operator record lapsing hangs quarantined the same way, and the confinement poll lifts it when the record 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 HostedAccount::policy() — the one call an enforcement point makes — is the state's baseline with every tag subtracted. No lock touches the document.

Over XRPC the account's own credential is the only one this server authenticates, so bot.did.freezeAccount/unfreezeAccount and bot.did.deactivateAccount/activateAccount hang and lift the account's own tags. The operator acts on one account through the dashboard's /dashboard/api/accounts/{did}/ routes, which didbot account calls. The policy's tag and every hold are in-process calls on Registry.

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::operator_erase and Registry::hard_delete ignore 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, the hard delete — is refused while it is set. The stale sweep passes a held row by. 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.

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.listAccounts carries every tag with the party that hung it. takendown, desynchronized and throttled are values this deployment has no position for. See conformance.md.