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

13 KiB
Raw Blame History

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 都支持 findCurrentfindByDigestpublish 与有界 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_requiredavailability 失败进入 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_allowedruntime 不取得 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=truepluginPackageAutomationPublicationSurvivesPromotion=truepluginPackageAutomationRecoverySourceConverges=truepluginPackagePublisherRevocationImmediatelyFencesAutomation=truepluginPackageAutomationSecurityFenceSurvivesPromotion=truepluginPackageAutomationStartGuardIsRuntimeOnly=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。