Files
qinglong/docs/adr/ADR-0098-policy-fenced-atomic-legacy-task-adoption.md
T

90 lines
8.4 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-0098Policy 围栏下的原子 Legacy Task adoption
- 状态:Accepted(本机原子 publisher、ledger、Policy/fence/audit 与端到端测试已实现;产品 issuer/CLI、实机写放大、Scheduler/Run admission 和 PostgreSQL 对等实现待完成)
- 日期:2026-07-22
- 关联 RFCQL-RFC-0001 D-03、D-04、D-08、D-17、D-23、D-62、D-70、D-85、D-90、D-91、D-92、D-93、D-94、D-95、D-96、D-97
- 关联 ADRADR-0074、ADR-0087、ADR-0088、ADR-0089、ADR-0091、ADR-0092、ADR-0093、ADR-0094、ADR-0095、ADR-0096、ADR-0097
## 背景
ADR-0095 至 ADR-0097 已把 Legacy Crontab source、逐项分类、review decision、强认证 reviewer、短 TTL 和私有 HMAC decision file 绑定起来,但尚未定义最终物化事务。常规 `appendTaskDefinitionRevision``appendTriggerRevision` 各自拥有 `BEGIN IMMEDIATE`;按行循环调用会在后续 Trigger 或 candidate 失败时留下已提交 Task,不能满足 adoption 的全有或全无语义。
同时,路由设备不能为 100,000 行审阅一次缓存全部 command/candidate;集群节点又需要确定的 canonical 结果、授权 fence 和可审计重放。publisher 不能进入常驻 runtime,也不应为了一个单 consumer 用例再拆一个 workspace package。
## 决策
### 1. 使用既有包的短生命周期 subpath
本机存储新增 `@qinglong/local-sqlite/adoption`local-admin 新增内部 `legacyCrontabPublisher` module;不新增 workspace package。常规 runtime、edge/standalone composition 和 adopted activation subpath不导入该入口,dependency audit 只允许 local-admin 的该内部 module 访问 adoption、Policy、security 与 audit contract。
`/adoption` 只支持 revision 1 的新建。目标已存在同 Task/Trigger identity、mutation/decision 冲突或任何 constraint 失败时整批回滚;常规 Task/Trigger Repository 的事务与更新语义保持不变。
### 2. canonical candidate 只能在进程内流转
classifier 内部生成 Task/Trigger canonical candidate,但根入口、diagnostic、receipt、decision file、ledger、audit 和响应都不得暴露 command。字段语义固定为:
- `name` 映射 Task name;缺失或空值使用 `Legacy Crontab <id>`;控制字符或超限为 malformed;
- `isPinned=1` 映射 `qinglong.io/legacy-pinned=true`
- `saved` 是 2.x crontab 同步瞬态,不迁移;
- `sub_id` 表示 Subscription binding,在 3.0 等价模型完成前固定为 `subscription_binding_requires_mapping`,只能 skip,不能静默丢弃;
- 只有 `lossless→adopt``requires_shell_compatibility→adopt_shell_compatibility` 能产生 publisher candidate。
publisher 重新以目标 `projectId` 运行 TaskSpec/TriggerSpec semantic registry,并重新生成 Task/Trigger content digestclassifier 的 placeholder Project 不能成为持久化事实。
### 3. 外部 authority 和目标事务的顺序固定
完整顺序为:
1. 对 legacy source 取得 `BEGIN IMMEDIATE` 写围栏并记录文件身份;
2. 在同一个 `O_NOFOLLOW` decision-file descriptor 上完成 HMAC、header、receipt、TTL、source 与 disposition 验证;
3. 使用 receipt reviewer 强 Principal 对目标 Project 请求 `project.manage`
4. denied、approval-required 或 policy-unavailable 在不触碰 Task/Trigger 时单独写低敏 audit
5. allow 路径进入目标单一 `BEGIN IMMEDIATE`,重验 Project version、最新 RoleBinding version、active 状态及 owner/admin role
6. 用 iterator 锁步消费 fenced source、第三遍 authenticated decision stream 和内部 candidate
7. 原子创建全部 TaskDefinition revision、context recipe、local execution revision、Trigger revision、allowed audit 和 adoption ledger
8. COMMIT 前再次确认 legacy source 路径身份和已认证 authorization descriptor/path 身份。
source 与 target 是两个 SQLite 文件,不能宣称跨库 ACID;正确边界是 source 全程只读且持有写围栏,所有新事实只在 target 的一个事务内提交。任何外部身份漂移、source/decision 锁步失败或 target mutation 失败都回滚目标事务。
### 4. append-only ledger 是 batch replay authority
SQLite `0033-legacy-adoption-ledger` 新增 `QingLong3LegacyAdoptions`,绑定:
- batch mutation ID、唯一 decision ID、Project/Profile
- plan、inventory、decision-set、receipt、authorization-file digest
- domain-separated publication digest
- source row、adopted Task、adopted Trigger、skip 计数;
- 与 mutation ID 相同的 allowed audit event 和创建时间。
Task/Trigger revision mutation ID 由 batch mutation ID、row ordinal 和本地 index 通过 domain-separated SHA-256 确定派生,并强制 UUIDv4 version/variant bit。batch replay 只有在 ledger 的所有输入事实精确相同时返回 `existing`;相同 decision 的不同 mutation 或相同 mutation 的语义漂移均冲突。`0034-capability-v17` 只在 ledger migration 成功后把本机 contract 推进为 v17readiness 现要求 34 条 migration、30 张 owned table。
### 5. 资源上限和部署档位
source、decision 和 candidate 均按 iterator 顺序消费,不缓存全量数组;Task 最多 100,000Trigger 最多 500,000,超过上限在同一目标事务内失败并回滚。compiler 只在短生命周期 adoption subpath 被调用时加载;没有 timer、watcher、sidecar、第二个目标写 connection 或新增 package。
这些是安全硬上限,不是路由设备容量承诺。正式发布仍要求在固定物理 Edge 设备记录不同规模的事务时间、数据库/WAL/rollback-journal 写放大、峰值 RSS、断电恢复和剩余空间;超大 adoption 可由产品层先拒绝或要求迁移到能力更强的维护环境,但不能改成非原子分批后冒充同一 reviewed batch。
## 被否决的替代方案
1. **逐行调用公共 Task/Trigger Repository**:每个调用独立提交,会留下半迁移状态,拒绝。
2. **把 Repository 改成可嵌套事务**:扩大所有常规写路径的复杂度,而 adoption 只需 revision-1 create-only primitive,拒绝。
3. **没有 ledger,只依赖 audit 或逐行 mutation ID**:无法持久绑定完整 batch 输入和 exact replay,拒绝。
4. **把 command 写入 decision file/ledger/audit**:扩大 secret 泄漏面并破坏低敏凭据边界,拒绝。
5. **为 publisher 新增 workspace package**:单 consumer、共同部署、无独立依赖/制品责任,不满足 D-85,拒绝。
6. **在常驻 runtime 暴露 adoption writer**:让一次性高权限入口永久可达,拒绝。
7. **为降低事务时长而自动分批提交**:破坏一个 reviewed decision set 的原子语义;若未来需要分批,必须成为新的显式 plan/receipt/ledger 协议,拒绝隐式实现。
## 验收证据
1. local-sqlite 测试证明两条 Task、execution facts、Trigger、allowed audit 与 ledger 同事务创建,并可 exact replay。
2. 第二个 candidate identity 冲突时,先前 Task、Trigger、audit 和 ledger 全部为零,证明整批 rollback。
3. Project version 或 RoleBinding fence 变化时,在任何 adoption mutation 前失败。
4. local-admin 端到端测试从 HMAC decision file 和 fenced legacy source 发布 Task/Trigger,并验证第二次调用只返回 existing。
5. classifier 测试覆盖 pinned label、Subscription manual gate、非法 name、candidate 不经根入口暴露及 diagnostic 不含 command/path。
6. local-sqlite 52 项、local-admin 25 项、dependency audit 22 项与 backend 664 项测试通过;typed Drizzle schema 与真实 migration catalog lockstep。
7. 六种 Profile 制品门禁通过:edge/standalone 为 216 files、34 loaded modulesedge-adopted/standalone-adopted 为 234 files、37 loaded modulesedge-application/standalone-application 为 295 files、64 loaded modules。最大制品为 1,962,274 bytes,最大本轮抽样 RSS delta 为 11,763,712 bytes,均低于既有 4 MiB/512 files/16 MiB 门禁。
## 后续约束
本 ADR 只完成本机持久化 mutation authority,不代表 Scheduler、Run admission、2.x process cutover 或产品 issuer 已接管。下一阶段必须提供可信本机产品 ceremony/CLI,使用 adoption 专用 keyring 签发 decision file,并在固定物理 edge 设备完成规模、写放大、断电和空间门禁;随后才可把已发布 Trigger 接到 Scheduler/Run。cluster 必须实现 PostgreSQL 对等 batch ledger/transaction 和独立管理面,不能远程调用本机文件 authority。