Files
qinglong/docs/adr/ADR-0024-local-context-secret-and-artifact-boundary.md
T

8.0 KiB
Raw Blame History

ADR-0024Local Context、Secret 与 Artifact 边界

  • 状态:Proposed
  • 日期:2026-07-18
  • 关联:QL-RFC-0001、ADR-0003、ADR-0007、ADR-0022、ADR-0023

上下文

不可变 Task revision 只能保存执行模板和 opaque contextRef。真正启动 Attempt 时还需要环境、Secret、stdout/stderr Artifact 和 completion capability。这些数据生命周期不同:public 环境和 Secret 引用可以随 revision 保留,Secret 明文只能短暂存在于可信内存,Artifact 必须与 Attempt 绑定并在控制面重启后仍可定位。

QingLong 同时运行在小型路由设备和 cluster 节点。edge 不能为每个任务常驻队列、sidecar、Collector 或对象存储,但也不能继续把日志路径、Secret 和当前 Crontab 状态临时拼接后直接 spawn。cluster 则不能复用本机文件和 SQLite 作为共享能力。

决策

1. Context recipe 只保存 public 值和 Secret 引用

LocalExecutionContextRecipe 是按环境变量名排序的 canonical bindings

  • public binding 保存明确可持久化的普通字符串;
  • secret binding 只保存 bounded secretRef,不保存解析值;
  • 环境名唯一,最多 256 项,并继续受 ExecutionContext 的单值 64 KiB、总计 256 KiB 边界约束;
  • contextReflocalctx:sha256:<digest> 内容地址,digest 只覆盖 canonical bindings。

0013-local-execution-context-recipescontext_ref 为主键,保存 canonical recipe、独立 digest 和首次创建时间。Repository 是 append-only:同内容重放幂等,不提供 update/delete;非法 JSON、非 canonical 顺序、非内容地址或 digest 损坏均 fail closed。

recipe 不包含 Secret 明文、Run/Attempt identity、callback token、Worker lease、PID/handle 或日志路径。

2. 发布顺序阻止可执行的悬空引用

本地发布器固定先插入 context recipe,再插入引用它的 Task execution revision,并要求两个 contextRef 完全相同:

  • recipe 写入失败时绝不创建 revision;
  • 两次 append-only 写之间断电最多留下不可执行的孤儿 recipe;
  • 不会留下已经可调度但 recipe 不存在的 revision
  • 并发重放由两个 repository 各自收敛为 inserted/idempotent。

当前没有通用删除接口。未来清理孤儿 recipe 或 revision 前,必须建立引用证明、保留窗口和备份门禁。

3. Secret provider 只在 Attempt 物化时批量解析

materializer 先精确读取 contextRef,再把去重后的 Secret refs 与冻结 candidate 交给可信 provider。provider 返回与 refs 位置一一对应的内存值:

  • recipe 或任一 Secret unavailable 时返回 plan unavailable
  • 缺失或非法结果在 Artifact 分配和 Executor 副作用前拒绝;
  • 明文不写回 recipe、revision、RunEvent、普通日志或异常消息;
  • 解析后的环境立即通过共享大小/NUL/prototype 边界规范化并冻结。

ADR-0025 已进一步提供 production-unreachable 的 AES-256-GCM envelope、append-only SQLite repository、私有文件 keyring 和批量 environment provider,并且没有把 Legacy Envs 表直接宣称为 3.0 SecretStore。权限/审计/API、自动 key 生成与备份、历史 rekey/退役证明和 cluster Secret provider 仍需独立实现。

4. 每个 Attempt 获得一个 opaque 本地 Artifact

本地文件 allocator 从 (runId, attemptId) 派生 36 字符 opaque Artifact ID,不在 ID 或日志路径暴露 Task 名、命令或 Secret。文件位于受控 root 的固定 shard,目录强制 0700、文件强制 0600append 打开使用 O_NOFOLLOWsymlink target fail closed。

同一 Attempt 重放得到相同 ID 并追加同一文件,不同 Attempt 必须得到不同 ID。普通 pipe 模式下 output sink 串行已接受写入;durable LocalProcess 模式通过非枚举 adapter capability 把绝对文件路径和 receipt root 交给 launcher,常规 context 序列化看不到路径。

Artifact ID 由 local Dispatcher 随 claimed activation 传入,并在 spawn 前的 attempt.starting 事务中持久化。非法/path-like ID 在 Run 状态变化前拒绝;已有 claimed Attempt 若夹带 Artifact identity 被视为 stale authority。这样 Reconciler、日志 API 和 retention 不必从文件名或进程推测所有权。

5. 清理只释放 capability,不提前删除历史日志

plan 校验、激活竞争或启动失败时会等待可选异步 dispose;成功激活后在 completion settle 后释放。dispose 关闭当前进程持有的文件 descriptor,并等待已经接受的写入,不删除 Artifact 内容。

删除策略必须独立依据 terminal Attempt、receipt 清理、保留期、容量水位和 revision fencing。ADR-0026 已提供 production-unreachable 的单 Attempt hard quota、statfs admission reserve、压力 retention、SQLite tombstone 和安全文件 retirement;生产启用前仍必须完成 lifecycle/cursor、容量告警、durable truncation fact 与真实 edge ENOSPC/inode/断电测试。

6. edge 与 cluster 共享语义,不共享存储实现

  • edge/standaloneSQLite immutable recipe + 本地 Secret provider + 私有文件 Artifact
  • cluster-controlPostgreSQL recipe/revision metadata + 集中 Secret provider + 对象 Artifact
  • worker:只接收已授权的 Attempt-scoped capability,不读取控制面的 SQLite 或 Secret 表。

cluster adapter 必须复用 canonical digest vectors、exact resolve、batch Secret、Attempt Artifact identity 和 fail-closed 规则。共享挂载 database.sqlite 或本机 Artifact root 不是 cluster 实现。

影响

正面影响:

  • 自动重试能重建同一 public/Secret 引用意图,同时为 Attempt N+1 分配新 Artifact
  • Secret 明文与静态 revision、recipe、日志 metadata 分离;
  • edge 每个 revision/recipe 各一次单行写,每个 Attempt 只持有有界数量的文件 descriptor;
  • Artifact ownership 在 spawn 前持久化,重启恢复不依赖内存映射。

代价与风险:

  • 需要把 ADR-0025 的本地 SecretStore 补齐权限、审计、key lifecycle 和轮换产品入口后才能生产接入;
  • 本地日志仍需要把 ADR-0026 adapters 接入受控 lifecycle,并完成 ENOSPC/告警/截断事实的运维闭环;
  • 两步发布可能留下孤儿 recipe,必须用引用感知维护任务清理,不能盲删;
  • context 环境仍驻留进程内存,诊断和 heap dump 策略必须避免泄漏。

未选择的方案

  1. 把 Secret 明文写入 recipe/revision:静态泄漏与备份扩散,拒绝。
  2. 启动时读取当前全局 Envs 并全部继承:不可复现且权限过宽,拒绝。
  3. 先写 revision、后写 recipe:断电会留下可调度悬空引用,拒绝。
  4. 日志文件名使用 Task 名或命令:泄漏且存在路径边界风险,拒绝。
  5. Artifact ID 只存在内存:重启后无法证明日志属于哪个 Attempt,拒绝。
  6. plan dispose 删除日志:激活竞争与 completion 时机无法证明 retention,拒绝。
  7. cluster 复用本地文件/SQLite adapter:多副本一致性和权限模型错误,拒绝。

验证要求

  • recipe canonical 顺序、内容地址和 digest 有固定 vectors
  • recipe 并发同内容写入恰好一次 inserted,其余 idempotent
  • 非 canonical、任意 ref、非法 JSON 和 digest 损坏 fail closed
  • recipe 失败时 revision repository 不被调用;
  • Secret refs 去重批量解析,missing/错位/越界值在 Artifact 前拒绝;
  • 本地 Secret current/exact 版本、CAS/mutation 幂等、密文损坏和跨 Project fail-closed 通过 ADR-0025 contract
  • Artifact ID 对同 Attempt 稳定、跨 Attempt 不同且长度有界;
  • 目录/文件权限、symlink、late write、异步 dispose 和 replay append 有真实文件测试;
  • Artifact ID 在 spawn 前持久化,invalid ID 不改变 queued Run
  • manual Primary 未传 Artifact ID 时行为不变;
  • SQLite adapters 拒绝 cluster dialect,生产启动链保持不可达;
  • Node 22、Node 24 全量回归通过。