# 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。配置显式选择: ```json { "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: ```text schema = qinglong/local-plugin-package-recovery-source@v1 lockDigest source = exact durable PackageLock.source bundlePath = bundleRoot/.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: ```text 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。 ## 拒绝方案 1. **把 bundle path/credential 写入 durable lock**:污染可迁移领域事实并扩大 Secret 生命周期,拒绝。 2. **application 启动时扫描 bundle 目录推断 lock**:文件名不是审批事实,且扫描成本 与目录内容相关,拒绝。 3. **OCI queued 时由 application 在线拉取**:把 egress、credential 和重试 authority 带入 steady-state runtime,拒绝。 4. **offline/OCI 各写一套 verifier**:会使签名、manifest 与 digest 语义漂移,拒绝。 5. **常驻 watcher 自动热装**:绕过 durable Approved Action/install head,并破坏低配 设备空载预算,拒绝。 6. **为 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,全部 在既有预算内。 ## 后续门禁 1. publisher trust 的 current-signer impact、旧 key retirement、紧急 revoke、 rollback 与重签恢复; 2. 短生命周期 OCI fetcher 的 Registry credential rotation、rate limit、审计和 response-loss 恢复; 3. 固定低配 Linux 路由器的真实冷启动、idle RSS、闪存写入与断电恢复证据; 4. 外部 deployment/cutover controller、API host 与完整插件管理产品面。