Skip to content

Recovery: the account outlives its last device - #109

Merged
lannbot merged 4 commits into
mainfrom
recovery/account-kits
Aug 25, 2026
Merged

Recovery: the account outlives its last device#109
lannbot merged 4 commits into
mainfrom
recovery/account-kits

Conversation

@lannbot

@lannbot lannbot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes the parked us-doc-through-the-bucket item (runtime/SYNC.md) and implements #11's recovery-path body. Design record: runtime/RECOVERY.md (rulings settled with the owner, including the file-kit amendment); round entry in NOTES.md.

The shape

Recovery is a device, not a resurrection. The kit ceremony mints a dormant member leaf through the ordinary enroll_device path — visible in the devices sheet, revocable like a lost phone — whose secrets exist only in a sealed bundle. Dormancy kills bundle staleness (the G5 self-rotation finding never fires for a leaf that never authors; the proven G4 tablet path with the tablet replaced by a blob). The K_p pickup answers the us-doc bootstrap chicken-and-egg: one pickup, sealed to prekeys that ride the bundle, bootstraps everything.

Two kit kinds, one mechanism: a bucket kit under a generated 10-word phrase (argon2id → HKDF → both the object name and the KEK; the exposure rule made structural) and a file kit under a user passphrase (the custody exception, warned loudly, provider-neutral). Single-use, consumed at restore — the K_p's deletion enforces it even for a file we cannot delete: a second restore is a 404, never an identity fork.

The tracks

  • T-A (engine): the recovery WIT surface, the throwaway-keyhive mint + enrollment + K_p grant, the derivation, provider-forked consume, KpPayload.devices = directory ∪ grantees, the empty-doc-id us-doc sentinel, a new just recover battery (8 acts).
  • T-B (worker): the us-doc joins the flush/pull cycle; two-stage restore ceremony (bind → engine-from-bundle → fan-out → checkpoint → consume); kit RPC; devstore rows 54–61.
  • T-C (solo + visor): both kit ceremonies, the restore door on the picker AND the first-run fork (the browser with no devices is the one that needs it), solo-recovery + solo-recovery-file e2e.

Three defects found by the round's own gates, each now owned by a regression test

  1. The solo page's nested-enqueue self-deadlock (a file-kit mint wedged the page-wide chain forever, masquerading as an engine bug).
  2. The consume that outlived its checkpoint: internal driver mutations bypass the client-call debounce, and the pull fan-out's self-filter keeps a device's own flushed clear permanently out of reach — every internal mutation now checkpoints explicitly (devstore row 61, negative control).
  3. The same stranding for kit create/revoke.

Known limitation recorded (follow-up issue on the way): a name-key rotation strands a bucket-only lagging sibling on the us-doc until wire contact — pre-existing SYNC.md territory that kit revocation now makes reachable.

Gates

just recover / pair / resume / check green; devstore 61 rows; e2e 27/27 (solo-recovery destroys the browser context and restores from phrase + re-entered credentials alone); invariants 9/9.

Automerge is armed.

lann added 3 commits August 24, 2026 22:38
runtime/RECOVERY.md is the round's design record: recovery is a DEVICE
— a dormant member leaf whose secrets exist only in a sealed bundle —
not a resurrection of a live device's identity. Dormancy is what kills
bundle staleness (the G5 self-rotation finding never fires for a leaf
that never authors), the K_p pickup answers the us-doc bootstrap
chicken-and-egg SYNC.md parked, and single-use-consumed-at-restore
makes the double-restore identity fork structurally impossible.

Engine surface: recovery-kit-create bucket/file arms (generated
10-word EFF-short phrase with rejection sampling vs user passphrase —
the exposure rule made structural: one slot per kit, the kind decides
which), recovery-restore bucket/file arms (config-parameterized fetch
before state exists; pickup-fork bootstrap verified, not assumed),
recovery-consume (idempotent, provider-forked; dropbox refuses by
name), recovery-kits / recovery-kit-revoke over a new additive us-doc
recovery map. KpPayload.devices becomes directory ∪ grantees — the
honest author set post-SYNC.md, without which a fresh account's K_p
would name no real author. An empty doc-id on bucket-flush/pull now
names the us-doc: the minimal unparking of us-through-the-bucket the
restore requires.

Gates: new 'just recover' battery (8 acts: bucket + file round trips
against MinIO with a post-kit revocation epoch crossed before restore,
wrong-phrase/wrong-passphrase/double-restore refusals asserted as
classes, consume verified by object set-difference and idempotent
retry); just pair, just resume, just check all green.
… bucket cycle

The us-doc joins the flush/pull fan-out via the empty doc-id sentinel
(flush: consume-retry, us-doc, pointer map; pull: us-doc first, then
re-read the map — 'no partitions' can no longer stand in for 'no
account'). Restore is a two-stage host ceremony (restorePrepare →
consent → restore) because AttachSpec is remembered for the worker's
life and a phrase must not rest there: bind with bindStore's own
fail-at-bind validation (factored, not copied), engine born from
recoveryRestore instead of stateResume/init, fan-out, first
checkpoint, then consume — which is never fatal: consumePending rides
SyncStatus and retries at the head of every flush cycle. Kit RPC
(create/list/revoke) drives the ceremony's step-6 flush fan-out
outside the store-hold. Secrets cross the port once and are nulled in
finally.

Devstore rows 54-60: create + phrase-absence scan, restore round trip
with post-kit content, single-use, both refusal classes, injected
consume-failure retried to success, the gdrive file kit end to end
against the fake drive (the gd consume arm's promised coverage), and
the us-doc riding the cycle. A new in-harness object store serves
reads the recorder by design refuses. Matrix 60/60; demo deno check
clean; invariants 9/9.

Two findings recorded: a harness account built on the init-time
partition is unreadable by every future device (rc-account-create now
runs the real ceremony — embedders take note), and a name-key rotation
strands a bucket-only lagging sibling on the us-doc until wire contact
(pre-existing, SYNC.md territory; kit revocation makes it reachable —
follow-up issue owed).
…he disaster, and the round's three pinned defects

The solo page grows both kit ceremonies (the phrase displayed once
behind an explicit confirm-dismiss with no timer; the file kind's
three warning sentences and confirm field, delivered as a blob-URL
download) hung off the storage sheet's bound view — the store-bound
precondition made structural. The restore door exists TWICE: on the
picker, and on the first-run fork — because a browser with no devices
never sees a picker, and that is exactly the browser a real recovery
happens on. Both walk one shared ceremony; the picker's ends at the
ordinary startApp claim, the fork's reloads (the visor has already
claimed by fork time). Kit lists render from the account's registry
with revoke; the rebind caveat sits beside the choice it warns about.

Three defects found by this track's gates, each now owned by a
regression test: the page's nested-enqueue self-deadlock (a file-kit
mint wedged the page-wide serialized chain forever — paintList now
takes its chain position explicitly, and the revoke flow had the same
bug unfired); the consume that outlived its checkpoint (internal
driver mutations bypass the client-call debounce, and the pull
fan-out's self-filter keeps a device's own flushed clear permanently
out of its author's reach — every successful consume, create and
revoke now checkpoints explicitly; devstore row 61, with a negative
control); and the restored device's sheet arguing with its own
announcement (now an e2e assertion: no kit, loudly, on the restored
device's own list). The native battery gained the file-kit registry
and own-view assertions the defects proved missing.

RECOVERY.md carries the settled amendments (the consume-checkpoint
discipline, the strand hazard); NOTES.md carries the round entry.

Gates: e2e 27/27 (solo-recovery 10 acts, solo-recovery-file 8 acts),
devstore 61 rows, just recover/pair/resume/check green, invariants
9/9, demo deno check clean.
@lannbot
lannbot enabled auto-merge August 25, 2026 05:24
…ows renumbered 57-64 behind the wave's 51-56; both scenario blocks kept in dependency order
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants