# ADR-0120:S3-compatible 不可变 Remote Worker Artifact Store - 状态:Accepted(具体共享 store adapter 已实现,Worker execution composition 已由 ADR-0121 实现;range read、retention 与完整 Worker 产品生命周期仍默认关闭) - 日期:2026-07-23 - 关联 RFC:QL-RFC-0001 D-14、D-17、D-85、D-114、D-115、D-116、D-119 - 关联 ADR:ADR-0026、ADR-0027、ADR-0087、ADR-0115、ADR-0116、ADR-0117 ## 背景 ADR-0117 定义了跨副本可见、digest-authenticated、put-if-absent 的 Artifact store port,但没有 具体生产 adapter。直接对最终 key 做流式 `PutObject` 有一个难点:SHA-256 只有完整消费 body 后才 确定,不能在发送 header 前作为 immutable metadata 写入;若目标已经存在,服务端又可能在完整读取 body 前返回 precondition failure,调用方无法证明 replay 内容与既有对象一致。 controller 本地临时文件虽然能先算摘要再上传,却会让每个并发请求消耗最多 64 MiB Pod ephemeral storage,并重新引入“本地盘被误当成集群 authority”的风险。全量内存缓冲则直接破坏固定内存目标。 ## 决策 ### 1. 留在 cluster-control 的独立懒加载子入口 adapter 位于既有 `@qinglong/cluster-control/s3-artifact-store`,实现 ADR-0117 的 `ClusterRemoteWorkerArtifactStore`,依赖精确版本 `@aws-sdk/client-s3@3.1093.0`。不新增 workspace package、schema、数据库连接、timer、queue 或 socket ownership;S3 client 由外层 composition root 创建并持有。Edge/Standalone 闭包不得导入该 cluster-only subpath。 ### 2. 临时流式上传后做不可覆盖的 server-side promotion 首次写入顺序固定为: 1. `HEAD` 最终 opaque key;若已存在,完整消费并以固定内存计算 incoming SHA-256,只有 identity/length/digest/truncation 全等才返回 `already_stored`; 2. 将 body 一次流式 `PutObject` 到 CSPRNG 临时 key,声明 exact Content-Length、SHA-256 checksum、 `If-None-Match: *` 和服务端加密; 3. `HEAD + ChecksumMode=ENABLED` 复验临时对象的 length、media type、owner nonce 与 full-object SHA-256; 4. 用同 bucket server-side `CopyObject` 提升到最终 key,设置 `If-None-Match: *`、替换为 canonical metadata,并继续要求 SHA-256 checksum 和服务端加密; 5. 再次 `HEAD` 最终对象。只有 S3 checksum、metadata digest、length、identity hash 和 truncation 全部一致才返回 receipt; 6. 删除已证明由本次请求拥有的临时对象。删除失败只进入低敏 diagnostic,不能逆转已耐久 promotion。 Copy 的 409/412 竞争和 Put/Copy 成功但响应丢失都不能直接解释为失败或成功,只能通过最终 `inspect` 裁决。明确收到 Copy 成功才返回 `stored`;竞争或丢响应经证据恢复返回保守的 `already_stored`。永久对象从不被该 adapter 覆盖或删除。 ### 3. key 与 metadata 不暴露业务 identity 最终 key 是 domain-separated SHA-256 identity digest 的两级 shard,不包含 Project、Run、Attempt 或 log Artifact ID。metadata 保存各 identity 的独立 domain-separated digest、byte length、content SHA-256 和 `omitted|true|false` truncation,不保存原始业务 ID、Worker/Session、Lease token 或 callback capability。`inspect` 从调用方 authority 重新派生并逐项比较。 临时 key 使用 UUID,另生成独立 256-bit owner nonce digest。若临时 ID 极端碰撞,未证明 owner nonce 的请求不得删除已有临时对象。部署必须对 `temporary/` 配置有界 lifecycle 作为崩溃垃圾回收,并让 runtime credential 只能删除该前缀;永久 `objects/` 的 delete/overwrite 权限不授予运行时。 ### 4. 加密与 confused-deputy fence 必须显式 构造必须显式选择 SSE-S3 (`AES256`) 或 SSE-KMS,并在 KMS 模式给出 key ID;不提供明文 fallback。 AWS 部署可同时给出 12 位 `expectedBucketOwner`,它会传给 destination 与 Copy source 请求。若 S3-compatible 服务没有启用 KMS/SSE,启动配置或第一次写入必须失败,不能静默降级未加密对象。 ### 5. 生产启用仍由更外层门禁控制 本 ADR 交付具体共享 store,但不自动注册 Worker Artifact/completion routes,也不自动启动 Worker headless lifecycle。ADR-0121 已在 Worker 侧完成默认关闭的 production execution-plane composition; 最终 Cluster Worker ingress 与完整 Worker 产品部署仍必须同时持有独立 mTLS/`ql3w` listener、最小权限 PostgreSQL service、该 adapter、Secret provider、Session/credential lifecycle 与 shutdown ownership, 缺任一项继续 fail closed。 range read、retention、临时对象 lifecycle 的部署验证和本地 spool 删除仍是后续 Gate。 ## 被否决的替代方案 1. **controller 本地临时文件**:并发磁盘成本随 64 MiB body 放大,并容易被误认为共享 authority。 2. **全量内存缓冲**:使 controller RSS 与 Artifact 大小和并发数相乘。 3. **直接最终 Put 后再补 metadata/tag**:内容与 receipt 不是一个不可变 publication,崩溃可见半状态。 4. **只信任 ETag**:单段、multipart、加密和不同 S3 实现的 ETag 语义不等于 canonical SHA-256。 5. **冲突后不消费 replay body**:无法证明同 identity 的内容一致,也会破坏 ingress complete-consumption gate。 6. **删除任何同名临时 key**:CSPRNG 碰撞或错误注入时会删除其他请求 authority。 7. **未配置 SSE 时明文继续**:把安全属性变成部署偶然行为。 ## 验收证据 1. 11 个 adapter 单元测试覆盖首次写入、exact replay、不同内容、Copy race、Put/Copy 丢响应、 metadata/checksum 漂移、短读/超读、零字节、SSE-KMS、expected owner、cleanup diagnostic、临时碰撞 no-delete 与 pre-abort 零请求。 2. 真实 `minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e` (`RELEASE.2025-09-07T16-13-09Z`)启用临时 static KMS 后,通过首次写入、HEAD checksum、条件 Copy、 exact replay、不同内容拒绝和最终单对象集成验证。 3. 未配置 MinIO KMS 时 SSE-S3 明确返回 501,adapter fail closed;测试没有加入明文兼容开关。 4. 本切片完成时 workspace importer 保持 23、TypeScript source file 为 293;ADR-0121 后当前为 294, cluster/edge dependency audit 继续 `findings=[]`。 5. 默认 Cluster Control 与 `/production` 子入口的独立进程加载证据均不含 `@aws-sdk/*`,只有 `/s3-artifact-store` 显式子入口加载 SDK;本切片完成时六种本机制品再次通过真实 pack/offline install/import,最大为 2,457,770 bytes、409 files、72 loaded modules 与 12,517,376 bytes RSS delta,且均不含 SDK;ADR-0121 后复跑的当前最大 RSS delta 为 12,566,528 bytes。