Watch
1
0
Fork
You've already forked SouveraineOS
0

tasks: add TASK-16 culver messenger+contacts; re-point TASK-01 to culver-contacts store

This commit is contained in:
Fimeg 2026-07-21 13:01:59 -04:00
commit 2d95f3743a
2 changed files with 153 additions and 9 deletions

View file

@ -1,9 +1,12 @@
# TASK 01 — Favorites: wife's name on her number
**Status:** blocked on a contacts-store decision. qtpim retired
2026-07-20 (see archive/06) — the store needs a non-Qt-PIM design
(small owned store the Person layer reads directly) before this lands.
**Size:** small, once the contacts store exists.
**Status:** blocked on TASK-16. The contacts-store decision resolved
2026-07-21: the "small owned store the Person layer reads directly" is
**culver-contacts** (`~/Projects/culver`, `crates/contacts`) — qtpim
retired 2026-07-20 (archive/06). This lands once TASK-16.B (store keyed
to the machine identity) and 16.C (contact card UI) are done; the insert
below then writes through the gated store instead of a qtpim engine.
**Size:** small, once TASK-16.B/C exist.
## Goal
@ -13,11 +16,11 @@ motivator for the whole contacts/Person layer.
## Prerequisites
- TASK-06 done: `qtpim` aarch64 built and installed on the phone, the
sqlite contacts engine live (`~/Projects/qtcontacts-souveraine`, path 3
per `~/Projects/SouveraineOS/docs/contacts-design.md`).
- Chatty identity-layer steps 13 already exist (`~/Projects/Chatty`
branch `identity-layer`); the Person layer reads the contacts store.
- TASK-16.B done: culver-contacts store keyed to the machine identity
(Personal class, `docs/STORAGE-ENCRYPTION.md`) — the write below goes
through the gated store.
- TASK-16.C done: the contact card / `ContactModel` bridge, so the added
name is viewable and the resolver renders it in the inbox.
## Steps

View file

@ -0,0 +1,141 @@
# TASK 16 — culver: usable messenger + contacts as the Personal-class store
**Status:** open. Rooms sync today; the app is not yet usable and the
contacts store does not yet meet the security bar. Unblocks TASK-01.
**Size:** not single-session — four distinct work units (AD), each
session-sized. Do them in order; A is the smallest and most load-bearing.
**Repo:** `~/Projects/culver` (Gitea `Fimeg/culver`, CI on `primary`).
## Where culver is (verified 2026-07-21)
- Matrix sync is real and further along than any reference bridge:
`SyncService` sliding sync, per-room `Timeline`, E2EE crypto store,
reactions/threads/edits/redaction. `crates/matrix` (1510 LOC) is not
the weak link.
- Runtime architecture is clean: QML → invokable → `AppCommand` (mpsc) →
`runtime.rs``Protocol`/`Store``CoreEvent``ModelUpdate` → GUI
thread. `crates/app/src/runtime.rs`.
- `crates/contacts` (1374 LOC) is real and is **the qtpim replacement**
(qtpim retired — archive/06): owned SQLite store, vCard 4.0 import/
export, number/MXID/email → identity resolver. The runtime already
uses it for name resolution via the `resolve_display` hook.
## The four problems (observed on device + read in tree)
### A. Tap a room → no messages render *(highest leverage)*
The QML and the `ChatModel` bridge were written to **two different
contracts** and never reconciled. Same failure *class* as the
2026-07-20 "matrix login never fires" bug (`#[auto_cxx_name]`), one
level up: here the invokable **names themselves** don't exist, so QML
silently no-ops instead of erroring.
| QML calls / binds | Real bridge (`crates/app/src/bridge/chat.rs`) | Effect |
|---|---|---|
| `chatModel.open(cid)` (Main.qml:58) | `load_conversation``loadConversation` | **`LoadTimeline` never dispatched → timeline stays empty** |
| `chat.loadOlder()` (ChatView.qml:112) | `paginate_back``paginateBack` | back-pagination dead |
| delegate roles `messageId, bodyText, bodyKind, senderDisplay, threadReplyCount, inReplyToId` | actual roles `id, body, html, sender, replyCount, inReplyTo` | **every row binding undefined → blank rows even once data arrives** |
| `chat.avatar / protocol / subtitle / typing` | not qproperties | header half-empty |
| `chat.threadModel` (ThreadView.qml) | doesn't exist — thread arrives via `thread_ready` signal + `thread_rows` | thread pane can't bind |
Two layers, both broken: fix `open``loadConversation` and
`loadOlder``paginateBack`, **and** align the ~14 role names, **and**
add the missing `subtitle`/`avatar`/`protocol` qproperties (or drop the
QML that references them). Decide the ThreadView contract
(signal-driven vs. a real `threadModel`) and make one side match.
`LoadTimeline` in `runtime.rs` reads **only from the store**
(`store.messages`) — correct, because the matrix backend streams
`MessageReceived` into the store on sync. So once the names line up,
tapping a room shows content with no backend change.
**Acceptance:** tap any synced room → its messages render; scroll up
paginates history; header shows title + protocol glyph.
### B. Contacts as the Personal-class store *(the security bar)*
The store works but is **plaintext today**`Connection::open(path)`,
`PRAGMA journal_mode = WAL`, names/numbers/MXIDs/emails/inline photo
BLOBs all in the clear in the shared DB file (`crates/contacts/src/
store.rs`). And `export_vcard` / `import_vcards` are unguarded free
functions — a plaintext off-system egress path. So "won't open without
the system key" and "cannot be shared off system" are the **design
intent, not the current behaviour.**
Bring it to the bar, per `docs/STORAGE-ENCRYPTION.md`:
- **Contacts = Personal class.** Key evicted on lock; rooted in the
machine seed via the Argon2id-wrapped Personal class key. A locked/
stolen device yields ciphertext. Use the primitive TASK-11 already
shipped: souveraine-secrets seals AES-256-GCM under a machined-
signature-wrapped store key (`source=Machined`, reboot-verified). The
contact DB key comes from that rail, not a file on disk. Store won't
open when the rail is unavailable — that is the point.
- **"Share a contact" is a Personal→egress operation, not a function
call.** `export_vcard` stops being freely callable and becomes an
authorized op gated by **stepUp + agent interaction** — the
`org.souveraine.Secrets.Manage` stepUp dialog (TASK-11 "ingress
pending") is the seam. Import (ingress) stays ungated; egress is the
boundary.
**Acceptance:** contact DB is ciphertext at rest and does not open
without the machine identity (verify: move the file to a bare box → no
plaintext). A vCard export attempt raises stepUp and does nothing
without it. Round-trip still green for a legitimately-authorized share.
### C. Contact card UI *(built ON TOP of B, not before)*
The cards at the top of the inbox can't open because there is **no
contact-card bridge or view** — `crates/contacts` has no UI surface
(only the `resolve_display` name hook is wired). Add a `ContactModel`
bridge + card view. Build it against the **gated** store from B, so
opening a card is a keyed read and "share" from the card triggers the
stepUp. Building the UI first would bake the plaintext/free-share model
we are moving away from.
**Acceptance:** tapping a contact chip/avatar opens a card (name,
numbers, MXIDs, photo); "share" on the card triggers stepUp.
### D. Notifications: culver → NotifyEvents *(small)*
Notifications on the phone already work — the **shell owns the surface**
(`org.freedesktop.Notifications` is quickshell's; TASK archive/04 built
the `NotifyEvents` fan-out seam + lock card). culver just isn't feeding
it: nothing in the tree emits. Chatty's `im.received` uses the
`notify-send`/libnotify pipe that lands in banner + lock card.
Emit on inbound `MessageReceived` for conversations that are **not the
open one and not muted** (mute state already exists — `Conversation::
muted`, Matrix `RoomNotificationMode::Mute`). One emit point in
`runtime.rs`'s event fold.
**Acceptance:** an inbound message to a non-open, non-muted conversation
raises a banner (and the lock card while locked) via the same pipe
Chatty uses.
## Also queued (not blocking AD)
Inbox favorites redesign: replace the pinned `ListView` section
(`Inbox.qml`) with a **horizontal circles rail** along the top, ordered
by last-contacted, later honouring a pinned state. New component, not a
fix — do it after A lands so there is content to open.
## Build / deploy
Per tree conventions: build on archdev, never the laptop. culver builds
in archdev `~/culver`; CI runs on push to `primary`. cxx-qt 0.9 keeps
snake_case Qt-facing names — every `extern "RustQt"` block needs
`#[auto_cxx_name]` (root cause of the class of bug in A). Deploy to the
Pixel 3 from the laptop over USB.
## Connects to
- **TASK-01** (favorites — wife's name on her number): *this is its
unblock.* culver-contacts is the "small owned store the Person layer
reads directly" TASK-01 has been blocked on since qtpim retired
(archive/06). Re-point TASK-01 here.
- **TASK-11** (souveraine-secrets): supplies the at-rest key + stepUp
for B.
- **archive/04** (notification server): supplies the `NotifyEvents`
seam for D.
- `docs/STORAGE-ENCRYPTION.md`: Personal-class placement for B.