feat(ql3): bind package secrets to generations

This commit is contained in:
whyour
2026-08-13 04:49:19 +08:00
parent 4c2a0b6adf
commit 55ef085a6e
39 changed files with 2182 additions and 55 deletions
+1
View File
@@ -26,6 +26,7 @@
`e7c1743e932f2d7c35dc9153cdf5bc4a03356a38d93fce5507354652aa207a05`,独立审计与 Docker 清理通过。完整验证证据见 ADR-0391。
- D-304/ADR-0392(已接受):Plugin Package 进入安全 quarantine 时,Workflow/Prompt automation publication 不再仅依赖运行时 start guard 间接拒绝,而是与 quarantine event、Package-owned Task disabled revisions、Project Tool snapshot 和 withdrawal receipt 在同一 SQLite/PostgreSQL 事务中收敛为 `withdrawn`。原先仅能引用普通 lifecycle event 的外键升级为 append-only disposition-event 联合引用,历史 migration 与 publication digest schema 保持不变;SQLite edge/standalone 崩溃矩阵覆盖 automation publication insert 后、event/task/receipt/COMMIT 前后,PostgreSQL 通过触发器在既有 `SECURITY DEFINER` quarantine commit 内登记 disposition,再由同一外层 SERIALIZABLE transaction CAS publication head。能力位为 `plugin_package_automation_security_withdrawal@1`;不新增 package、daemon、timer、连接或常驻缓存,适用于低配路由设备和集群节点。SQLite 全量 228/228PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0backend 1,188 pass/2 skippackage/dependency boundary 零 findingPostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `ab156901b9c96ec5a62259c44d83d24ded011e0616dc827d928f3e13efd11786`
- D-305/ADR-0393(已接受):Plugin Package Manifest 的逻辑 Secret requirement 获得按 resource generation 固定的不可变 binding。binding 精确覆盖 Manifest requirements,只保存同 Project、显式 version 的 `qlsecret://` 引用与 `approved-action-execution|local-owner-confirmation` authority evidence digest,不保存 Secret 明文;required 不可为空,optional 可显式为 `null`。发布由当前 active installation head、lock、generation 与 Manifest digest 联合 fencing,相同事实幂等、不同事实冲突;domain-separated digest、最多 64 项和 64 KiB 单行预算同时约束 Local 与 Cluster。SQLite 追加 `0091` ledger 与 capability v46PostgreSQL 追加 `pg-0059`、capability v58,并只向 package executor 授予 `SELECT, INSERT`。不新增 package、daemon、timer、watcher、连接、缓存或集群 workload,低配路由设备只承担一个有界表和三个索引。D-305 不冒充 Secret 已进入执行路径:现有 materialization 拒绝仍保留,D-306 再完成用户授权、Secret resolution、runtime consumption 与 lifecycle/rebinding 语义。core 509/509、SQLite 232/232、PostgreSQL 316 pass/1 条件 skip;完整 18-package clean build/test 退出 0backend 1,188 pass/2 skip,五项边界审计零 findingworkspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`
- D-302/ADR-0390(已接受)
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
@@ -0,0 +1,75 @@
# ADR-0393:按 Generation 固定的 Plugin Package Secret 绑定账本
- 状态:Accepted
- 日期:2026-08-13
- 关联 RFCQL-RFC-0001 D-305
- 关联 ADRADR-0216、ADR-0221、ADR-0392
## 问题
Plugin Package Manifest 已能声明逻辑 Secret 需求,但 materialization 仍会拒绝任何 Secret requirement。仅把 Secret 名称传入执行器会留下三类不确定性:Secret rotation 会让同一 generation 在不同时间解析到不同值,跨 Project 引用可能越权,安装确认与实际执行也缺少可审计的同一事实。
QingLong 3.0 同时面向低配路由设备与集群节点。解决方案不能为 Local/Edge 引入 Secret watcher、常驻缓存、额外数据库连接或独立 daemon,也不能在集群数据库中向宽权限角色暴露 Secret 绑定事实。
## 决策
1. 引入 `qinglong/plugin-package-secret-binding@v1`。一个不可变 binding 精确绑定一个 Package resource generation、Manifest digest 与 lock digest,并完整覆盖 Manifest 声明的所有 Secret requirement。
2. binding 只保存 `qlsecret://` 引用,不保存 Secret 明文。引用必须与 Package installation 位于同一 Project,并固定显式 versionrequired requirement 不允许为空,optional requirement 可以显式记录 `null`
3. binding 必须携带 `approved-action-execution``local-owner-confirmation` authority kind 及其 evidence digest。D-305 只定义并持久化 authority 事实,不新增绕过既有审批或 owner-confirmation 的入口。
4. 发布时必须证明目标仍是当前 active installation head,且 installation、Project、Package、lock、generation 与 Manifest digest 全部一致。相同 generation 的完全相同内容幂等返回 existing;不同内容冲突并失败关闭。
5. binding 使用 domain-separated SHA-256 摘要,持久 JSON 上限 64 KiB,最多沿用 Manifest 的 64 个 Secret requirement 上限。SQLite 与 PostgreSQL 分别追加同构 ledger,不改写历史 migration。
6. Local 使用既有单写者 SQLite authorityCluster 仅向 `ql3_package_executor` 授予 ledger 的 `SELECT, INSERT`runtime、admin、package manager 与 worker ingress 均无权限。实现不新增 package、进程、timer、watcher、连接或常驻缓存。
7. ledger 是历史事实,不直接成为 Package lifecycle 的 active-consumer blocker。D-306 在消费 binding 并产生实际 materialization/执行依赖时,再定义 Secret resolution、撤回与 rebinding 的生命周期规则。
## 方言映射
| 项目 | Local / Edge / Standalone | Cluster |
| --- | --- | --- |
| migration | `0091-plugin-package-secret-bindings` + `0092-capability-v46` | `pg-0059-plugin-package-secret-bindings` |
| capability | `plugin_package_secret_binding@1`contract v46 | `plugin_package_secret_binding@1`contract v58 |
| 写入串行化 | 既有 SQLite 单写者 authority | PostgreSQL 唯一约束与 active-head 条件写入 |
| 资源增量 | 一个有界 append-only 表、三个索引 | 一个有界 append-only 表、三个索引 |
| 常驻开销 | 无新增进程、连接、timer、watcher、cache | 无新增 Pod、worker、连接、timer、watcher、cache |
## 明确不在 D-305 中完成
- 不读取或解密 Secret 明文,不把 Secret 注入 Task 环境。
- 不提供面向用户的 bind/rebind 命令或远程管理 API。
- 不声明 Secret rotation 自动改变既有 generation;新版本必须由后续显式授权和新事实消费。
- 不改变现有“Manifest 含 Secret requirement 时 materialization 拒绝”的执行边界。
## 替代方案
### 执行时解析浮动 Secret 名称
拒绝。它让同一 generation 的行为随 rotation 漂移,无法复现,也无法把授权证据与实际版本绑定。
### 把加密 Secret 值复制进 Package ledger
拒绝。它扩大密文托管面、备份恢复面与轮换复杂度,也不必要地增加路由设备存储和集群权限面。
### 只在产品层保存临时绑定
拒绝。崩溃、重启或主库切换后无法证明执行消费的是安装时确认的同一绑定。
## 影响
- 相同 Package generation 的 Secret 选择成为可复现、可摘要、可审计的不可变事实。
- Secret 明文仍留在既有 Secret custody 边界内;ledger 泄露只暴露有界引用元数据,不暴露值。
- D-305 建立的是消费前置条件,不代表 Secret-aware Package 已可运行;产品授权与 runtime consumption 仍属于 D-306。
- Local 与 Cluster 保持同一领域契约、不同存储 authority,低配设备不承担集群组件成本。
## 验证
- core contract:精确 Manifest coverage、required/optional、同 Project、固定 version、canonical order、authority/digest/shape drift 与 generation/Manifest 重校验。
- SQLite:幂等、冲突、active generation fencing、tamper fail-closed、migration checksum、typed schema、readiness 与 rollout safety。
- PostgreSQL:同构 repository contract、migration checksum、schema/readiness 与 package-executor 最小权限。
- 阶段完成前运行完整 18-package build/test、backend 回归、package/dependency boundary 审计和 PostgreSQL 18 physical-streaming HA gate。
## 当前证据
- runtime-core 全量 509/509local-sqlite 全量 232/232。
- cluster-postgres 317 项:316 pass、1 条仅在未提供独立测试数据库 URL 时条件 skip、0 fail。
- 完整 18-package clean build/test 退出 0Worker 133/133AI 209 pass/3 条件 skipCluster Admin 302 pass/2 条件 skipLocal Owner 163 pass/5 条件 skip,其余 package 同样 0 fail。
- backend 1,190 项:1,188 pass、2 条件 skip、0 fail。package boundary、cluster dependency boundary、edge import、service bridge import 与 local image 五项审计均为 0 findingworkspace 仍为 18 package、无 single-source/shallow-source packagePostgreSQL 60 个与 SQLite 93 个 migration 文件继续作为显式 `ordered_ledger` 管理。
- PostgreSQL 18.4 arm64 physical-streaming HA 125 项 gate 全绿,timeline `1→2`,旧主完成 fence 与 `pg_rewind` 只读重入;报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`
+1
View File
@@ -396,6 +396,7 @@
| [ADR-0390](./ADR-0390-offline-cluster-operator-context-preflight.md) | Offline Cluster Operator Context Preflight | Accepted |
| [ADR-0391](./ADR-0391-read-only-cluster-operator-context-readiness.md) | Read-only Cluster Operator Context Readiness | Accepted |
| [ADR-0392](./ADR-0392-atomic-plugin-package-automation-security-withdrawal.md) | Plugin Package Automation 安全隔离原子撤回 | Accepted |
| [ADR-0393](./ADR-0393-generation-bound-plugin-package-secret-binding-ledger.md) | 按 Generation 固定的 Plugin Package Secret 绑定账本 | Accepted |
## 规则
+3 -3
View File
@@ -6,13 +6,13 @@
"reviewedDenseDirectories": [
{
"kind": "ordered_ledger",
"maxDirectSourceFiles": 59,
"maxDirectSourceFiles": 60,
"path": "packages/ql3-cluster-postgres/src/migrations",
"rationale": "PostgreSQL migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
},
{
"kind": "ordered_ledger",
"maxDirectSourceFiles": 91,
"maxDirectSourceFiles": 93,
"path": "packages/ql3-local-sqlite/src/migrations",
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
}
@@ -84,7 +84,7 @@
"path": "packages/ql3-cluster-postgres",
"name": "@qinglong/cluster-postgres",
"rootSourceFileHardCap": 1,
"rootSourceLineHardCap": 125,
"rootSourceLineHardCap": 126,
"rootSourceFileRoles": {
"index.ts": "public_export"
},