Files
qinglong/docs/adr/ADR-0222-generation-bound-plugin-package-automation-publication.md

243 lines
13 KiB
Markdown
Raw Permalink 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-0222:按 generation 绑定的 Plugin Package 自动化发布账本
- 状态:Proposed
- 日期:2026-07-29
- 关联 RFCQL-RFC-0001 D-144、D-145、D-147、D-175、D-207、D-211
- 关联 ADRADR-0150、ADR-0151、ADR-0153、ADR-0221
## 背景
ADR-0150 已把 Package 中的 Workflow 与 Prompt 解析成同一份 immutable
materialized revision,但“已经物化”不等于“已经发布给运行时”。如果运行时直接读取
materialized revision,会出现以下问题:
- active generation 切换时,Workflow 与 Prompt 可能分别可见;
- disable/enable 只撤出 Task 与 Tool,自动化定义仍可被新执行消费;
- 集群多副本无法证明读取的是同一 generation、同一 publication head
- 路由设备若用 watcher、目录扫描或常驻缓存弥补,会增加空闲内存、I/O 和恢复状态。
Secret binding 具有独立授权、密文材料和审计边界,不能与只包含低敏定义的
Workflow/Prompt publication 合并。
## 决策
### 1. Workflow 与 Prompt 使用同一 publication
`qinglong/plugin-package-automation-publication@v1` 一次发布同一 Package generation
中的完整 Workflow 与 Prompt 定义:
- target 精确绑定 Project、Package、installation、lock、generation、
generation digest 与 materialized revision digest
- definitions 分别按 ID 排序并拒绝重复;
- Workflow 与 Prompt 各最多 128 项;
- canonical publication JSON 最大 12 MiB
- publication 使用 domain-separated SHA-256 digest。
同一 generation 不拆成 Workflow head 与 Prompt head。消费者只能看到完整 generation
或旧完整 generation,不能看到半批定义。
### 2. append-only digest chain
每个 `(Project, Package)` 只有一个 CAS head,历史 publication 永不 update/delete。
- 每个 active Package generation 都必须拥有一条 publication;包含 Workflow/Prompt
时状态为 `active`,不包含时状态为 `absent` 且 definitions 为空;
- `absent` 是 generation tombstone,禁止旧 generation 的自动化 head 跨过一个空
generation 继续可见;
- 首条 publication 固定为 `active|absent`、version 1、无 predecessor
- Package generation 升级从任意 current head 追加更高 generation 的
`active|absent` publication;允许恢复直接跨过已不再 active 的中间 generation
但 generation 必须严格前进;
- lifecycle disable 追加 `active → withdrawn`
- lifecycle enable 追加 `withdrawn → active`
- `absent` 不参与 lifecycle toggle
- lifecycle successor 必须保留同一 target 与同一 definitions,并绑定 lifecycle
event digest
- version、predecessor digest 与 `publishedAtMs` 单调递增。
uninstall 只能从 disabled 进入,因此 automation head 已是 withdrawn。v1 不再追加
“retired” publication,保留 withdrawn immutable history;物理回收由独立 retention
协议处理。
### 3. 双方言 durable repository
SQLite 使用:
- `QingLong3PluginPackageAutomationPublications`
- `QingLong3PluginPackageAutomationPublicationHeads`
- migration `0079-plugin-package-automation-publications`
- capability `local-core@40`
PostgreSQL 使用:
- `ql3.plugin_package_automation_publications`
- `ql3.plugin_package_automation_publication_heads`
- migration `pg-0043-plugin-package-automation-publications`
- publication capability `control-core@42`
- runtime-only start guard migration
`pg-0044-plugin-package-automation-start-guard`
- current capability `control-core@43`
publication 表 append-onlyhead 只允许 CAS 推进。双方言 repository 都支持
`findCurrent``findByDigest``publish` 与有界 keyset `listPendingPage`。pending
source 只返回“active install 已有 materialized revision,但 automation head 缺失或
generation 不匹配”的 Package。materialized revision fence 不匹配、fork、
JSON/index projection 漂移或缺失 predecessor 均失败关闭。
PostgreSQL runtime、manager 与 package executor 可 SELECT;只有 package executor
可 INSERT publication,并对 head INSERT/UPDATE。没有业务角色取得 DELETE。
### 4. 生命周期同事务联动
ADR-0221 lifecycle repository 不调用另一个自带事务的 repository。automation
repository 提供显式 transaction-bound writer,由 lifecycle 已持有的 SQLite
`BEGIN IMMEDIATE` 或 PostgreSQL `SERIALIZABLE` transaction 调用。
对 current automation head 为 `active|withdrawn` 的 Package
- disable 要求 current automation head 精确匹配 active Package generation 且为
active,然后在 lifecycle event/receipt、Task revision 与 Tool snapshot 的同一事务
追加 withdrawn publication
- enable 要求同一 generation 的 current head 为 withdrawn,在同一事务追加 active
publication
- uninstall 要求 current head 已 withdrawn,不创建新的定义版本;
- 第二次 authorization fence 失败、外键失败、head CAS 失败或任一 capability 写失败
时,整笔事务回滚。
`absent` tombstone 没有可撤出的定义,因此 lifecycle transaction 不为其追加空 toggle
消费者仍以 Package lifecycle/quarantine authority 作为外层准入围栏。
publication 的 lifecycle event 外键要求 lifecycle event 先在事务中插入,再写
publication,最后才允许 COMMIT。这个顺序不改变原子可见性。
### 5. Profile 与资源边界
- 不新增 workspace package
- 不新增 daemon、timer、watcher、listener、socket 或常驻缓存;
- Edge/Standalone 复用单 SQLite operation authority
- Cluster 复用 caller-driven package executor 与单连接 transaction
- 禁用 AI 的 Profile 不因该账本引入 AI provider、SDK 或 credential
- Secret material、SecretRef、Prompt 渲染输入与模型输出均不得进入 publication。
### 6. caller-driven publication 与恢复顺序
initial/next publication 已由 profile-neutral coordinator 发布,并在双方言启动链路中
固定为:
`install recovery → Task materialize/reconcile → Workflow/Prompt publication → Tool snapshot → admission`
- coordinator 在发布前后各观察一次 active generation;换代返回 `superseded`,不把
旧 generation 冒充 current
- 同 generation exact replay 返回 existing,并保留 lifecycle 已写入的
`withdrawn` 状态;
- conflict/invalid 进入 `manual_required`availability 失败进入 `retry`
- 每页和每轮硬上限均为 64Local Edge/Standalone 无显式配置时使用 8×8;
- 不创建 resident reconcilerLocal 复用一个 SQLite operation authorityCluster
复用 caller-driven package-executor Job。
### 7. 安全事实优先的启动门
publication head 只表达 generation 与正常 lifecycle,不把 quarantine 或 publisher
revocation 重写成虚假的 `withdrawn`。启动必须通过独立、精确的
`PluginPackageAutomationPublicationStartGuard`
- 输入固定为 Project、Package 与 current publication digest;旧 digest、非 active
publication、非 current active install、non-active lifecycle 一律返回 false
- SQLite 在同一 operation authority 内同时检查 current publication/install、
lifecycle 与精确 installation/lock quarantine event
- PostgreSQL 只向 `ql3_runtime` 暴露
`SECURITY DEFINER plugin_package_automation_start_allowed`runtime 不取得
quarantine、provenance 或 revocation 表的直接读取权;
- PostgreSQL guard 与 publisher revocation 使用同一 transaction-level signer
advisory lock。若启动观察先完成,它发生在撤销提交之前;若撤销先取得锁,启动等待
后必定观察到 receipt 并返回 false
- pending-source 与 publication writer 同样排除 security-fenced generation,避免
recovery 在隔离或撤销后重新发布能力;
- 不新增 safety-state 镜像表、daemon、timer、watcher 或常驻 denylist。quarantine
event 与 publisher revocation receipt 始终是事实源。
调用方必须在“记录一次自动化启动”的同一数据库事务内消费 PostgreSQL guard;把一次
boolean 查询缓存后再异步启动仍属于 TOCTOU,不能视为生产执行门已经关闭。
### 8. 尚未关闭的产品门
本 ADR 已关闭 durable publication、lifecycle overlay、双方言 quarantine 启动收敛,
以及双方言 publisher revocation 的即时启动收敛:Local 由文件型不可变撤销
receipt 桥接到 SQLite 精确 installation/lock quarantineCluster 由 PostgreSQL
receipt 直接参与启动判定。但不把定义解释为可执行能力。D-211 继续 Proposed
直到至少完成:
1. Workflow Run/StepRun executor 与 admission fence
2. Prompt 产品入口、参数绑定、Policy/Audit 与内容治理;
3. Secret binding consumer。
在这些门关闭前,消费者必须同时检查 Package lifecycle/quarantine authority,不能只因
automation head 为 active 就执行。
## 不采用方案
### 直接读取 materialized revision
拒绝。materialization 是 immutable source fact,不是运行时 publication head。
### Workflow 与 Prompt 分别发布
拒绝。会引入同 generation 部分可见和双 head 恢复协议。
### 把 Secret binding 放入 publication
拒绝。低敏定义发布不能取得 Secret 授权和密文材料 authority。
### 生命周期提交后异步撤出 automation
拒绝。崩溃窗口会留下 Task/Tool 已撤出但 Workflow/Prompt 仍 active。
### 新建 automation workspace package 或 watcher
拒绝。现有 runtime-core 与双方言 storage subpath 足够,额外部署边界没有独立价值,
并会增加路由设备供应链和空闲资源。文件数量不是 package 边界;本能力没有独立部署、
权限域、重依赖或发布责任,因此即使代码增长也继续使用现有 package 的显式 subpath。
## 当前验证
1. runtime-core publication/coordinator 定向 9/9,通过 canonical digest、跨
generation successor、`absent` tombstone、lifecycle withdraw/restore、并发换代
与有界 recovery 分类;
2. SQLite repository 定向 8/8,覆盖 exact current start guard、quarantine 后
pending-source 收敛、publication 拒绝、跨 generation `absent` tombstone 与损坏
失败关闭;
3. SQLite lifecycle 定向测试证明第二次 authorization 检查失败时,Task、Tool
snapshot、lifecycle facts 与 automation head 全部回滚;
4. Local application startup 固定执行 Task→automation→Tool,定向启动回归 14/14
SQLite runtime 只在调用时惰性加载 publication repository
5. PostgreSQL 18.4 arm64 HA fixture 中,同一 Package publication 从 v1 经四次
disable/enable 原子推进到 v5,包含 1 Workflow 与 1 Prompt5 条 publication 和
active head 经 `remote_apply`、timeline 1→2 promotion、旧主 fencing、
`pg_rewind` 及 fresh control replicas 后保持一致;
6. PostgreSQL `pg-0044` 把 control-core 推进至 v43start function 只授予 runtime
package executor/admin/manager/worker ingress 均无执行权;迁移、checksum、
schema/readiness 与 repository 定向 43/43
7. HA gates
`pluginPackageAutomationPublicationTransitionsAtomically=true`
`pluginPackageAutomationPublicationSurvivesPromotion=true`
`pluginPackageAutomationRecoverySourceConverges=true`
`pluginPackagePublisherRevocationImmediatelyFencesAutomation=true`
`pluginPackageAutomationSecurityFenceSurvivesPromotion=true`
`pluginPackageAutomationStartGuardIsRuntimeOnly=true` 且总
`gates.passed=true`;撤销前 current publication 允许,revocation receipt 提交后、
quarantine 后和 promotion 后均拒绝;
8. 调度器 COMMIT-response-loss 同轮修复为“发送 COMMIT 后绝不透明重试”,回归 7/7,
HA 证明只产生 1 Run、1 Attempt、2 Event、0 duplicate occurrence。
9. Cluster caller-driven recovery 已把 automation publication 放在 Task publication
与 Tool snapshot 之间;Cluster PostgreSQL 全量 211 pass/1 条无 DSN 条件 skip
最新 HA 总
`gates.passed=true`,且未访问 registry、未拉取镜像。
10. Local 签名 catalog 已覆盖 active install → materialized Task/Workflow/Prompt →
active automation 的完整链路;撤销 publisher key 后,不可变 receipt callback
以精确 installation/lock provenance 写入 SQLite `withdrawn` quarantineraw
automation publication 仍保持原 generation/lifecycle 事实,但 exact start guard
立即返回 false。测试在 quarantine 与 snapshot 已 durable、文件型 trust current
generation 尚未 promotion 的边界注入崩溃,观察到 current generation 3、
pending generation 4、`recoveryRequired=true` 且启动已拒绝;重放同一命令收敛为
generation 4,随后为 `existing`,SQLite 始终只有 1 条精确 quarantine event。