Purpose: define the profile lifecycle, on-disk/at-rest forms, and the sync protocol. This is
the model proven by Vision (<uuid>.zip + <uuid>.sig, partial local materialization) and
formalized here (ADR-0004).
1. Two forms of a profile
- Runtime form: a live
user-data-dirChromium can use (cookies, localStorage, IndexedDB…). - Storage/sync form: a Profile Blob =
manifest + payload:- payload =
user-data-dir→ deterministic archive (tar/zip) → compressed → encrypted (AEAD). <uuid>.blob(ciphertext) +<uuid>.sig(signature over ciphertext+manifest).- manifest (cleartext, integrity-protected): uuid, version vector, size, core version last used, fingerprint id, createdAt, deviceId, contentHash.
- payload =
Bridges between the two: materialize (blob → dir) and pack (dir → blob). They are the only sanctioned conversions.
2. Lifecycle state machine
create / import
│
┌─────▼──────┐ pull(blob) ┌──────────────┐ materialize ┌───────────────┐
│ CLOUD_ONLY │────────────────►│ LOCAL_CACHED │────────────►│ MATERIALIZED │
└─────▲──────┘ └──────▲───────┘ └──────┬────────┘
│ evict │ pack │ start
│ │ ▼
┌───────┴────────┐ upload ┌──────┴───────┐ stop ┌──────────────┐
│ SYNCING_UPLOAD │◄───────────────│ DIRTY │◄────────────│ RUNNING │
└────────────────┘ └──────────────┘ └──────────────┘
remote newer │ ▲
▼ │ resolve
┌────────────────┐ ┌──────┴──────┐ lock held elsewhere
│ SYNCING_DOWNLOAD│ │ CONFLICT │◄────── LOCKED_REMOTE
└────────────────┘ └─────────────┘
State definitions:
CLOUD_ONLY— only the blob exists in cloud; nothing local.LOCAL_CACHED— blob present locally, not unpacked.MATERIALIZED— live dir exists, not running.RUNNING— a core is using the dir (exclusive; no sync may touch it).DIRTY— stopped with local changes not yet uploaded.SYNCING_UPLOAD/DOWNLOAD— transfer in progress.CONFLICT— local and remote diverged (both changed since common base).LOCKED_REMOTE— another device holds the run/edit lock.
3. Encryption & signing (zero-knowledge, multi-KEK — ADR-0008)
- Envelope hierarchy: each profile payload is AEAD-encrypted with a random per-profile Data Key (DEK). The DEK is wrapped independently by multiple KEKs — any one unwraps it. Server stores only ciphertext + the wrapped-DEK copies → server cannot decrypt on any path.
- KEK factors: password (Argon2id) + a mandatory Recovery Key (offline emergency kit); optional passkey (WebAuthn PRF) and OS keychain; team profiles add an org-key KEK (public-key wrap) for admin-assisted recovery — still zero-knowledge to the server.
- AEAD (AES-256-GCM or XChaCha20-Poly1305) for payload; signature (Ed25519) over manifest+ciphertext
for tamper/anti-corruption detection (the
.sig). - Recovery: lose one factor → recover via another. Lose all factors → lose the profiles (honest E2EE floor). No server-side escrow. Details: KEY_RECOVERY_COMPARISON.md, SECURITY_POLICY §4.
4. Sync protocol
- Lock:
sync.lock.acquire(id)before upload/edit. Locks are leased (TTL) + owner-stamped. - Version vectors: each blob carries
{deviceId: counter}; compare to detect fast-forward vs divergence. - Upload: pack → encrypt → sign → multipart upload to object storage → commit manifest in metadata DB.
- Download: fetch blob → verify sig → decrypt → materialize on demand.
- Conflict: if remote advanced beyond local base while local is DIRTY →
CONFLICT. Default resolution: last-writer-wins guarded by lock; CONFLICT keeps both blobs (a/b) for manual pick. - Eviction: LOCAL_CACHED/MATERIALIZED can be evicted to CLOUD_ONLY under disk pressure (LRU), never if DIRTY/RUNNING.
5. Local-first guarantee
With no cloud account, everything works: profiles live as local blobs + dirs, lifecycle minus the SYNCING/LOCKED_REMOTE states. Cloud is an additive layer, never a dependency (ARCHITECTURE.md §1.3).
6. Invariants (enforced by Profile Lifecycle, never bypassed)
- A
RUNNINGprofile is never packed/uploaded/evicted. - Sync never edits a live
user-data-dir(DO_NOT_DO.md). - Materialize/pack are the only dir↔blob conversions.
- Every state transition emits a
profile.state_changedevent + (for dangerous ones) an audit entry. - Materialized = the only plaintext window. While
MATERIALIZED/RUNNINGtheuser-data-diris plaintext on disk (Chromium needs it). Onpack/evict the dir is securely wiped; the window is kept minimal. (Local-compromise residual risk: THREAT_MODEL §4.) materializenever hands a raw path to remote clients. The path goes only to the local UI/owner context; CLI/MCP/script clients get an opaque handle, and any raw-path return is power mode + audited — a path would otherwise bypass the single control boundary.
Open questions
Archive determinism across OSes, partial/delta sync vs full-blob, lock TTL + steal policy, conflict UX, key recovery options (social/escrow?) → OPEN_QUESTIONS.md.