xworkmate-app/docs/architecture/secure-local-persistence-architecture.md
2026-03-23 08:59:44 +08:00

272 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Secure Local Persistence Architecture
## 目标
本文记录 `XWorkmate.svc.plus` 当前桌面端本地持久化实现的真实基线,并明确区分:
- 当前正在使用的持久化路径
- 仅用于旧版本恢复的 legacy sealed-state 路径
- secret 与 recoverable local state 的边界
如果后续重新引入 sealed local state这份文档必须和 `SettingsStore` 写路径、测试断言一起更新。
## 当前实现基线v0.6.1
### 1) macOS 标准持久化目录
默认目录按 Apple 常规结构落在:
- `~/Library/Application Support/plus.svc.xworkmate/xworkmate`
当前活跃文件与目录:
- `config-store.sqlite3`
- `SettingsStore` 主库
- `settings-snapshot.json`
- `SettingsSnapshot` 的 durable mirror
- `assistant-threads.json`
- `AssistantThreadRecord` 列表的 durable mirror
- `gateway-auth/secure-storage/*`
- `SecretStore` 的文件型 secure-storage fallback
### 2) 首次安装初始化
- `SettingsStore.initialize()` 会初始化并打开 `config-store.sqlite3`
- `SecretStore.initialize()` 会初始化 `gateway-auth``secure-storage` 目录结构
- 因此首次安装后,不需要等用户手工保存一次,目录与主存储文件就会被准备好
### 3) 升级与重启行为
- 应用升级 / 系统更新重启不会改写既有持久化目录
- 用户主动执行“设置 -> 诊断 -> 清理任务线程与本地配置”时,清理的是本地 settings / thread 状态
- 清理流程不会删除已保存 secretsGateway token / password、AI Gateway API key、Vault token、device token 等)
### 4) 路径解析失败策略(默认)
- 默认策略仍然是 `fail-fast`
-`SettingsStore` 无法解析或打开耐久数据库路径时,直接抛错
- 只有显式开启 `allowInMemoryFallback` 时才允许内存数据库回退
### 5) 当前最重要的实现结论
- 长期 secret 继续通过 `SecretStore` 持久化,主路径是 `FlutterSecureStorage`
- `SettingsSnapshot``AssistantThreadRecord` 当前写入的是明文 JSON 字符串
- 会写入 `config-store.sqlite3`
- 也会写入 `settings-snapshot.json` / `assistant-threads.json`
- `assistant-state-backup.json`、`sealedState`、`xworkmate.local_state.key` 现在不是当前主写路径
- 它们只保留在旧版本恢复 / 迁移兼容逻辑里
## Trust Boundary
当前需要区分 3 类状态:
### 1. 高敏感 secret
- Gateway shared token
- Gateway password
- AI Gateway API key
- Vault token
- device token / device identity 私钥材料
### 2. 可恢复的本地应用状态
- `SettingsSnapshot`
- `AssistantThreadRecord` 列表
- assistant custom task titles
- archived task keys
- last session key
- 本地审计 trail
### 3. Legacy sealed-state 恢复输入
- 旧版 `assistant-state-backup.json`
- 旧版 `xworkmate.sealed.local-state.v1` payload
- `local-state-key.txt`
- secure storage 里的 `xworkmate.local_state.key`
边界规则:
- 第 1 类状态走 `SecretStore`
- 第 2 类状态当前走 `SettingsStore`,属于 recoverable app state不是 secret store
- 第 3 类状态只用于 recovery / migration不是当前版本的常规写入目标
## 当前架构图
```mermaid
flowchart TD
A["Gateway / Settings Form"] --> B["SettingsController"]
C["Assistant Thread State"] --> D["AppController"]
B --> E["SecureConfigStore"]
D --> E
E --> F["SecretStore"]
E --> G["SettingsStore"]
F --> H["FlutterSecureStorage"]
F --> I["gateway-auth/secure-storage/*<br/>file fallback"]
G --> J["config-store.sqlite3"]
G --> K["settings-snapshot.json"]
G --> L["assistant-threads.json"]
M["Legacy sealed-state sources"] --> N["legacy recovery / migration"]
N --> G
```
说明:
- 当前活跃写路径是 `SecretStore` + `SettingsStore`
- legacy sealed-state 只参与读旧数据并迁移到当前 store不参与当前常规写入
## 存储分层
### 1. 当前 secret 存储
用途:
- 保存 Gateway token / password
- 保存 AI Gateway API key
- 保存 Vault token
- 保存 device identity / device token
实现要点:
- 主路径是 `FlutterSecureStorage`
- 当 secure storage 不可用时,`SecretStore` 会尝试提升到文件型 fallback
- 文件型 fallback 位于 `gateway-auth/secure-storage/*`
### 2. 当前本地状态持久化
当前覆盖对象:
- `xworkmate.settings.snapshot`
- `xworkmate.assistant.threads`
- `xworkmate.secrets.audit`
实现要点:
- `SettingsSnapshot` 通过 `toJsonString()` 写入
- `AssistantThreadRecord` 列表通过 `jsonEncode(...)` 写入
- 当前写路径没有 AES-GCM seal / unseal
- durable mirror 文件内容当前也是明文 JSON不是 sealed envelope
### 3. Durable mirror files
当前保留两类 durable mirror
- `settings-snapshot.json`
- `assistant-threads.json`
语义:
- 作为 SQLite 的文件镜像 / fallback 来源
- 也是测试里会直接读取和断言的当前持久化内容
### 4. Legacy sealed-state recovery path
旧版 sealed local state 兼容仍然保留,但仅用于 recovery
- 识别旧版 `xworkmate.sealed.local-state.v1`
- 读取旧版 `assistant-state-backup.json` 里的 `sealedState`
- 通过 legacy local state key 解密旧 payload
- 成功恢复后重写到当前 `SettingsStore`
这条路径的目标是兼容旧数据,不代表当前版本仍在主动写 sealed local state。
## 当前写入流程
### SettingsSnapshot
1. `SettingsController``AppController` 生成新的 `SettingsSnapshot`
2. `SecureConfigStore.saveSettingsSnapshot()`
3. `SettingsStore.saveSettingsSnapshot()`
4. `snapshot.toJsonString()`
5. 写入 SQLite
6. 同步写入 `settings-snapshot.json`
### Assistant Threads
1. `AppController` 更新线程记录
2. 更新被串行排入 `_assistantThreadPersistQueue`
3. `SecureConfigStore.saveAssistantThreadRecords()`
4. `jsonEncode(records.map(...))`
5. 写入 SQLite
6. 同步写入 `assistant-threads.json`
这么做的目标是避免异步写晚到覆盖较新的线程快照;当前目标不是加密封装。
## 当前读取与恢复流程
恢复顺序:
1. 初始化 SQLite
2. 优先读取 SQLite entry
3. SQLite 读不到时,再读 durable mirror 文件
4. 如果当前 state 不可读,再尝试 legacy recovery
5. 若发现旧 sealed-state 但缺少 key则产生 locked recovery report
补充说明:
- `SharedPreferences` 只作为旧数据迁移兼容来源,不是当前桌面端的主状态真值源
- Web 端有独立的 `WebStore`,不适用这里的桌面持久化链路
## Legacy backup / sealedState 的当前语义
当前代码里:
- `assistant-state-backup.json` 只在 legacy recovery 时读取
- `sealedState` 只在旧版 backup 或旧版 durable value 解密时出现
- `xworkmate.local_state.key` 只通过 legacy loader 参与旧数据恢复
因此这三者现在应该被理解为:
- 兼容旧版本
- 避免升级后直接丢历史
- 不属于当前日常写入架构
## Clear 行为
`clearAssistantLocalState()` 当前会清理:
- `SettingsSnapshot`
- `AssistantThreadRecord` 列表
- `settings-snapshot.json`
- `assistant-threads.json`
- 旧版 `assistant-state-backup.json`(如果存在)
不会误删:
- Gateway token / password
- AI Gateway API key
- Vault token
- device token / device identity
## Debug / Test 策略
为了让测试稳定运行,当前保留可注入的 secure storage client
- `SecureStorageClient`
- `FlutterSecureStorageClient`
- `FileSecureStorageClient`
- `MemorySecureStorageClient`
策略:
- release优先真实 `FlutterSecureStorage`
- debug / test允许注入式或文件型 secure storage
- `allowInMemoryFallback` 只在显式场景下允许内存数据库回退
## 当前文档结论
当前桌面端本地持久化不是 sealed local state 架构,而是:
- secrets 走 secure storage / file fallback
- recoverable local app state 走 SQLite + plain JSON durable mirrors
- legacy sealed-state 只用于恢复旧数据
如果后续要把本地状态重新升级为 sealed payload必须同步更新
- `SettingsStore` 写路径
- 文档中的架构图与存储分层
- 相关测试断言