Live PDS provisioning: creating a mailbox with a new Atmosphere account never creates the account

open
#a6c4223 opened by agent Oct 8

Observed (mx99 staging, 2026-10-08)

Adding [email protected] through the UI with “create a new Atmosphere account” (mode new, handle test.at.test.kilimanjaro.io), not tied to an existing ATProto identity, created the Stalwart mailbox but no ATProto account: ZDS for the domain (did:web:test-kilimanjaro-io.pds1.eu.sovrn.at) lists one repo, the domain’s postmaster (did:plc:6ynxrsfjhhm62o72cgptdvmo), and nothing for test. POST /domains/{id}/users/new took 21 ms and logged no error. Tying mail accounts to ATProto accounts is the core of the product, so this blocks beta.

Cause

The live provisioner was never built: - router.go:182-194: every cfg.PDSMode (“fake”, “manual”, “live”, default) installs pdsprovisioner.Fake{} or Manual{}. The “live” case says “Real PDS wiring lands in Issue 4; fall back to the pending stub until then”. No bug was ever filed for that Issue 4; 31b625f (pending accounts: admin nudge + user activation/password) depends on it. - Fake/Manual.Provision returns DID “did:plc:pending:” with status pending (internal/pdsprovisioner/fake.go), which internal/appview/ui/users_new.go createUser stores as the account’s DID. Nothing ever replaces it. - Only the domain saga creates a real account (postmaster.at., internal/appview/provision.go, internal/postmaster), on the domain’s ZDS instance through the pdslifecycle path. The same machinery is the natural basis for user accounts.

Wanted

  • A live pdsprovisioner.Provisioner (selected by pds mode “live”, the default on cells) that, for ModeNew, creates
  • Decide the initial state with 31b625f: create the account with a random password and status pending until the user activates and sets their own, or active immediately; either way the DID is real from the start.
  • Idempotent and safe against partial failure: a mailbox without an account, or an account without a mailbox, must be retried or rolled back (the code already provisions only after validation, “so a rejected form never leaves an orphan”).
  • The service record (at.sovrn.mail.service, written on first login by authbroker) and the handle (needs *.at.<domain> DNS, bug 6844d4f) then make the account usable from Bluesky and the sovrn login.
  • Migration for existing pending accounts (placeholder DIDs): provision them when live mode turns on, or list them for the admin.
  • Tests: unit for the provisioner against a fake ZDS; extend checks.cell (or a new VM test with a ZDS instance) to create a user and assert a real did:plc and a repo on the instance.

Related

31b625f (pending accounts, activation/password), 6844d4f (crawl + *.at. handle DNS), 858fad1 (closed; accounts provisionable, mailbox side).

2 Comments

agent a263cb4 Oct 8

Reuse existing identities (user, 2026-10-08)

Testing creates postmaster.at. and .at. repeatedly. The provisioner must find and reuse an identity that already exists instead of minting another DID for the same handle, the UI must show it, and every partial failure must be recoverable.

What exists today

  • Domain saga (internal/appview/provision.go): reuses the postmaster only when createAccount on the SAME ZDS instance returns postmaster.ErrExists (retry after a crash), via CreateSession. Not covered: a postmaster DID already in plc.directory whose account isn’t on this instance (domain removed and re-added, instance wiped, cell rebuilt without restore, another cell). Today that mints a second did:plc claiming the same handle; the old one keeps the handle in its alsoKnownAs.
  • UI (internal/appview/ui/users_new.go hostedHandleTaken/checkHandle): a hosted handle is “taken” only if a placeholder account row exists or the handle resolves publicly. Public resolution needs *.at.<domain> DNS (6844d4f), so today an existing DID is invisible to it; and “taken” just blocks, it never offers to reuse.
  • plc.directory has no index by handle (only by DID), so existence can’t be asked of PLC directly. Sources of truth, in order: sovrnd’s own records (including in-flight intents, below); the domain’s ZDS instance (com.atproto.identity.resolveHandle / admin getAccountInfo for the handle); public handle resolution (DNS _atproto TXT, then https:///.well-known/atproto-did) -> DID -> plc.directory document to see which PDS it points at.

Wanted behaviour

  • postmaster.at.: always reuse when an identity exists. On this instance: session, as today. Its DID points at another sovrn PDS (another cell, an old instance): adopt it, i.e. a PLC operation (signed with sovrn’s rotation key, which must be in every DID sovrn creates) moving its service endpoint to this instance, plus a repo import/restore if the repo isn’t here. A foreign DID (rotation keys not ours): refuse with a clear message; never mint a duplicate.
  • .at.: the add-user form checks as you type and, if the handle already has an identity, says so with its DID and current PDS and offers “use this existing account” (link the mailbox to that DID; same adoption rules as postmaster if it lives on another sovrn PDS) or “choose another handle”. Linking a foreign DID is the existing “use my Atmosphere account” mode, not a hosted handle.

Failure handling (must be designed and tested step by step)

Write an intent before any external side effect: a provisioning row (domain, handle, email, idempotency key, state = intent) committed in sovrn.db first; every later step records its result on that row; a reconciler (startup + periodic) drives intents to done or rolled back. Cases: 1. PLC/ZDS succeeded, sovrnd crashed or failed before persisting the DID: the intent row exists; the reconciler resolves the handle on the instance, finds the account, records the DID, continues. Never create again. 2. createAccount timed out or errored ambiguously (the PLC op may or may not have been published): retry only after looking the handle up on the instance; if it exists, it’s ours (intent proves it): adopt. 3. DID persisted, Stalwart mailbox creation failed: retry the mailbox; don’t delete a live identity. If the admin abandons, deactivate (not delete) the account and record it, so the handle can be reused later. 4. Mailbox created, account row write failed: the reconciler finds both via the intent and links them. 5. Double submit / two admins / concurrent saga runs for the same handle: idempotency key + unique (domain, handle) in sovrn.db; the second attempt joins the first intent. 6. plc.directory or the ZDS instance unavailable: fail before creating the mailbox (or leave the intent pending and retry), never leave a mailbox pointing at a placeholder DID silently. 7. Handle claimed by a DID whose PDS is gone (instance lost without backups): adopt via PLC rotation key if ours; otherwise report. 8. Domain removed: what happens to its postmaster and user identities (deactivate, keep rotation authority) so re-adding the domain reuses them. 9. Placeholder DIDs (did:plc:pending:*) from before live mode: the reconciler treats each as an intent.

Tests

Fault injection at every step boundary (crash after PLC/ZDS, after DID persist, after mailbox) with a fake ZDS + fake PLC, asserting exactly one DID per handle and a consistent end state after reconcile; UI tests for the “exists, use it?” path; a VM test that re-adds a removed domain and gets the same postmaster DID.

agent ac69cc4 Oct 8

Scope note (2026-10-08): the domain-saga half of the identity-reuse comment above (postmaster.at. reuse across instances/cells, crash after PLC in the saga, domain removal and re-add) moved to 4e9d7ba. This issue keeps user accounts: the live provisioner, user-handle reuse in the add-user form, and the intent/reconciler design for account creation.