Multi-user ownership migration and recovery
September 11, 2026 ยท View on GitHub
This runbook freezes the rehearsal and activation contract for moving an existing Labby AccessStore installation (schema v1 through v6; v5 is the last released schema) to the multi-user ownership schema v7. The rehearsal proves that the input can be inventoried, backed up, reopened, restored, and deterministically classified before the ownership migration is allowed to exist.
Activation boundary
The point of no return is the first successful scoped write after multi-user enforcement is enabled. Before that boundary, rollback restores the complete pre-migration checkpoint. After it, rollback means forward repair or restoring the complete checkpoint and losing all later writes; an old binary must never open the new store and reinterpret scoped rows as globally owned.
Ordinary startup never crosses a schema boundary implicitly: every legacy
version (v1 through v6) is refused with MigrationApprovalRequired until the
operator sets LABBY_ACCESS_MIGRATION_EVIDENCE to the owner-controlled
approval document described in ENV.md. Bootstrap of a
never-initialized legacy store is gated the same way. Labby binds that
document to an independent checkpoint, the exact source/target schema pair, a
durable operation ID, and an explicit activation decision before opening an
exclusive migration transaction.
Checkpoint verification is logical, not byte-identical. The checkpoint file's
digest is streamed and must match the document; the live store is then
compared with the checkpoint by schema manifest plus every table's
primary-key-ordered content digest. A WAL-mode source whose committed frames
have not been checkpointed into its main file therefore still verifies
against its consolidated VACUUM INTO / backup-API checkpoint. A prepared
sidecar marker makes an interrupted attempt replay only with the same
evidence; after the migration transaction commits, the complete marker is
published, and a marker write failure is logged rather than failing the
already-durable open.
The migration is split into separately observable phases:
inventory: open v5 read-only, run quick/FK/schema/bootstrap validation, and produce the signed inventory report;checkpoint: stop writers, checkpoint WAL, create an SQLite-consistent backup, copy required sidecars/keys/configuration, and hash the restore set;expand: create only the next-version structures in one exclusive transaction;classify: attach exactly one typed owner to every durable resource;verify: compare counts, stable IDs, logical content digests, references, quarantine counts, and audit/outbox facts;reopen: close and reopen twice using the new binary, repeating integrity and inventory verification each time;shadow: compare old and new authorization decisions without enforcing;activate: persist the enforcement generation and emit its audit/outbox event in one transaction; andcontract: remove obsolete compatibility fields only in a later release.
Every phase has a durable operation ID and completion marker. Repeating a completed phase verifies its recorded input/output digests and returns the prior result. A changed replay fails closed.
Offline operator activation
labby state migrate-access is the installation-owner entry point. Stop the
Labby daemon before running it. The command acquires the same installation
lifecycle lock as the daemon and refuses to run while that lock is held.
It requires an existing, initialized AccessStore; it does not bootstrap an
owner or issue credentials.
First rehearse against an independent, SQLite-consistent copy of the source
store in an owner-only installation directory. Select that directory with
LABBY_HOME. Create the approval document described in
ENV.md, bound to a separate checkpoint of that rehearsal
store. Do not point rehearsal approval at the live installation. Run:
LABBY_HOME=/absolute/rehearsal-installation \
LABBY_ACCESS_MIGRATION_EVIDENCE=/absolute/rehearsal-approval.json \
labby --json state migrate-access
The command applies the existing approval and logical checkpoint checks,
migrates through the existing transaction, then closes and opens the store
twice. Its result reports schema_version and verified_reopens. Repeating
the command on a valid current-schema store verifies the reopens without a
new schema migration. An error after the migration transaction commits does
not imply rollback; preserve the checkpoint and inspect the store before
retrying.
After reviewing the rehearsal evidence, prepare a separate approval bound to
the quiesced live source and its independent checkpoint. With the daemon
still stopped, run the same command using the live installation's LABBY_HOME
and its approval document. Keep the checkpoint and approval until the
post-migration authorization and inventory checks in this runbook pass.
Ordinary startup continues to refuse legacy stores; setting the evidence
variable alone does not activate migration through the daemon.
Production-shaped v5 inventory
The rehearsal fixture is non-empty and includes:
- the canonical bootstrap Organization, Principal, identity link, default Project, owner membership, and audit record when bootstrap generation is one;
- multiple active and inactive Principals with unique verified identity links;
- multiple Projects, all four Project roles, optional Loadout mappings, and audit rows;
- current credential/proof, tombstone, policy-publication, admission, and security tables, including empty-table counts where emptiness is meaningful;
- WAL mode with committed rows still represented through the WAL checkpoint path; and
- IDs and text containing realistic maximum-safe lengths and Unicode.
The report records the exact v5 application ID, user version, schema
fingerprint, bootstrap generation, global revision, table/index manifest
digest, table row counts, primary-key set digest per table, logical content
digest per table, quick_check, foreign_key_check, and source file/sidecar
digests. File-byte equality is not expected after a valid SQLite migration;
logical digests are canonical encodings ordered by primary key.
scripts/ci/validate-multi-user-migration-rehearsal.py is an
operator-run tool: it generates and verifies a provenance-bound rehearsal
manifest for stores the operator supplies, and CI runs only its unit tests
against synthetic databases. It does not itself execute a migration. The
executable migration evidence CI does run is the Rust
access::migrations test module in full: production-shaped v4 and v5 fixtures
are migrated, reopened twice, and restored from their checkpoints; every
legacy version is proven to refuse without evidence; and the approval gate is
exercised end to end on a WAL-diverged v5 file, including the checkpoint
mismatch refusal.
Ownership classification
The only automatic owner seed is the canonical verified bootstrap Principal.
Existing private user material becomes that Principal's Personal scope. A
migrated store that was never bootstrapped receives no platform administrator,
no Team, and no Team-Project assignment; pre-existing direct Project
memberships survive unchanged and receive authority epoch 1.
For a store with bootstrap generation one, the existing migration grants
PlatformAdministrator authority to the canonical bootstrap Principal, creates
one Initial Team, and records that same Principal as its owner. It adds two
audit rows for these authority seeds. It preserves the original Organization,
Principal, Project, identity-link, and direct-membership IDs; it does not
replace the owner, issue credentials, or assign existing Projects to the new
Team. Bootstrap generation remains unchanged.
Installation configuration, host filesystem operations, raw logs, recovery, and provider credentials become Installation-owned. Email, display name, namespace, directory name, creator string, and OAuth scope are never used to infer a Team or PlatformAdmin.
Rows whose owner cannot be proven, whose identifiers collide after canonical normalization, or whose references disagree are copied without mutation to a PlatformAdmin-only quarantine ledger. The ledger records source store/table, stable source key, safe digest, reason code, discovery phase, and resolution state. It does not contain secret material. Quarantine is never included in ordinary listing or compatibility-owner fallback.
Activation requires:
- every source row accounted for as classified or quarantined;
- zero duplicate target owner rows;
- zero dangling references or unclassified usable resources;
- unchanged stable IDs and logical content digests for non-policy payloads;
- exactly one PlatformAdministrator derived from the canonical bootstrap Principal; and
- independently reproducible before/after reports.
Failure injection
Before production activation, a separate executable rehearsal must interrupt
every phase and inject the following failures. This proof is not provided by
validate-multi-user-migration-rehearsal.py:
- busy/locked, read-only, disk-full, I/O, corrupt/not-a-database, and truncated SQLite inputs;
- invalid application ID, user version, schema fingerprint/manifest, bootstrap shape, and foreign keys;
- process termination with non-empty WAL/SHM;
- duplicate IDs, canonical-name collisions, dangling owner candidates, and invalid UTF-8 at external inventory boundaries;
- missing/truncated backup members and mismatched restore-set generations; and
- failure to append audit/outbox/quarantine records.
Before activation, each failure leaves the source v5 store byte/logically recoverable and the enforcement generation unchanged. Transactional failures must leave the schema version and logical inventory unchanged. After restart, the reconciler either resumes the exact operation or rejects changed inputs; it does not skip ahead.
Backup and restore
The operator blocks writers and records a maintenance lease before backup.
SQLite backup uses its online backup API or VACUUM INTO from a validated
connection after an explicit WAL checkpoint. Copying access.db alone while a
WAL may contain committed state is invalid.
The restore set contains:
- AccessStore database and required WAL/SHM state or a verified consolidated SQLite backup;
- schema and capability registry generations;
- signing private/public keys and active/overlap key generations;
- authority outbox head, Depot acknowledged watermark, and snapshot digest;
- bootstrap and enforcement generations; and
- configuration that selects standalone/managed authority mode.
Restore occurs into a new owner-only directory. The operator verifies every manifest digest before atomically selecting it, starts without traffic, opens and validates twice, verifies counts/digests/quarantine, and only then removes the maintenance lease. A partial or cross-generation restore enters recovery-required mode.
Old-binary proof
The checkpoint retains an exact v5 database that the current v5 reader opens
and validates after restore. The future ownership database advertises a newer
user version and fingerprint; v5 must return UnsupportedSchema without
mutation. This is the rollback proof: restore the v5 checkpoint first, then run
the old binary. Never point the old binary at a post-activation database.
Evidence retained
Release evidence includes source and target binary commits/digests, fixture seed, phase operation IDs, start/end times, pre/post reports, checkpoint and restore-set digests, injected-failure matrix, reopen results, quarantine inventory, shadow-decision differences, and the activation audit/outbox event. Secrets and raw identity assertions are excluded.