From edbdbd869761314cea7a50b7a1c41db2951f3673 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Sun, 21 Jun 2026 23:58:05 +0200 Subject: [PATCH] docs: update CLAUDE.md with biometric unlock + reasoning persistence --- CLAUDE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0dc32a5..4651910 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ Make the **native Android app** (`kaizen-app`) a valid daily-driver that talks t ### Package structure (`app/src/main/java/dev/kaizen/app/`) ``` -auth/ LoginScreen +auth/ LoginScreen, BiometricUnlock (KeyStore + CryptoObject) chat/ ChatScreen, ChatViewModel, Sidebar, ChatComponents, ChatModels, ModelSheet, Markdown, Background db/ Room offline cache: KaizenDatabase, Entities, DAOs, @@ -174,7 +174,7 @@ Send message → in-memory during streaming → Room flush after stream ends - `db/KaizenDatabase.kt` — singleton via `companion object` (double-checked locking), thread-safe - `db/ConversationRepository.kt` — `observeAll()` (Flow of ConversationSummary), `refresh()` (server → Room), `allUnlockedIds()` - `db/MessageRepository.kt` — `observeMessages()`, `getCached()`, `fetchAndCache()`, `cacheMessages()`, `prefetchMissing()` (background prefetch for offline) -- `db/Converters.kt` — `List` ↔ JSON string via kotlinx.serialization +- `db/Converters.kt` — `List` + `List` ↔ JSON string via kotlinx.serialization - `db/Mappers.kt` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity` **AGP 9.x compat:** `android.disallowKotlinSourceSets=false` in `gradle.properties` (KSP issue [#2729](https://github.com/google/ksp/issues/2729)). @@ -191,6 +191,8 @@ All backend work for the app is done. `pnpm test:run` green (774+), `tsc` + `lin - **`GET /api/v1/me`** — identity + `defaultModel` for native clients. - **`GET /api/v1/models`** — Bearer-authed, returns filtered model catalog. - **`/api/v1/conversations`** — thin re-exports of canonical handlers, Bearer-authed. +- **`POST /api/v1/auth/unlock`** — Bearer-authed, returns unlock token in response body (not cookie) for native clients. Rate-limited 10/5min per user. +- **`GET /api/v1/conversations/[id]`** — accepts `X-Unlock-Token` header alongside the existing `kaizen_unlock` cookie for locked conversations. - **`GET /api/v1/meta`** — capability handshake. - **App-devices card** (`components/settings/app-tokens-card.tsx`) — see/revoke signed-in devices on `/settings/security`. - **API versioning** — `/api/v1/` for outward contract, additive-only within v1. Web-internal endpoints stay unversioned. @@ -254,13 +256,13 @@ These are what separate "prototype" from "daily-driver": - [ ] **Conversation rename/delete** from the sidebar (currently read-only) - [ ] **Conversation pin/unpin** from the sidebar -- [ ] **In-app unlock for locked conversations** (currently skipped with "Gesperrter Chat" label) +- [x] **In-app unlock for locked conversations** — BiometricPrompt with CryptoObject (AES key in TEE/StrongBox), hardware-enforced biometric auth → server unlock token via `POST /api/v1/auth/unlock` → fetch messages with `X-Unlock-Token` header ### 3. Security hardening - [ ] **Auto-logout on 401** — if the token is rejected, route to login screen instead of showing error banner - [ ] **Server version check** — call `/api/v1/meta` after login to detect server skew and warn cleanly -- [ ] **Biometric lock** (`BiometricPrompt` + KeyStore binding) — see `LATER.md` §1C +- [x] **Biometric lock** (`BiometricPrompt` + KeyStore/CryptoObject) — implemented for conversation unlock; `setUserAuthenticationRequired(true)` + `setInvalidatedByBiometricEnrollment(true)` ensures hardware-enforced biometric, not bypassable even on rooted devices - [ ] **`FLAG_SECURE`** on sensitive screens — see `LATER.md` §1B ### 4. Features for parity with the web