7.3 KiB
ADR-0179:Materialized Local Plugin Package Recovery Catalog
- 状态:Accepted
- 日期:2026-07-27
- 关联:RFC D-65、D-138、D-147、D-168、D-169;ADR-0138、ADR-0140、 ADR-0143、ADR-0178
背景
本机 Plugin Package install repository 会把完整 PackageLock 和 queued/staged/
activating 状态持久化,但 lock 有意不保存主机文件路径、Registry credential 或其他
部署细节。进程若在 durable create 之后、stage 之前退出,下一次启动必须能按同一 lock
重做 stage;否则 ADR-0140 的 admission gate 会正确地失败关闭,却没有可用的产品恢复
路径。
把 bundlePath 写入 durable lock 会把主机拓扑变成领域事实,并让备份恢复依赖旧路径;
让常驻 application 直接拉 OCI 又会把网络、Registry credential、重试和供应链 authority
带进低配路由设备的 steady-state runtime。offline 与 OCI lock 的恢复也不能使用两套
不同的签名和内容校验语义。
决策
1. 使用按 lock digest 寻址的本机物化 catalog
@qinglong/local-application/plugin-package-recovery-catalog 在既有 package 内提供
PluginPackageStageProvider,不新增 workspace package。配置显式选择:
{
"mode": "materialized_catalog",
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles",
"publisherTrustFilePath": "/opt/qinglong/private/plugin-package-publisher-trust/current.json"
}
catalog root 只允许当前 real/effective UID 拥有的、非 symlink、真实路径不漂移的
0700 目录。目录最多包含 64 个条目,而且只能出现
<64-lowercase-hex-lockDigest>.json;未知文件或越界目录整体失败关闭。
每个 source entry 必须是当前 UID、regular、no-follow、精确 0600、最大 256 KiB 的
exact-shape JSON:
schema = qinglong/local-plugin-package-recovery-source@v1
lockDigest
source = exact durable PackageLock.source
bundlePath = bundleRoot/<source.artifactDigest>.bundle
manifest
signature
读取前后复验 catalog directory identity;私有 JSON reader 在打开前、打开后和读取后 复验文件 device/inode/UID/mode/size,并多读一个 byte 防止并发增长。
2. offline 与 OCI lock 共用本地 bundle 验证语义
source.kind 可以是 offline 或 oci,但 source 的 kind、locator、artifact digest、
artifact bytes 和 content digest 必须与 durable PackageLock 完全相同。OCI locator
仍保持 digest-pinned;catalog 中的 bundlePath 只是部署/管理 authority 已下载完成的
本地副本,不把 locator 改写成 offline。
provider 复用既有 createLocalPluginPackageFileStageProvider,因此两种 source 都执行
同一组发布者 Ed25519 trust/lifetime、签名、canonical manifest、tar、artifact、
content 和 install-plan 校验,并写入既有私有 opaque staging。已存在的 exact stage
按原协议 replay,不重新解释为新 generation。
3. trust 是每次 stage 重新读取的私有 authority
publisher trust 文件必须是当前 UID、regular、no-follow、精确 0600、最大 256 KiB
的 exact-shape JSON:
schema = qinglong/plugin-package-publisher-trust@v1
keys = PluginPackagePublisherKeyDefinition[]
每次实际 stage 都重新加载并由既有 PluginPackagePublisherTrustRegistry 校验 key
类型、状态和有效期。进程不持有可变 trust cache,也不从环境变量或 catalog entry
接受额外公钥。
4. application 只在 queued stage 真实发生时加载 catalog
pluginPackages.recoverySource 是 process config 的必填字段:
disabled:空队列可启动;存在 queued stage 时保持 ADR-0178 的QL3_LOCAL_APPLICATION_PLUGIN_SOURCE_UNAVAILABLE;materialized_catalog:只在 coordinator 调用stage()时 dynamic import catalog provider。
没有 queued work 时不读取 catalog、trust 或 bundle,也不加载 bundle inspector/ staging 依赖。实现不增加 timer、watcher、socket、网络客户端、第三方依赖或新的 workspace importer。应用不接收 Registry credential,也不负责下载 OCI。
配置规范化同时要求 storage、runtime、staging、activation、catalog 与 trust 的全部 authority path 互不别名。
5. catalog 发布与在线 OCI 获取不属于常驻 runtime
本 ADR 只定义消费协议。catalog entry、bundle 和 trust 的认证发布、替换、回收应由 短生命周期、可审计的部署或管理 ceremony 完成;在线 OCI 获取也属于该 authority。 ADR-0180 已产品化本机认证发布、inspect 与有界 collect;ADR-0181 已提供认证 trust provision、addition-only overlap rotation,并把 key lifetime 绑定到不可变 lock 创建时间。在线 OCI fetch、旧 key retirement/紧急 revoke、自动更新与完整插件市场 仍不属于本 ADR。
拒绝方案
- 把 bundle path/credential 写入 durable lock:污染可迁移领域事实并扩大 Secret 生命周期,拒绝。
- application 启动时扫描 bundle 目录推断 lock:文件名不是审批事实,且扫描成本 与目录内容相关,拒绝。
- OCI queued 时由 application 在线拉取:把 egress、credential 和重试 authority 带入 steady-state runtime,拒绝。
- offline/OCI 各写一套 verifier:会使签名、manifest 与 digest 语义漂移,拒绝。
- 常驻 watcher 自动热装:绕过 durable Approved Action/install head,并破坏低配 设备空载预算,拒绝。
- 为 catalog 再拆一个 package:没有独立部署或版本生命周期,只增加碎片化, 拒绝。
当前证据
- offline 与 digest-pinned OCI lock 都使用真实 Ed25519 签名 bundle 完成 stage;
- source 漂移、trust 权限放宽、未知文件、超过 64 条、缺失 entry 均失败关闭;
- 已存在 exact stage 在原 bundle 删除后仍按既有 staged evidence replay;
- 真实 SQLite durable queued install 经 application recovery 推进到 active,最终
safeToAdmit=true且 active lock digest 精确一致; - disabled 和 materialized-catalog 的空队列路径都不加载 catalog module;
- 未新增 package、第三方依赖、timer、watcher、socket或 cluster/runtime authority。
- local-command-file 3/3、local-application 30/30、dependency/source boundary 30/30;
- 22-package clean build/test 全绿,legacy/back 802/802,edge import audit 为 121 modules 且 forbidden root dependency/import 为空;
- disabled AI benchmark 只加载 1 个 AI module,storage/provider/management authority loader 均为 0;
- edge/standalone application 为 4,668,784/4,668,928 bytes、605 files、90 个 startup modules;AI-inclusive application 为 5,346,605/5,346,761 bytes、649 files、89 个 startup modules。四个制品门还会在离线安装后单独加载 catalog public subpath,全部 在既有预算内。
后续门禁
- publisher trust 的 current-signer impact、旧 key retirement、紧急 revoke、 rollback 与重签恢复;
- 短生命周期 OCI fetcher 的 Registry credential rotation、rate limit、审计和 response-loss 恢复;
- 固定低配 Linux 路由器的真实冷启动、idle RSS、闪存写入与断电恢复证据;
- 外部 deployment/cutover controller、API host 与完整插件管理产品面。