feat: state.db with encrypted auth, in-memory pi settings, models allowlist#35
feat: state.db with encrypted auth, in-memory pi settings, models allowlist#35sasicodes wants to merge 5 commits into
Conversation
bf8e76b to
5ad16f9
Compare
|
| Filename | Overview |
|---|---|
| packages/desktop/src/main/auth-backend.ts | New KeychainAuthBackend wiring safeStorage encryption to SQLite BLOB; async queue mutex is correctly implemented, but readCurrent() missing try/catch around decryptString — a corrupted or cross-keychain ciphertext would crash instead of gracefully returning undefined. |
| packages/desktop/src/main/db.ts | New openStartDb/closeStartDb singleton with WAL pragmas and transactional migrations; migration correctness is solid, but closeStartDb() only clears cachedDb without a way to signal storage.ts to invalidate its own cachedStatements. |
| packages/desktop/src/main/storage.ts | Migrated from JSON file to SQLite kv table; module-level cachedStatements is never cleared by closeStartDb(), so a post-close write opens a new DB connection but executes statements compiled on the old one, causing a cross-connection error. |
| packages/desktop/src/main/chat.ts | Wires new backends into ChatService; settingsManager threaded into all three createAgentSession call sites; closeStartDb() called in cleanup path. |
| packages/desktop/src/main/settings-backend.ts | Simple in-memory Map-backed SettingsStorage; synchronous withLock with correct undefined-means-delete semantics. |
| packages/desktop/src/main/models.ts | Hardcoded allowlist extracted from helpers.ts into its own module; Sets and arrays pre-computed at module load; no logic changes. |
Sequence Diagram
sequenceDiagram
participant App as Electron Main
participant CS as ChatService
participant DB as db.ts
participant ST as storage.ts
participant AB as KeychainAuthBackend
participant SS as safeStorage
App->>CS: new ChatService()
CS->>DB: openStartDb()
DB-->>CS: Database instance
CS->>AB: new KeychainAuthBackend(db)
CS->>CS: AuthStorage.fromStorage(backend)
CS->>CS: SettingsManager.fromStorage(backend)
Note over ST: First state access
App->>ST: readStartState()
ST->>DB: openStartDb()
DB-->>ST: cachedDb
ST->>ST: prepare and cache statements
ST-->>App: StartState
Note over AB: Auth read
CS->>AB: withLockAsync(fn)
AB->>SS: isEncryptionAvailable()
SS-->>AB: true
AB->>AB: readStmt.get
AB->>SS: decryptString(ciphertext)
SS-->>AB: credentials JSON
Note over CS: Cleanup path
CS->>DB: closeStartDb()
DB->>DB: close and clear cachedDb
Note over ST: cachedStatements still set to old connection
Reviews (2): Last reviewed commit: "chore: drop unused resetAppStateCache ex..." | Re-trigger Greptile
5ad16f9 to
6f901af
Compare
Native dep for PR 2 (state.db). Added better-sqlite3 to pnpm onlyBuiltDependencies so its install script runs and the platform binary is fetched/built.
…owlist
PR 2 of the storage migration plan.
- main/db.ts: openStartDb() opens <baseDir>/state.db with WAL + NORMAL sync
+ mmap + cache PRAGMAs, runs idempotent migrations for schema_version,
app_state, and auth tables. Prepared statements cached.
- main/storage.ts: readStartState/writeStartState/updateStartState back onto
the app_state kv table; one row per logical field (composer_shortcut,
last_workspace, selected_model_key, selected_thinking_level, session_notices,
workspace_bookmarks). Public API unchanged so call sites and tests are
untouched. ~/.start/state.json is no longer read or written.
- main/auth-backend.ts: KeychainAuthBackend implements AuthStorageBackend.
Single-row store ('__all__' provider). safeStorage.encryptString round-trips
the credential JSON; ciphertext lives in the auth.ciphertext BLOB column.
Returns undefined gracefully if safeStorage isn't ready yet so constructor-
time reload() never crashes; a later refreshAuth() picks up real creds.
- main/settings-backend.ts: InMemorySettingsBackend implements the
SettingsStorage shape (withLock(scope, fn)). Pi defaults apply at runtime;
no settings.json is ever written.
- main/models.ts: hardcoded allowlist of provider/id pairs (Claude 4.x,
GPT-5.x, Gemini 3.x). When a new model ships, add a line and release.
helpers.ts now imports the lists from this file.
- chat.ts: ChatService uses AuthStorage.fromStorage(new KeychainAuthBackend),
ModelRegistry.create(authStorage) (built-ins only — no models.json),
SettingsManager.fromStorage(new InMemorySettingsBackend()). Threads
settingsManager into all three createAgentSession call sites.
Tests: FakeAuthStorage.fromStorage / FakeSettingsManager.fromStorage stubs.
@main/db mocked to return a no-op stub so ChatService construction works
without touching disk. 132/132 tests pass.
After this change, no JSON config files exist anywhere under <baseDir>/agent/
or <baseDir>/state.json. The only on-disk state outside session JSONLs is
<baseDir>/state.db (+ -wal/-shm sidecars).
…stmts Greptile flagged two issues: 1. withLockAsync had a real concurrency gap. It read the current credential blob, awaited the caller-provided fn, then wrote — letting two concurrent calls each base their next-blob on the same stale read and silently drop one of the writes. In production this would bite during background OAuth token refresh, where two refresh calls can overlap. Fixed with a serialization chain (a Promise-chained queue that ensures one withLockAsync invocation finishes its persist before the next one reads). 2. storage.ts was preparing fresh SQL statements on every helper call (SELECT, INSERT, DELETE), violating the project's "prepare once at module load" rule. Hoisted the three statements into a cached AppStateStatements object resolved lazily on first use, exposed resetAppStateCache() for tests/dev.
- resetAppStateCache had no callers; per "don't add features beyond the task" rule, drop the export. If a future test needs to clear caches we'll add it back when there's a caller. - ChatService.dispose now calls closeStartDb so the singleton DB is closed cleanly on app shutdown (PRAGMA optimize runs, WAL is checkpointed). Wires the lifecycle that was already partially set up.
…ests agents.md / user-flagged cleanups on PR 2: File naming: - main/auth-backend.ts -> main/pi/auth.ts - main/settings-backend.ts -> main/pi/settings.ts Drops the redundant "-backend" suffix and groups Pi-SDK adapter implementations under a dedicated pi/ folder so future additions land beside their siblings. Dev/prod auth split: - Introduces an AuthCodec abstraction (encode/decode/available) so the single DbAuthBackend works for both environments. - safeStorageCodec drives the packaged build (encrypted at rest via Electron safeStorage / OS keychain). - plaintextCodec drives unsigned dev builds — credentials persist in the same auth.ciphertext column as raw bytes, no keychain access at all. This eliminates the macOS keychain prompts the user was seeing on every dev launch. - resolveAuthBackend(db) is the only export the rest of the app touches; it picks the codec based on app.isPackaged. Sort + small fixes: - AllowedModel interface members sorted by length. - DbAuthBackend / Codec types' fields sorted by length. Tests: - New units/models-allowlist.test.ts: provider coverage, set membership, declaration-order preservation, non-empty per provider. - New units/pi-settings-backend.test.ts: read-before-write returns undefined, write-then-read persists, undefined returned from callback clears, scopes (global vs project) are isolated.
71589a5 to
d007a0e
Compare
Summary
PR 2 of the storage migration plan (see
STORAGE-PLAN.md). Stacks on top of #34 (PR 1) — base isfeat/storage-pr1-repoint-pi. Will retarget tomainonce #34 lands.Introduces
<baseDir>/state.db(better-sqlite3 + WAL) and replaces three of Pi's default storage backends with custom ones. After this PR, the only JSON file anywhere under<baseDir>/is gone — auth/settings/models/app-state all live in SQLite or in code.what changes
main/db.ts—openStartDb()opens<baseDir>/state.dbwith WAL/NORMAL/mmap/cache PRAGMAs, runs idempotent migrations forschema_version,app_state, andauthtables. Prepared statements cached.main/storage.ts— backsreadStartState/writeStartState/updateStartStateonto theapp_statekv table (one row per logical field). Public API unchanged so call sites and tests are untouched.~/.start/state.jsonno longer exists.main/auth-backend.ts—KeychainAuthBackendimplementsAuthStorageBackend. Single-row store (provider='__all__').safeStorage.encryptString↔decryptStringround-trips the credential JSON; ciphertext lives inauth.ciphertextBLOB. Returns undefined gracefully ifsafeStorageisn't ready yet so the constructor-time AuthStorage reload never crashes; a laterrefreshAuth()picks up real creds.main/settings-backend.ts—InMemorySettingsBackendimplements theSettingsStorageshape (withLock(scope, fn)). Pi defaults (compaction, retry, transport, etc.) apply at runtime; nosettings.jsonever written.main/models.ts— hardcoded allowlist of{ provider, id }pairs (Claude 4.x, GPT-5.x, Gemini 3.x).helpers.tsnow imports the lists from this file. When a new model ships, add a line and release.chat.ts—ChatServicewires the new backends; threadssettingsManagerinto all threecreateAgentSessioncall sites.what we drop in this PR
<baseDir>/state.jsonstate.db.app_statetable<baseDir>/agent/auth.jsonstate.db.auth+safeStorage<baseDir>/agent/settings.jsonInMemorySettingsBackend(Pi defaults in code)<baseDir>/agent/models.jsonmain/models.tsconstantTest plan
pnpm check— lint, format, typecheck all greenpnpm test— 132/132 desktop tests passing (addedFakeAuthStorage.fromStorage,FakeSettingsManager.fromStorage, mocked@main/db)~/.start/state.dbis created on first launch and contains only the three tables; verify noauth.json/settings.json/models.json/state.jsonever appearssafeStorage+ DB)state.dbin a hex viewer orsqlite3 ".dump auth"— confirmciphertextis opaque bytes, not plaintext tokens~/.start-dev/state.dbis touched; prod tree unchangednext
PR 3 —
sessionspointer table + fast recents + archive flag. Will branch from this PR.