# ADR-0261:显式加密的 Durable Plugin Package Prompt 输出 Artifact - 状态:Proposed - 日期:2026-08-02 - 关联:QL-RFC-0001 D-207/D-243/D-244、ADR-0026、ADR-0027、ADR-0159、 ADR-0160、ADR-0260、ADR-0262 ## 上下文 ADR-0260 默认只向首次 live caller 返回模型正文。Prompt plan、RunEvent、ModelInvocation、 admission/finalization receipt 和数据库镜像列全部 content-free;exact replay 不再次调用 provider,也不承诺找回正文。这个默认值适合低配路由设备,但 UI、MCP 或长时间任务有时需要在 调用断线、进程重启或 Cluster 主库晋升后继续读取结果。 现有 Artifact 不能直接改名复用:Tool Invocation Artifact 是 input/preview 双对象和审批 start barrier;Remote Worker Artifact 是日志流、对象存储 promotion 与 Worker ACK;LocalProcess Artifact 又依赖本地文件、range 与 truncation fact。Prompt 输出需要保存完整、最多 1 MiB 的 `GenerateResult`,并在 provider exactly-once 窗口修复 ModelInvocation completion。三者的原子性、 授权和 GC authority 均不同。 ## 决策 ### 1. 默认仍为 live-only,durable 必须显式 opt-in Prompt execution plan 后续只允许 `live_only` 或带精确 retention policy revision/digest 的 `durable_artifact`。默认 `live_only` 不解析 key provider、不构造 Artifact repository、不执行 Artifact find/put/read,也不增加数据库行、文件、对象存储请求或后台状态。 当前 plan contract 已加入上述显式 output intent。为保持 alpha 孵化期间已经落盘的 receipt 可重放, 旧 plan 缺少 `output` 字段时按 `live_only` 解释,但计算旧 plan digest 时仍保持字段缺失;新建 plan 始终写入显式 `output`。这是一条仅用于既有 alpha 数据的 digest compatibility 规则,不允许新调用方 省略 output intent。 同一 request ID 的 output mode 和 retention digest 必须进入 immutable plan digest。live-only 完成后不能通过 retry 补取 durable output;durable request 也不能降级成 live-only exact replay。 调用方若改变模式或 policy,得到稳定 conflict,而不是隐式复用旧费用或正文。 ### 2. Envelope、Reference 与端口留在现有 AI package 共享 contract 位于 `@qinglong/ai/plugin-package-prompt-output-artifact`,不新增 workspace package。首个 v1 已实现: - `qinglong/plugin-package-prompt-output-artifact@v1` AES-256-GCM envelope; - Project/Run/StepRun/ModelInvocation/User、provider/model、content digest、output bytes、key ID、 retention revision/digest/eligible time全部进入 AAD 和 Artifact digest; - Artifact ID 由 ModelInvocation ID 的 domain-separated SHA-256 确定性派生; - ciphertext 打开后必须重新规范化 `GenerateResult`,复验 provider/model、UTF-8 output bytes 与 content digest; - content-free reference 只含 opaque identity、digest、bytes、key 与 retention metadata; - immutable `put/find` repository、key provider 与 read authorizer ports; - 明文沿用 Model Gateway 的 1 MiB 上限,完整 JSON envelope 硬限 1.5 MiB;retention 只允许 1 小时至 365 天。 Edge/Standalone adapter 已留在既有 `@qinglong/ai` SQLite storage subpath,Cluster adapter 已留在既有 AI PostgreSQL storage subpath。它们共享 protocol/test contract,但不共享连接或部署 实现。单个领域文件、repository 或 migration 不构成新 package;只有未来出现独立对象存储重依赖、 进程/权限域或可替换部署单元时才重新评审边界。 ### 3. Artifact 必须先于成功 Completion,并由同一事务绑定 不能在 `gateway.generate()` 已写成功 Completion 并返回后再保存 Artifact:进程可能在两者之间 退出,exact replay 会正确阻止 provider,却永远失去正文。 durable 路径必须把以下事实放进同一个方言事务: 1. 读取并复验 ModelInvocation start、Prompt plan output intent 与 Project policy fence; 2. 以 active key 构造确定性 Artifact,执行 immutable put/exact replay; 3. 让 ModelInvocation completion 的 output reference 指向 Artifact reference digest; 4. 写 usage/quota/pricing settlement、StepRun completion 与 RunEvent; 5. commit 后才向 live caller返回正文和 reference。 SQLite 使用现有单 operation authority 的短 `BEGIN IMMEDIATE`;PostgreSQL 使用现有 AI schema 的短 `SERIALIZABLE` transaction。v1 不把最多 1 MiB 的密文转移到 Worker 本地文件或 S3,因为 那会在数据库 completion 与对象 promotion 之间引入第二个事务域。若未来 Cluster 改用对象存储, 必须先增加 durable staged→bound→published bridge、响应丢失裁决和 orphan GC,不能只替换 URI。 ### 4. exact replay、恢复和错误语义 - completion + matching Artifact:返回 content-free receipt/reference;只有再次通过 read policy、 key resolution 和 retention 状态后才可打开正文,provider 调用数保持不变; - Artifact + incomplete completion:同事务复验 Artifact 后补 completion,再 finalization;绝不调 provider; - start 已存在但 Artifact/completion 都缺失:沿用 in-progress/outcome-unknown,不能假定 provider 未执行; - completion 声称 durable 但 Artifact 缺失、digest/key/identity 漂移:corrupt/unavailable,不能 回退 live-only 或再次调用 provider; - 同 Artifact ID 不同规范化内容:conflict;相同内容:exact replay。 该顺序把 crash window 收敛为“全回滚”或“Artifact 与 completion 同时 durable”。SIGKILL fixture 必须覆盖加密前、Artifact insert 后、completion/settlement/StepRun/Event 后和 COMMIT response loss; 物理断电仍需独立设备证据,不能由进程 crash 冒充。 ### 5. 读取授权、Retention 与 GC 是独立 authority 执行 runtime 只有 insert/select,不取得 delete 或任意跨 Project read。产品读取只接受 subject + Project + Run + opaque Artifact identity,不接受 key、路径、URI 或 ciphertext;固定顺序为 metadata identity → `artifact.read` Project Policy → retention/tombstone → key resolve/decrypt。deny、 require-approval 与 not-found 对非可信 transport 默认屏蔽存在性。 Artifact 保存创建时 policy revision/digest 和 eligible time,但这不是自行删除授权。GC 由显式、 短生命周期 maintenance authority 执行 bounded keyset page:重新验证 retention policy、终态 Prompt/Run,并在同一写事务中先写 content-free immutable tombstone、再按精确 Artifact digest 删除 密文。v1 不增加读取租约:repository 单次读取取得完整 immutable row,授权、retention inspection 与 解密只处理该次调用已持有的内存副本;GC 若在 retention inspection 后提交,不得使已经开始的读取在 同一进程内凭空失效。读取在 inspection 前观察到 tombstone 则只返回 `retained`,不会重算正文。 这条线性化规则避免为每次读增加写事务、租约表、续租 timer 与 WAL。 Edge 不增加 timer;由 Owner CLI 或现有单 maintenance cadence 显式触发。Cluster 使用独立 `ql3_ai_maintenance` 短生命周期 role/Job,不把 DELETE 授给 control runtime。GC 对每个候选重新读取 精确 row、终态与 policy revision/digest,使用方言内互斥并把 tombstone+DELETE 原子提交;崩溃只能 留下“密文仍在”或“tombstone 已在且密文已删”两种状态,不能先删后丢失 retained 证据。 key rotation 允许新写使用 active key、旧读按 key ID resolve。key retirement 现固定为“不改写 immutable Artifact”的两阶段协议:先把旧 key 切为 inactive,再以方言内事务追加 preparation fence、 在同一 key advisory/SQLite write authority 下证明 live ciphertext 为零,外部删除 key material 后追加 completion;崩溃后由 preparation、material absence 与确定性 absence proof 恢复。共享 coordinator、双方言 repository/migration 与 PostgreSQL HA 已实现。Edge/Standalone 已有私有 POSIX file-keyring material authority:限定当前 UID、目录私有、文件 `0600`、非 symlink、有界 generation/key/retirement,并以 CAS + atomic rename + directory fsync 完成 rotation/retirement;同一 preparation 的删除响应丢失可由 持久 absence proof 精确恢复。既有 `ql3-owner-gc` 已提供一次性 `owner.prompt-output-key.retire` 命令; Cluster Admin 的注入式 one-shot retirement process 已由 ADR-0262 接入固定 UID/resourceVersion 的 Kubernetes Secret material authority、command-file-only CLI 与 opt-in Job/RBAC/NetworkPolicy。这个 adapter 只关闭 Kubernetes Secret 上的单对象 retirement CAS,不等同于 KMS/HSM;运行时同源读取已由 独立只读投影组件闭环,首次 provision/active rotation 管理面、外部 wrapping/non-exportable key、 遗失 key 人工恢复仍须闭环。read service、双方言 GC adapter 与 storage authority 已实现;Local Owner maintenance 命令使用 private durable command file 携带最多 128 条 Project+revision+digest 绑定的 策略目录,每次只执行一个 bounded page。Cluster 使用同一目录协议和独立 maintenance role 的显式 one-shot Job;GC 完成仍不能等同于所有部署的 key 生命周期已经完成。 ## 低配与 Cluster 影响 - live-only 路径的目标增量是 0 Artifact I/O、0 常驻资源、0 package; - durable 单请求最多额外持有一个 bounded result JSON、ciphertext 和数据库事务;128/256 MiB arm64 门现以 512 KiB 输出分别运行 Edge DELETE/FULL 与 Standalone WAL/FULL 产品纵切面,并硬校验 peak RSS、SQLite logical/allocated growth 与 WAL 写放大; - SQLite 写入大 envelope 只对明确 opt-in 请求发生,不能成为默认日志策略; - PostgreSQL 密文会进入 WAL/HA replica,因此需要独立 9009/9010/9011 migration、ACL、角色与 timeline promotion/rewind 门。`pg-9009-ai-plugin-package-prompt-output-artifacts` 和 `pg-9010-ai-plugin-package-prompt-output-tombstones`、 `pg-9011-ai-plugin-package-prompt-output-key-retirements` 已在 PostgreSQL 18.4 arm64 physical-streaming HA 门完成 `remote_apply`、timeline 1→2、旧主 fencing/rewind、fresh control replicas、GC 后 exact replay 与 promotion 后 schema/ACL 复验;用户现有 evidence control-plane 容器未被修改。 ## 当前实现与接受门 当前已完成以下显式 opt-in 的写入闭环: - 共享 envelope/reference/key/read-authorizer contract,以及加密 round-trip、content-free reference、 tamper fail-closed 和 retention/key budget 四项定向测试; - plan 的显式 `live_only|durable_artifact` intent、retention digest binding 与旧 alpha plan digest compatibility; - SQLite `9010-ai-plugin-package-prompt-output-artifacts` 与 PostgreSQL `pg-9009-ai-plugin-package-prompt-output-artifacts` migration; - 两方言 immutable put/find/exact-replay repository。repository 会复验 admission plan、 Project/Run/StepRun/Invocation/requester/provider/model 和 retention mirror,live-only plan 在 insert 前 fail closed; - SQLite `BEGIN IMMEDIATE` 与 PostgreSQL `SERIALIZABLE` repository 已把 Artifact、 ModelInvocation completion/usage/quota/pricing settlement、StepRun/Event output reference 放入同一 方言事务;已有 Artifact 可修复缺失 completion,已有 completion 但 Artifact 缺失或漂移会失败关闭; - Model Gateway 只在部署组合显式提供同一个 successful-completion capability 时启用 durable hook; Prompt executor 会做 capability identity handshake,缺少任一 key/repository/hook 时都在 admission/provider 前失败关闭; - Local application 与 Cluster Prompt application 均只在显式注入 `promptOutputKeys` 后装配该能力; live-only 路径不读取 active key,也不增加第二个 provider registry、连接、timer、watcher 或 package; - Cluster Prompt route 已以 exact body 开放 `live_only|durable_artifact`,严格校验 1 小时至 365 天的 retention policy,并只返回 content-free Artifact reference; - PostgreSQL runtime 仅有 artifact table `SELECT/INSERT`,无 `UPDATE/DELETE`,其余管理/执行/Worker roles 无权访问;该边界已通过 physical-streaming HA promotion/rewind; - Local/Cluster 产品读取 service 已固定执行 metadata identity → Project Policy → retention/tombstone → key/decrypt,使用 opaque Artifact identity、屏蔽 forbidden/not-found,并对响应施加硬上限; - SQLite tombstone migration 与 PostgreSQL `pg-9010` 均只保存 identity/digest/policy/删除时间等 content-free 事实;双方言 GC 都是 caller-driven bounded page,逐项复验终态与 policy 漂移,并在 同一事务内先插入 tombstone、再删除精确密文; - PostgreSQL 新增非继承、短连接的 `ql3_ai_maintenance` 角色。runtime 只能读 tombstone,maintenance 只能读取裁决所需事实、插入 tombstone 和删除 Artifact,不能取得 runtime/admin authority; - key retirement preparation/completion 是 content-free append-only 事实;SQLite Artifact insert 与 preparation 共用 `BEGIN IMMEDIATE`,PostgreSQL 两条路径共用 key-scoped advisory transaction lock, 因而 preparation 一旦提交,迟到/重放的旧 key Artifact 就不能重新落库;completion 仍会复验 live ciphertext 为零,且精确重放保留首次时间戳; - `@qinglong/ai/plugin-package-prompt-output-file-keyring` 实现 Edge/Standalone 的 bounded 私有文件 material authority;active key 不可退役,inactive material 的 catalog/material proof 必须与 durable preparation 完全一致,删除后保存 content-free preparation、retired catalog digest 与确定性 absence proof。死进程 lock 可回收,不增加 timer、watcher、daemon 或 workspace package; - 既有 `ql3-owner-gc` 新增 `owner.prompt-output.collect`,只从 mode 0600 private command file 读取 bounded digest-bound policy catalog,一次执行一页并只输出 scanned/tombstoned/skipped/hasMore; 非该命令分支不加载 AI 模块,未新增 package、daemon、timer 或 watcher; - 同一个 `ql3-owner-gc` 又新增 `owner.prompt-output-key.retire`,命令文件只携带 key/retirement/request/ mutation identity,返回只含状态、identity、prepare/complete digest 与数据库时间;真实 SQLite feature、 file-keyring 删除和 exact replay 已贯通。Cluster Admin 的 `prompt-output-key-retirement-process` 使用 `ql3_ai_maintenance` 短连接、启动时复验 retirement 表的 append-only ACL,并只接受显式注入的 material authority;它不把 Kubernetes Secret 删除伪装为 KMS 一致性,也不让常驻 control plane 获得该权限; - `@qinglong/cluster-admin/prompt-output-kubernetes-secret-keyring` 只对固定 namespace/name/UID/data key 的 mutable Opaque Secret 执行 get/update `resourceVersion` CAS;拒绝 list/watch/create/delete/patch、 Secret 重建、active key、last-applied material 副本和 annotation/manifest 漂移。响应丢失与并发相同 retirement 通过 winner 重读收敛; - `ql3-prompt-output-key-retire` 在 material 读取前用 SelfSubjectAccessReview 证明 exact Secret get/update 且扩权矩阵被拒绝,再打开 `ql3_ai_maintenance` 短连接。对应 opt-in Job 为 `backoffLimit=0`、非 root、只读根、128 MiB;Role 只绑定单 Secret,base 默认仅 DNS egress, CloudNativePG overlay 仅再放行数据库 5432,Kubernetes API `/32 + port` 必须由部署私有 overlay 提供; - `cluster-ai-prompt-output` Kustomize Component 现在把同一个 `ql3-prompt-output-keyring/keyring.json` 以 required、单文件、`0440`、read-only volume 装配到既有 Cluster AI runtime,并只注入已有 projected-keyring adapter 的两个环境变量。默认 Cluster 和默认 `cluster-ai` 仍保持 live-only;该组件不创建 Secret、不挂载 ServiceAccount token、不授予 RBAC,也不 新增 package、进程、listener、watcher 或 timer; - 三节点 K3s v1.34.3/Flannel arm64 实跑已证明真实 Kubernetes atomic-writer symlink、Secret generation 1→2 与 revision 变化会被同一 Pod/同一进程重新打开。新 active key 生效后,历史 key 仍能解开轮换前 创建的真实 encrypted Artifact;运行时无 Kubernetes 凭据,投影仍为 exact `keyring.json`/`0440`/ read-only。轮换窗口中真实命中的中间代竞态返回 `PLUGIN_PACKAGE_PROMPT_OUTPUT_PROJECTED_KEYRING_UNAVAILABLE`,没有读取混合代数据;正式门允许该 fail-closed 操作重试后再验证稳定新代; - 既有 Cluster Admin image 增加同协议的 `ql3-prompt-output-gc` 一次性 CLI;Kubernetes Job `backoffLimit=0`、无 API token、只读 root、128 MiB limit,NetworkPolicy 只允许 DNS 与 CloudNativePG 5432,policy ConfigMap immutable 且故意不进入默认 Kustomization; - SQLite 故障注入已证明 Artifact insert 之后、usage ledger 之前失败会整事务回滚,Run/StepRun 不会 冒充成功;recovery 将已调用 provider 但无终态的 invocation 收敛为 `outcome_unknown`; - PostgreSQL 18.4 arm64 HA 门已用真实 durable Prompt execution 证明 GC 前 Artifact 为 1、GC 后为 0 且 tombstone 为 1,StepRun output reference 与 Artifact ID 一致,GC 后 exact replay 仍不再次调用 provider;随后旧 key preparation/material deletion/completion 精确重放、迟到密文拒绝、同步复制和 promotion 后逐字段一致也已通过。Prompt/输出明文不进入 durable JSON;原子 Artifact、 tombstone-before-delete、GC 后 replay、key retirement durable fence、maintenance least-privilege 专用 gate 与总 `gates.passed` 均为 true。 - Node 24.18.0 arm64 的 128 MiB/0.5 CPU/64 PID 压力门和 256 MiB/1 CPU/128 PID release 门均在 零 swap、非 root、只读 root/workspace、seccomp、NoNewPrivs 下通过,且零 max/OOM。512 KiB durable output 的 Edge DELETE/FULL process peak RSS 为 103,616,512/104,517,632 bytes,SQLite logical/allocated 写放大均为 1.383×;Standalone WAL/FULL peak 为 102,330,368/97,800,192 bytes,logical/allocated/WAL 写放大为 2.185×/2.188×/2.185×。 两档都同时证明 provider 两次(live 与 durable 各一次)、各自 exact replay 零重调、live-only 零 key load、durable key load/resolve 各一次、零 RunAttempt 与 durable bytes 无明文;门禁会对任一证据 漂移失败关闭。这是 tmpfs/cgroup CI 写入放大,不是物理闪存 FTL 或最低配置承诺。 AI suite 当前为 142 项、139 通过、3 项仅因未提供外部 PostgreSQL 环境而条件跳过;Local application 为 42 项、39 通过、3 项平台条件跳过,Local Owner maintenance 为 13/13;Cluster control 为 172 项、170 通过、2 项外部服务条件跳过,Cluster Admin 为 206 项、204 通过、2 项 外部服务条件跳过;Cluster PostgreSQL 为 272 项、271 通过、1 项真库条件跳过。本切片的 package closure/dependency/SBOM/deployment/DR 组合门禁为 75/75,独立依赖审计为 `compatible: true`。 workspace 仍为 19 个 QL3 package(pnpm 输出的 20 个 workspace project 包含根项目),没有为本 能力新增 package。 接受前必须完成: 1. [x] output intent 已进入 plan digest、双方言 admission mirror 与 Cluster route strict body; 2. [ ] SQLite 同事务 Artifact + completion、exact replay、事务中段失败回滚与 20 点外层 SIGKILL phase matrix 已完成;仍需物理断电证据; 3. [x] PostgreSQL migration、最小 ACL、同事务产品 repository、exact replay 与 physical-streaming HA promotion/rewind; 4. [x] Local/Cluster 已显式注入 key/repository/completion capability,disabled/live-only 零加载; 产品读取组合已接入 read authorizer、存在性屏蔽与 bounded response; 5. [x] read service、双方言 retention tombstone/GC storage authority、PostgreSQL 最小权限角色、 digest-bound policy catalog、Local Owner maintenance CLI 与 Cluster one-shot Job; 6. [x] 128/256 MiB arm64 durable-output resource/write-amplification gate; 7. [ ] key retirement 的共享状态机、双方言 durable fence/repository、POSIX file-keyring authority、 Local Owner 产品命令、Cluster Kubernetes Secret adapter/CLI/Job 与 PostgreSQL HA 已完成;仍需 首次 provision/active rotation 的受审管理面、具体 KMS/HSM backend 与遗失 key 人工恢复。Cluster 运行时同源 read-only projection、同进程 active rotation/historical decrypt 和 retirement 的真实 Kubernetes API 纵切面均已完成; 8. [ ] 固定物理 Edge 存储/闪存写放大与断电证据。 ## 被拒绝方案 1. **新建 `ql3-prompt-artifact` package**:没有独立部署或重依赖价值,违反 D-207。 2. **复用 Tool Invocation 双 Artifact 表**:input/preview、审批和 action identity 均不相容。 3. **复用 Worker S3 log store**:会引入第二事务域和 cluster-only SDK,扩大 Edge closure。 4. **Gateway 返回后异步保存**:completion→Artifact crash window会永久丢正文。 5. **把正文写入 completion/finalization receipt**:扩大热表、审计、备份和 HA 泄漏面。 6. **exact replay 再调 provider**:重复计费且破坏 exactly-once。 7. **runtime 自带删除 timer/DELETE**:混淆执行与数据生命周期 authority,增加低配稳态成本。