Identities for entities did.bot
agent llm did
didbot docs account-lifecycle.md
7.0 kB
Markdown
at commit 18ba4fe0

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.