Skip to content

ADR-016 — 基于用户自有 Git 仓库的多机同步

English: ADR-016-multi-machine-sync.md

  • 状态: 已接受
  • Spec: 010-sync
  • 修订: 宪章原则 I(0.3.0 —— 用户掌控的同步介质例外)

背景

Coffer 采用本地优先(local-first)架构:每台机器拥有自己的保险库(vault),任何 厂商云都不是系统的权威记录源(system of record)。在多台机器上运行 Coffer 的用户 此前无法保持这些保险库的一致性 —— 知识、记忆、已注册资源和凭据会出现分歧。多机 同步此前是一个明确的非目标,恰恰是因为显而易见的解决方案(托管式同步服务)会违反 原则 I。

我们希望在放弃本地优先保证的前提下实现同步。

决策

通过用户自己拥有的 git 仓库(私有 GitHub 仓库、自托管 git 等)来同步保险库 状态。git 既是传输通道,也是历史/合并引擎;每台机器都保留一份完整的保险库。这一 做法获得了原则 I 受限例外(0.3.0)的授权:不引入新的权威记录源、密文形式存储机密、 用户主动选择并拥有。

同步是一项横切服务,而非资源种类

同步作用于整个保险库;它不是像 mcp_serverchannel 那样由用户管理的实体。 因此它遵循 retention/credentials 这类横切模式 —— application/sync/ + infrastructure/sync/ + 专用的 CLI/HTTP 接口 + 守护进程 worker + 单行的 sync_config 表 —— 而不是套用 Kind 框架。这避免了用一个非种类的东西去污染跨 种类的导入契约。

独立的同步工作区 + 导出/导入

一个专用的 git 工作树(~/.coffer/sync/)与运行时目录 ~/.coffer/ 分开保存。 一次同步运行会先把本地状态导出到工作区,运行 git,然后再把合并后的结果 导入回来:

  • 知识 / 记忆以文件形式镜像(它们本就是磁盘上的真相),SQLite 索引则通过既有 的按内容哈希对账(reconcile-by-content-hash)路径重建。
  • 配置资源会被确定性地序列化(键排序、时间戳归一化、剥离仅本地字段)为每个 资源一个 YAML 文件,使得 git 的 diff 和合并有意义,并通过与种类无关的 ResourceService 对账回 SQLite。
  • 凭据以 Fernet 密文 blob 的形式导出,并导入 credentials 表。

SQLite 仍然是本地的权威记录源;工作区则是可 diff 的文本。

主密钥永不进入同步介质

git 仓库中始终只保存密文。Fernet 主密钥通过带外方式(coffer sync key export/import)引导到每台机器上。一台拥有密文但没有密钥的机器会报告 credentials_locked 并拒绝启动受影响的资源 —— 它绝不会悄无声息地解密失败。

这也正是让宪章层面的论证保持干净的原因:即使远端是 GitHub,厂商也始终只持有无法 解密的密文。

手动 + 可选的自动

coffer sync 是显式、可预期的默认方式。一个可选启用的守护进程 worker(仿照 retention worker 设计)为希望免操心收敛的用户增加了去抖的变更即推送(push-on-change)

  • 按间隔拉取。默认关闭。

冲突会中止本次运行

每个资源一个文件使冲突保持很小的粒度。在任何 git merge 冲突时,本次运行进入 conflicted 状态且不导入任何内容;用户通过 coffer sync resolve (ours/theirs/path)来解决。任何一方都不会被悄无声息地丢弃;自动同步在冲突期间 暂停推送。

考虑过的备选方案

  • 托管式 Coffer 同步服务 —— 体验最佳,但属于厂商掌控的权威记录源;已否决 (将需要一个范围大得多的修订)。
  • 点对点(Syncthing 风格) —— 最符合本地优先,但需要设备发现/在线重叠,且不能 免费获得历史/合并能力;作为首版方案已否决。
  • 用户自有对象存储(S3/Dropbox) —— 简单,但冲突处理薄弱,且不利于按资源 diff/合并的模型;已否决。
  • 直接提交 coffer.db —— SQLite 是二进制且无法合并;已否决。因此才有了文本 导出/导入层。

后果

  • 跨全部四层新增 sync/ 切片、一张 sync_config 表 + 迁移、CLI/HTTP 接口、一个 桌面面板,以及一个守护进程 worker。
  • 出站 git 是一种网络出口;它遵守仅回环(loopback-only)的姿态(git 作为子进程 运行,使用用户环境中的 git 凭据,而非 Coffer 的 HTTP 客户端)。
  • 资源序列化器的确定性对于干净的合并至关重要,并有单元测试覆盖。
  • 凭据引导是每台机器一次性的手动步骤 —— 这是为了绝不把密钥放入同步介质而接受的 代价。