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

7.9 KiB
Raw Permalink Blame History

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-authsecure-storage 目录结构
  • 因此首次安装后,不需要等用户手工保存一次,目录与主存储文件就会被准备好

3) 升级与重启行为

  • 应用升级 / 系统更新重启不会改写既有持久化目录
  • 用户主动执行“设置 -> 诊断 -> 清理任务线程与本地配置”时,清理的是本地 settings / thread 状态
  • 清理流程不会删除已保存 secretsGateway token / password、AI Gateway API key、Vault token、device token 等)

4) 路径解析失败策略(默认)

  • 默认策略仍然是 fail-fast
  • SettingsStore 无法解析或打开耐久数据库路径时,直接抛错
  • 只有显式开启 allowInMemoryFallback 时才允许内存数据库回退

5) 当前最重要的实现结论

  • 长期 secret 继续通过 SecretStore 持久化,主路径是 FlutterSecureStorage
  • SettingsSnapshotAssistantThreadRecord 当前写入的是明文 JSON 字符串
    • 会写入 config-store.sqlite3
    • 也会写入 settings-snapshot.json / assistant-threads.json
  • assistant-state-backup.jsonsealedStatexworkmate.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不是当前版本的常规写入目标

当前架构图

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. SettingsControllerAppController 生成新的 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 写路径
  • 文档中的架构图与存储分层
  • 相关测试断言