Files
qinglong/docs/adr/ADR-0138-durable-plugin-package-lock-and-exact-local-activation.md
T

197 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0138:耐久 Plugin Package Lock 与精确本地激活
- 状态:Accepted(完整 PackageLock 原子持久化、双 staging evidence、activation intent、
fresh/recovery coordinator、本地 POSIX publisher、SQLite 端到端组合与 PostgreSQL
四角色持久化验证已完成;ADR-0139 已补 Cluster publisherADR-0140 已补调用方驱动
启动恢复、本机 application gate、标准 OCI resolver、Cluster admin process、
独立镜像与 Job/RBACADR-0149 已补原子资源 generation identity/source,具体语义
materializer 与私有 registry 仍未开放)
- 日期:2026-07-24
- 关联 RFCQL-RFC-0001 D-08、D-09、D-132 至 D-136
- 关联 ADRADR-0087、ADR-0134、ADR-0135、ADR-0136、ADR-0137
## 背景
ADR-0134 至 ADR-0137 已冻结 PackageLock、安装状态机、确定性 staging 和两种数据库
Repository,但实现审计发现三个不能留给产品层修补的缺口:
1. Repository 只保存 record JSON 与 lock digest。进程重启虽能判断
`resume_stage | resume_activation | inspect_activation`,却无法重建被审批的完整
PackageLock
2. POSIX staging adapter 返回 `receipt.json` 的真实 SHA-256,而领域
`stage_completed` 又生成另一个 receipt digest。后者没有保存前者,激活时无法证明
正在发布的仍是已验签、已 fsync 的字节;
3. activation receipt 只绑定 generation 和 content。相同 generation/content 可在不同
installation 或不同 previous pointer 之间误重放;`activating` 的 fresh publish、
response-loss 和 restart recovery 也没有唯一协调器。
这些问题在低配路由设备上不能靠常驻 watcher 或额外数据库解决,在 Cluster 多副本上
更不能靠进程内锁解决。修复必须继续服从 ADR-0087:能力应优先成为现有包的显式
subpath,不能因为一个 adapter 或 coordinator 再创建单文件 workspace package。
## 决策
### 1. 完整 PackageLock 与 queued record 原子保存
`PluginPackageInstallCreate` 必须携带规范化后的完整 PackageLock。Repository create
在同一个数据库 transaction 中保存 lock 与 queued record,并逐项复验
installation、Project、Package、generation、previous active pointer 和 lock digest。
Repository port 新增 `findLock(lockDigest)`。读取必须重新运行 PackageLock normalizer、
重算 digest,并拒绝 JSON、索引列或 record 之间的任何漂移;未知 digest 返回 null
corrupt durable fact 返回 unavailable。
- SQLite `0037` 使用 `lock_json TEXT NOT NULL`CHECK 固定 JSON object、lock digest、
Project 和 Package
- PostgreSQL `pg-0018` 使用 `lock_json jsonb NOT NULL`,除同样绑定外还限制 JSON
object 和大小;
- 两种 adapter 继续由同一 shared repository contract 驱动。
不单建 lock 表:PackageLock 与 installation 同生且不可变,一对一列存储能在不增加
join、表、migration version 或 importer 的前提下保持事务原子性。
### 2. staging 同时保存领域回执和外部字节证据
`PluginPackageStageReceipt` 增加 `evidenceDigest`。领域 receipt digest 继续绑定
lock、artifact、Manifest、content 和 stage reference`evidenceDigest` 则精确保存
adapter 可重新检查的外部事实:
- 本地 staging 为 canonical `receipt.json` 原始字节的 SHA-256
- 未来 OCI/Kubernetes adapter 必须提供同等不可变、可 inspect 的证据摘要。
激活 publisher 必须同时检查领域 stage receipt 和外部 evidence,不能把“状态机曾进入
staged”当作“当前落盘字节仍可信”。
### 3. activation intent 绑定完整切换事实
ADR-0149 将其升级为 `qinglong/plugin-package-activation-intent@v2`,除下列切换事实外
还完整携带并摘要绑定受限的 resource generation
- installation、Project、Package 和 PackageLock
- target generation 与 exact previous active lock
- stage reference、领域 stage receipt digest、外部 evidence digest
- content digest 与 domain-separated intent digest。
activation receipt 必须携带并匹配 intent digest。这样即使 content 和 generation
相同,也不能跨 installation、Project/Package、previous pointer 或 stage evidence
重放。
### 4. fresh publish 与 recovery inspect 严格分离
`PluginPackageActivationCoordinator` 只有两条路径:
- fresh:先 CAS 保存 `activating`,再调用 publisher `publish`;得到 exact receipt 后
才 CAS `active`
- recovery:只调用 publisher `inspect`。exact published fact 提交 `active`
`not_published` 提交受审 failure,冲突提交 `activation_fact_conflict`
publish 抛出 availability 或响应丢失时,durable record 保持 `activating`,调用者不得
在同一不确定路径重新 publish。terminal exact retry 只返回已保存事实。
`PluginPackageInstallationCoordinator` 固定
Approved Action consume → durable create/findLock → stage → staged CAS → activation
coordinator 的顺序。Approved Action consumer 是必须由产品层提供的受信、exact-replay、
带 fence capability;当前不提供默认或弱化实现。
### 5. 本地 publisher 使用单一私有原子 pointer
`@qinglong/local-admin/package-activation` 只接受两个预创建、当前 UID 拥有、真实
`0700` 且 device/inode 稳定的独立目录。每次 inspect/publish 都重新验证 authority
再逐项验证 staging receipt、blob exact set、owner、mode、size 和 digest。
active pointer 名称由 Project/Package 的 domain-separated SHA-256 派生,内容为
canonical intent + receipt,权限固定 `0600`。ADR-0149 将 pointer 升级为 v2,使资源
generation 与 package pointer 同一次 rename 生效。publication 使用:
1. per-pointer `O_CREAT | O_EXCL | O_NOFOLLOW``0600` 锁;
2. 写入并 fsync 私有临时文件;
3. 同目录 atomic rename
4. fsync activation directory
5. 重新 inspect 最终 pointer。
升级必须匹配 exact previous pointerstale writer 冲突失败关闭。publisher 记录自己
创建的锁 inode/devicefinally 只删除身份仍匹配的 owned lock;若另一 publisher 已持锁
或路径被替换,绝不按文件名删除。
### 6. 组合继续留在既有包
- runtime-core 增加显式 `plugin-package-activation`
`plugin-package-installation` subpath
- local-admin 增加显式 `package-activation``package-installation` subpath
- local-sqlite、cluster-postgres 只扩充原 repository subpath
- 不新增 workspace package、第三方依赖、timer、watcher、socket、sidecar 或数据库
connection。
本地组合已用 SQLite Repository、真实 staging evidence 和 POSIX publisher 完成端到端
测试,但没有从 package root 或 production application root 导出。
### 7. Cluster 恢复已组合,产品安装入口继续失败关闭
ADR-0140 已把 allowlisted OCI source resolver、可选 exact-registry credential
provider、同一 PostgreSQL lock authority 驱动的 stage verifier、独立 cluster-admin
image 与最小权限 Job/RBAC 组合完成。
它仍不开放:
- 具体 Approved Action durable consumer、管理 API/CLI/UI
- operator repair workflow
- ADR-0149 已完成的原子 generation identity/source 之上的
Task/Workflow/Prompt/Tool/Trigger 语义 materializer 与旧代 GC
- object-store stage
- publisher revoke/index、Runtime/UI Extension 或动态代码加载。
因此“本地端到端组合可执行”不等于 QingLong 3.0 已允许用户安装插件;生产入口必须等
上述 authority、Audit、recovery 与资源语义 materializer 闭环完成后另行评审。
## 拒绝的方案
- 为 activation、installation coordinator 或每种 publisher 各建 workspace package
拒绝;这些边界由 subpath 和 dependency audit 足以隔离,拆包只增加 importer、
build、发布和路由设备安装元数据成本。
- 只保存 lock digest,恢复时从 OCI/tag/文件重新生成 lock:拒绝;来源可能不可用,
也无法重建已消费审批和当时环境。
- staging receipt 只保存领域摘要:拒绝;无法复验 adapter 已落盘的具体字节。
- `activating` 重启后再次 publish:拒绝;响应丢失时会重复外部副作用。
- 用无条件 `unlink(lockPath)` 清理竞争锁:拒绝;文件名不证明所有权。
- 让 local-admin 默认创建 Approved Action consumer:拒绝;会绕过身份、Policy、
Approval ledger 和 Audit 的产品 ceremony。
## 影响
- edge/standalone 仍使用单 SQLite authority、短 transaction 和调用时 I/O;禁用路径
不加载安装/激活 subpath,常驻成本不增加。
- Cluster Repository 已能耐久恢复完整 lockADR-0139 也已提供 ConfigMap publisher
stage verifier、短生命周期 process 和 RBAC 已组合;ADR-0149 已组合原子 resource
generation identity/source,但语义 materializer 与 Approved Action 产品入口仍未
组合,默认控制面仍不可安装 Package。
- 两个方言和未来 publisher 共用同一 intent/recovery 语义;数据库与部署差异留在
adapter。
- workspace importer 保持 21 个;本切片没有新增 package 或第三方 dependency,回应
“packages 是否拆得过细”的原则是:单文件能力先并入职责相符的包并用显式 subpath
隔离,只有出现独立发布、独立运行或真正不同依赖生命周期时才考虑拆包。
## 验证
必须覆盖:
1. SQLite/PostgreSQL lock 与 record 原子保存、`findLock` exact replay 和 corruption
2. shared repository contract 的缺失 lock、head replacement、CAS 和 recovery page
3. staging 外部 evidence 进入 durable receipt 并参与 activation intent
4. fresh activation 在 publish 前已保存 `activating`
5. publish response loss 后只 inspect、不重复 publish
6. external fact absent/conflict 的稳定 failed reason
7. 本地 pointer exact replay 不推进时钟;
8. previous pointer replacement 和 stale writer conflict
9. stage blob、receipt、pointer、root/symlink tamper fail-closed
10. 预先存在的另一 publisher lock 不被删除;
11. Approved Action → SQLite lock → stage → POSIX active 的端到端组合;
12. explicit subpath/root isolation、依赖方向、六 Profile artifact 与 edge import
13. PostgreSQL 四角色最小权限与物理 HA。
当前针对性结果为 runtime-core 224/224、local-sqlite 68/68、local-admin 51/51、
cluster-postgres 非数据库 124 pass/1 条件 skip,以及 PostgreSQL 18 四角色真实集成
28 pass/1 条件 skip。21-package clean build/聚合测试退出 0dependency/source
boundary 与 edge import 通过且 `findings=[]`ADR-0140 production wiring 后六种
Profile 全部通过,最小 Profile 仍为 39 loaded modules,最大 application 为
2,849,491 bytes/439 files/78 loaded modules,最大 RSS delta 13,090,816 bytes
PostgreSQL 18 HA 的 21 个具体 gate 与总 `passed` 全为 true。