# ADR-0220:Plugin Package 当前安装清单查询 - 状态:Accepted - 日期:2026-07-29 - 关联 RFC:QL-RFC-0001 D-140、D-141、D-142、D-174、D-176、D-175、D-207、D-210 - 关联 ADR:ADR-0142、ADR-0143、ADR-0144、ADR-0184、ADR-0186、ADR-0217 ## 背景 QingLong 3.0 的 Plugin Package 已有以下写入和恢复链路: | 能力 | 当前状态 | | --- | --- | | install / reinstall / upgrade / rollback | 已实现 | | proposal / approval / consume / dispatch | 已实现 | | staged / activating / failed recovery | 已实现 | | publisher revoke / quarantine / capability withdrawal | 已实现 | | 精确查询当前安装 | D-210 前缺失 | | 分页查询 Project 当前安装清单 | D-210 前缺失 | | disable / uninstall | 未定义,后续独立状态机 | 管理 CLI 和 Cluster transport 虽然能检查 proposal/approval,却不能回答“某个 Project 当前装了什么、哪个版本可用、是否已经因 publisher 撤销而隔离”。部署者只能直接查 SQLite/PostgreSQL;这既不是稳定产品 API,也会暴露内部 schema 和过宽数据库权限。 此外,安全隔离采用 append-only overlay,而不是改写 install history。一个 install record 可以仍是 `active`,但对应 lock 已有 quarantine event 和 withdrawal receipt。 仅回显 `install.state` 会把已撤回能力误报为可用。 ## 决策 ### 1. 独立只读 port,不扩大 mutation authority `@qinglong/runtime-core/plugin-package-install` 增加 `PluginPackageInstallInventoryRepository`: - `findCurrent(projectId, packageName)`; - `listCurrentPage({ projectId, limit, after? })`。 该接口与既有 `PluginPackageInstallRepository` 分离。查询 composition 只依赖 inventory port,不因复用同一个具体 adapter 而取得 admit/stage/activate/fail 等写能力。 清单只读取 current head,不返回历史 revision;顺序固定为 Package name 升序,游标仅含 最后一项 Package name。core 与 Cluster 页大小为 `1..64`,使用 `limit + 1` 判定 `truncated`,禁止 offset、任意排序、模糊查询和无界数组。 ### 2. 查询结果必须感知 quarantine SQLite 和 PostgreSQL 使用相同语义: 1. 从 `(project_id, package_name)` current head 取得精确 install record; 2. 只连接同时匹配 Project、Package、installation、lock 和 install record digest 的 quarantine event; 3. quarantine event 必须存在对应 withdrawal receipt; 4. event、receipt 与 record 的 canonical JSON、digest 和 target binding 必须重新规范化 并完全一致。 无隔离事实时返回 `quarantine: null`。event/receipt 只有一侧存在、JSON 损坏、target 漂移或出现多个 current 结果时失败关闭,不降级成“未隔离”。 产品层派生: - 存在完整 quarantine fact:`availability=quarantined`; - 无 quarantine 且 install state 为 `active`:`availability=active`; - 其他 install state:`availability=not_active`。 因此安全 availability 不由可变缓存或 install enum 推断。 ### 3. Local 产品入口与低配预算 既有 `ql3-package` 增加: - `plugin-package.installation.inspect`; - `plugin-package.installation.list`。 命令继续使用 Owner credential、pepper、私有命令文件和 Project Policy 认证。认证成功后 才动态加载 SQLite inventory adapter;只读命令不构造 proposal、approval、dispatcher 或 Package executor service。 Profile 预算固定为: | Profile | 默认页 | 最大页 | | --- | ---: | ---: | | Edge / 路由设备 | 8 | 16 | | Standalone | 32 | 64 | 每次仍是一次命令、一个短进程、一个 SQLite authority;不增加后台 timer、watcher、 listener、缓存或常驻连接。 ### 4. Cluster 管理入口 Cluster 复用现有独立 `cluster-admin` management process,不接入常驻 `cluster-control`: - 只接受受信 transport 注入的 active `multi_factor|hardware` User principal; - 使用目标 Project 的 `package.manage`; - 复用 durable `plugin-package.inspect` quota; - 由 package-manager PostgreSQL authority 执行只读 exact/keyset 查询; - 页大小硬限 64。 Cluster client 不只校验 exact JSON shape,还校验响应 Project、Package identity、 Package name 严格递增、请求游标之后的顺序、`next` 必须等于末项,以及 availability/quarantine/failure 事实完整性。远端不能用格式正确但语义漂移的响应欺骗 operator。 ### 5. 低敏产品摘要 Local/Cluster 只返回: - installation / Project / Package / version / operation; - state、generation、active/previous lock digest、recovery action; - failure 摘要; - derived availability; - quarantine reason/mode/event/time 与 withdrawal status/receipt/time; - record version/time/digest。 不返回 source locator、完整 manifest、approval/stage reference、authentication ID、 credential、数据库 DSN 或内部命令内容。 ### 6. 不在本决策实现 disable / uninstall 正常停用和卸载不是查询,也不能等同于安全 quarantine: - 必须定义 Task、Tool、Workflow、Prompt 与 Secret binding 的撤出顺序; - 必须处理 durable running、completion/recovery 与新 start barrier; - 必须保留安装、审批、执行和审计历史; - 必须检查跨 Package/Workflow 引用; - uninstall 必须区分逻辑退役、物理制品回收和失败恢复。 这些语义需要独立、可重放、双方言原子的状态机 RFC。D-210 不删除任何安装或历史, 也不把 quarantine 当作用户可选的 disable。 ## 不采用方案 ### 直接暴露数据库表 拒绝。会把 schema 变成公共 API,绕过认证、Project fence、配额、边界校验和脱敏。 ### 把查询方法加入管理 mutation service 拒绝。会迫使只读入口装配 proposal、approval、dispatcher 和 executor authority。 ### 全量或 offset 分页 拒绝。并发安装时会重复/遗漏,且无法给路由设备提供确定内存上限。 ### 新建 inventory package 或常驻同步 daemon 拒绝。没有独立部署或依赖隔离价值,违反 D-175/D-207,并增加低配设备安装、SBOM、 空闲 RSS 和维护成本。 ### 顺便删除、disable 或复用 quarantine 拒绝。删除破坏历史;quarantine 表达 publisher/lock 安全撤回,不表达用户正常退役。 ## 影响 - workspace 保持 20 个 package; - 不新增 migration、第三方依赖、进程、端口或后台资源; - mutation repository contract 和既有写路径不变; - Local 与 Cluster 获得一致的 current inventory 语义; - Cluster transport public operation 从 9 个增加为 11 个; - disable/uninstall 明确列为后续独立状态机,而不是缺省删除行为。 ## 验证 1. GitNexus impact:SQLite repository 为 HIGH(24 upstream、4 direct),PostgreSQL repository 为 HIGH(4 upstream、2 direct);transport normalizer 为 HIGH(6 upstream、2 direct);新增服务/transport/client/HA 场景入口为 LOW。实现只增加只读 方法并保持既有 mutation 语义不变; 2. runtime-core page/cursor、SQLite exact/list、双方言共享 repository contract、 Local 完整 Package 生命周期、Cluster service/transport/client 定向回归通过; 3. Local 生命周期在 publisher revoke 后证明 install record 仍为 active,但 inspect/list 均输出 `availability=quarantined`、精确 reason 和 withdrawal status,且不泄露敏感 material; 4. Cluster 三包 TypeScript 闭包通过;`cluster-admin` 全包 132 pass、1 条真实 Kubernetes 条件 skip,TLS client 6/6,覆盖 11 个公开 operation、跨 Project、 重复/乱序 keyset、错误 next cursor 与不完整 quarantine 响应拒绝; 5. PostgreSQL 18.4 arm64 physical HA 使用公开 `PostgresPluginPackageInstallRepository` 在隔离后和 timeline 1→2 提升后复验 current exact/list,event/receipt 完全一致; 6. HA 输出 `pluginPackageQuarantineInventorySurvivesPromotion=true`,36 个具体 gate 与总 `passed=true`;临时依赖 link 由 trap 清理,Docker 资源由 HA fixture 清理。 当前未完成证据是六个 Local Profile 的正式 bytes/files/RSS artifact matrix:它仍由 exact local-sqlite Drizzle RC 未物化阻断。该限制不影响 D-210 的 Cluster HA 结论,也 不能用从受审本机镜像临时提取的依赖替代正式 lockfile 安装与远端 CI。