7.9 KiB
7.9 KiB
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.sqlite3SettingsStore主库
settings-snapshot.jsonSettingsSnapshot的 durable mirror
assistant-threads.jsonAssistantThreadRecord列表的 durable mirror
gateway-auth/secure-storage/*SecretStore的文件型 secure-storage fallback
2) 首次安装初始化
SettingsStore.initialize()会初始化并打开config-store.sqlite3SecretStore.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. 可恢复的本地应用状态
SettingsSnapshotAssistantThreadRecord列表- assistant custom task titles
- archived task keys
- last session key
- 本地审计 trail
3. Legacy sealed-state 恢复输入
- 旧版
assistant-state-backup.json - 旧版
xworkmate.sealed.local-state.v1payload local-state-key.txt- secure storage 里的
xworkmate.local_state.key
边界规则:
- 第 1 类状态走
SecretStore - 第 2 类状态当前走
SettingsStore,属于 recoverable app state,不是 secret store - 第 3 类状态只用于 recovery / migration,不是当前版本的常规写入目标
当前架构图
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.snapshotxworkmate.assistant.threadsxworkmate.secrets.audit
实现要点:
SettingsSnapshot通过toJsonString()写入AssistantThreadRecord列表通过jsonEncode(...)写入- 当前写路径没有 AES-GCM seal / unseal
- durable mirror 文件内容当前也是明文 JSON,不是 sealed envelope
3. Durable mirror files
当前保留两类 durable mirror:
settings-snapshot.jsonassistant-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
SettingsController或AppController生成新的SettingsSnapshotSecureConfigStore.saveSettingsSnapshot()SettingsStore.saveSettingsSnapshot()snapshot.toJsonString()- 写入 SQLite
- 同步写入
settings-snapshot.json
Assistant Threads
AppController更新线程记录- 更新被串行排入
_assistantThreadPersistQueue SecureConfigStore.saveAssistantThreadRecords()jsonEncode(records.map(...))- 写入 SQLite
- 同步写入
assistant-threads.json
这么做的目标是避免异步写晚到覆盖较新的线程快照;当前目标不是加密封装。
当前读取与恢复流程
恢复顺序:
- 初始化 SQLite
- 优先读取 SQLite entry
- SQLite 读不到时,再读 durable mirror 文件
- 如果当前 state 不可读,再尝试 legacy recovery
- 若发现旧 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() 当前会清理:
SettingsSnapshotAssistantThreadRecord列表settings-snapshot.jsonassistant-threads.json- 旧版
assistant-state-backup.json(如果存在)
不会误删:
- Gateway token / password
- AI Gateway API key
- Vault token
- device token / device identity
Debug / Test 策略
为了让测试稳定运行,当前保留可注入的 secure storage client:
SecureStorageClientFlutterSecureStorageClientFileSecureStorageClientMemorySecureStorageClient
策略:
- 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写路径- 文档中的架构图与存储分层
- 相关测试断言