272 lines
7.9 KiB
Markdown
272 lines
7.9 KiB
Markdown
# 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 状态
|
||
- 清理流程不会删除已保存 secrets(Gateway 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` 写路径
|
||
- 文档中的架构图与存储分层
|
||
- 相关测试断言
|