Files
qinglong/docs/adr/ADR-0097-private-authenticated-legacy-adoption-decision-file.md
T

8.0 KiB
Raw Blame History

ADR-0097:私有且可认证的 Legacy adoption 决策文件

  • 状态:Accepted(流式载体、HMAC issuer capability、私有 no-replace 发布、同 descriptor 消费及 ADR-0098 Policy/audit 原子 publisher 已实现;产品 issuer ceremony 待完成)
  • 日期:2026-07-22
  • 关联 RFCQL-RFC-0001 D-03、D-04、D-08、D-17、D-23、D-62、D-70、D-85、D-88、D-90、D-91、D-92、D-93、D-94、D-95、D-96
  • 关联 ADRADR-0073、ADR-0074、ADR-0078、ADR-0085、ADR-0086、ADR-0087、ADR-0088、ADR-0095、ADR-0096

背景

ADR-0096 的 receipt 是常量大小的逐项决策摘要,适合路由设备,但 publisher 仍需要完整 decision stream 才能知道每一行采用、以 shell compatibility 采用或跳过。若只保存 receipt,进程重启后无法重建决策;若把最多 100,000 项放进一个 JSON 数组,读取时又会制造与任务数成正比的内存峰值。

普通 SHA-256 还能被任意持有内容的调用方重算,不能证明 durable decision stream 是由持有部署级 issuer capability 的管理面发布。直接复用可覆盖文件、宽权限目录或符号链接,则会在 review 与 publisher 之间留下替换、降权和 TOCTOU 窗口。

决策

1. 载体留在 local-admin,不新增 package

决策文件是 @qinglong/local-admin 的短生命周期内部模块,与 classifier、receipt 和未来 publisher 共同拥有一个 adoption 管理边界。它不是独立部署物、没有第二个 production consumer,也不引入不同 engine/native 依赖,因此按 ADR-0087 不拆新的 workspace package。

根入口只用 type import 和 lazy require() 暴露 publish/verify@qinglong/local-admin/runtime 不载入文件、HMAC、classifier 或 receipt 模块,不创建 timer、watcher、连接池或常驻 key cache。

2. 文件采用有界 NDJSON 和两遍流式验证

文件顺序固定为一个 header、零到 100,000 个 decision row、一个 receipt 和一个 authentication footer。每行最多 64 KiB,整个文件最多 32 MiB,并且必须以 newline 结束。decision row 只保存 rowOrdinal/sourceDigest/disposition/reason,不得保存 command、hook、path、label、Secret 或任意 replacement spec。

发布时 decision iterable 一边被 receipt classifier 锁步消费,一边写入临时文件;不会缓存数组。验证第一遍以固定 64 KiB chunk 检查 record 顺序、行数、总字节、内容摘要和 HMAC,只保留 decision 区间的起止 offset;认证通过后第二遍从同一已打开 inode 逐行向 receipt verifier 提供 decision。内存上限与任务数无关,代价是一次性管理操作进行两遍顺序文件读取。

3. 文件系统边界必须 owner-only、no-follow、no-replace

目标必须是规范化、绝对、非根路径;父目录必须由当前 real/effective POSIX UID 拥有、为真实目录且权限精确为 0700。最终文件必须由同一 UID 拥有、为真实 regular file、权限精确为 0600,读取执行 lstat → O_NOFOLLOW open → fstat 并在完成后同时复核打开 inode 与路径 inode。

发布先以随机同目录 O_CREAT|O_EXCL|O_NOFOLLOW 临时文件写入,强制 0600、完整 write、fsync,再以 hard link 原子发布,目标存在即失败且绝不覆盖;随后删除临时链接并同步目录。失败只清理未发布的临时文件,若 hard link 已成功但目录 durability 无法确认,则保留最终事实供显式恢复,不伪装成未发生。

4. HMAC issuer capability 认证完整内容

authentication footer 使用 HMAC-SHA-256,对域隔离的 content digest 与精确 content byte count 认证。key ID 使用共享的有界 key contract;发布调用 LocalSecretKeyProvider.active(),验证按 footer 的 exact key ID 调用 resolve(),不尝试所有历史 key。每次取得的 32-byte key copy 在使用后立即擦除,比较使用 constant-time equality。

调用方必须为 adoption authorization 配置专用 keyring/capability,不得把 Secret envelope encryption key 或 Owner credential pepper 当作无区分的全局密钥复用。local-admin 只依赖 runtime-core/local-secret 的窄 provider contract,不直接依赖 local-secret package;这样 edge/standalone adopted 产物不因一次性管理能力被迫携带或加载完整 Secret 管理实现。旧 authorization 在 TTL 内可由 provider 精确解析历史 keykey retirement 必须先证明没有仍可能消费的文件。

5. 认证文件仍必须复验 receipt 与当前 source

HMAC 通过只证明持有 issuer key 的组件发布过这些字节,不证明当前 Legacy source、plan、Profile、timezone 或 reviewer TTL 仍有效。公开 verify API 必须显式给出 expected decision ID 与 plan digest,重建当前 inventory,并让 ADR-0096 verifier 对文件中的全部 decision 重新执行处置矩阵、receipt digest、强 User、TTL 和 source identity 校验。

header、receipt、authentication、expected inputs 任一不一致都失败关闭。文件不能跨 decision、Profile、plan、inventory 或 source snapshot 重放。

6. 本 ADR 不授予 Task mutation authority

认证文件不是 Policy decision、ApprovalRequest、安全审计或数据库 transaction。当前没有任何常驻 Profile、Scheduler、Run admission 或 migration 自动消费它,也没有 API/CLI/UI 获得 key provider。

产品入口仍必须把经过认证的 User principal、task.manage/adoption 专用 Policy、Project/RoleBinding fence、低敏 audit 与专用 key capability 组合成受审 ceremony。未来 publisher 必须在一个 SQLite write transaction 内重验 source fence、plan、receipt、decision stream 和 target expected versions,并让 TaskDefinition、context recipe、execution revision 与全部 Trigger 同成同败;HMAC 不能替代该事务。

被否决的替代方案

  1. 为载体新增 package:没有独立部署、依赖或 consumer 边界,只会继续 package-per-file,拒绝。
  2. 单个 JSON decision 数组:解析内存随 100,000 项增长,不适合路由设备,拒绝。
  3. 只保存 receipt,不保存 decision stream:重启后无法执行逐项 publisher 复验,拒绝。
  4. 只用 SHA-256:持有文件内容者可自行重算,不能证明 issuer capability,拒绝。
  5. rename 覆盖固定文件:可能替换已审阅授权,破坏 decision ID 的一次性事实,拒绝。
  6. 信任 0600 而不做 HMAC:同 UID 或错误装配仍可构造可解析文件,不能区分 issuer capability,拒绝。
  7. 把 HMAC 当作 Policy 或 audit:密码学完整性不包含 RBAC、撤权 fence、业务授权或审计原子性,拒绝。
  8. 在常驻 runtime 自动生成 keyring:混淆部署责任并扩大路由器常驻 authority,拒绝。

验收证据

  1. local-admin 测试完成 publish→JSON-lines durable file→verify 往返,结果与原 receipt 完全一致。
  2. 测试证明文件为 0600、父目录要求 0700、已存在目标不可覆盖,命令和脚本路径不会进入载体。
  3. 测试覆盖错误 key、TTL expiry、内容篡改、同 inode 同大小语义等价改写、宽权限父目录和宽权限文件并全部失败关闭。
  4. receipt 测试补充五分钟认证年龄与三十分钟 lifetime 的精确上限负例。
  5. dependency audit 只允许新模块访问 runtime-core/local-secret,没有新增 package 或第三方依赖。
  6. runtime import 与六种 Profile artifact gate 必须证明新模块仍为 lazyloaded-module 基线不得上升。

后续约束

ADR-0098 已实现 adoption publisher 的单目标事务、Project/RoleBinding fence、allowed audit 与 ledger。publisher 在同一已认证 descriptor 上第三遍消费 decision,按顺序生成 canonical Task/Trigger factsskip 不写任何 head;任一行、identity、source fence 或 audit 失败都回滚整个批次。受信产品 issuer ceremony、Scheduler/Run admission 尚未就位;大批量 transaction 的锁时长、SQLite WAL/rollback journal 增长和路由设备写放大仍必须另设物理门禁,不能仅以逻辑测试宣称可用于 100,000 项。