Skip to content

feat(sandbox):持久化 Docker 容器生命周期所有权、代际租约与崩溃恢复(#38) - #58

Merged
Qiyuanqiii merged 2 commits into
mainfrom
codex/issue-38-docker-lifecycle-recovery
Aug 14, 2026
Merged

Qiyuanqiii merged 2 commits into
mainfrom
codex/issue-38-docker-lifecycle-recovery

Conversation

@Qiyuanqiii

@Qiyuanqiii Qiyuanqiii commented Aug 14, 2026 •

Copy link
Copy Markdown
Member

背景

Issue #38 要求把 Docker 容器从一次性、进程内的生命周期探针,提升为可持久化、可抢占、可恢复且能够拒绝陈旧写入的生命周期协调机制。

此前的实现虽然已经具备固定本地端点、确定性容器名、严格配置校验和基础清理逻辑,但仍存在几个根本缺口:

  • Docker create/start/TERM/KILL/delete 可能先发生,持久状态只能在整个流程结束后一次性返回,崩溃后无法区分“尚未调用”“调用结果未知”和“已生效但未提交”。
  • LeaseGeneration 只参与请求指纹,没有在每次 Docker 写入前校验当前租约的完整身份与有效期。
  • 原有六标签只绑定 Run/authority/spec 等基础计划,不能把实际容器精确绑定到 lifecycle attempt、resource generation 和 launch intent。
  • 单体 Run 无法从已经 created、running、exited 或 cleaning 的中间状态继续收敛,也无法安全处理 supervisor 竞争和 takeover。
  • v56 的 rehearsal aggregate 明确要求容器从未启动,不能被扩展为真实 started lifecycle。

本次改动

1. 新增独立的 SQLite schema v97 生命周期聚合

新增不可变且非授权的 Docker lifecycle ledger,包括:

  • launch intent:绑定 Mission、Run、Workspace、Attempt、Plan、endpoint、request/spec/authority/label/name 等指纹;
  • generation lease:使用完整的 lease_id + owner_id + lease_generation + resource_generation + expiry 做 fencing;
  • prepared actions:在每次有副作用的 Docker 动作之前写入不可变动作意图;
  • transitions:以追加式、带前序指纹的状态链记录 created -> started -> exited/cleaning -> cleaned,并保留显式 failure facts;
  • cleanup receipt:每个 lifecycle intent 最多一个不可变清理收据。

迁移中的 CHECK、UNIQUE 与 trigger 共同约束:

  • 当前租约身份和有效期;
  • action/transition ordinal 与状态迁移顺序;
  • transition 指纹链;
  • cleaning 原因、cleaned 终态和 receipt outcome 的一致性;
  • append-only/immutable 行为;
  • transition/receipt 与对应 Run event 的事务原子性;
  • 在“已写 cleaned、尚未写 receipt”时崩溃后,允许新一代合法租约补写唯一收据,同时拒绝旧租约或不匹配证据。

v56 rehearsal 表及其“容器从未启动”语义保持不变。

2. 为实际 Docker 生命周期拆分可检查点操作

在现有固定端点 HTTP transport 上增加可恢复的窄操作:

  • Observe
  • Start
  • Wait
  • Terminate
  • Cleanup

CREATE/START/WAIT/TERM/KILL/DELETE 都会在发出 daemon 请求前调用逐动作 fence;有副作用的动作还会先持久化 prepared action。调用上下文严格受当前租约到期时间约束,避免旧 owner 在 takeover 后继续写 Docker。

保留已有 transport 与 endpoint allowlist:

  • Unix 仅使用 /var/run/docker.sock;
  • Windows 仅使用 Docker Desktop Linux Engine NPipe;
  • 忽略 DOCKER_HOST 和远程/TCP endpoint;
  • 拒绝 redirect、异常 request URL 和不一致响应。

3. 增加九标签精确所有权与安全 adoption

在既有六个 io.cyberagent.* 基础标签上增加:

  • lifecycle attempt;
  • resource generation;
  • launch intent fingerprint。

恢复和 adoption 必须同时满足:

  • 确定性容器名;
  • 完整且无额外项的九标签;
  • image、command、working directory、user、mount、network、resource/security 配置完全一致;
  • 已持久化的 container ID fingerprint 一致。

未知容器、旧六标签容器、缺少标签、额外标签、Run/Attempt/generation/intent 不匹配、配置被修改或 inspect 证据不一致时,协调器都会 fail closed,并保持零 POST/DELETE 副作用。

4. 新增数据库主导的 lifecycle supervisor 与启动恢复

新增 package-private supervisor,提供:

  • BeginAndRun:先提交 launch intent 与 lease,再接触 Docker;
  • RecoverOne:抢占过期租约,重新校验 Run/Workspace/permission/capability,然后按精确观察状态继续;
  • RecoverStartup:只扫描数据库中可恢复 intent,不枚举或收养 daemon 中的未知对象。

覆盖并收敛以下崩溃窗口:

  • intent 已提交、create 尚未发生;
  • create 已生效、created transition 尚未提交;
  • start 已生效、started transition 尚未提交;
  • delete 已生效、cleaned/receipt 尚未提交;
  • cleaned 已提交、receipt 尚未提交。

当恢复时已经存在 durable cleaning 且容器仍在运行,supervisor 会沿用 sticky timeout/cancel reason,直接继续 TERM/KILL/cleanup,不会重新等待完整 timeout。自然退出、取消、超时和重复启动恢复最终只生成一个逻辑 cleaned 事实和一个 cleanup receipt。

5. 稳定的失败关闭与隐私边界

新增或补充测试,确保:

  • daemon 不可达稳定映射为 connection_failed;
  • inspect/config 不一致稳定映射为 configuration_mismatch;
  • stale lease 无法追加 action、transition、receipt,也无法触发 Docker 副作用;
  • 不确定响应不会伪造 exited/cleaned;
  • lifecycle event 只暴露受限状态、原因、代数、结果和布尔值,不暴露 owner、lease token、原始容器 ID、container fingerprint、host path、endpoint、operation key、request 或秘密。

并发与恢复测试覆盖

本 PR 新增的回归场景包括:

  • create 前崩溃后只创建/启动一次;
  • create 已成功但 DB 未确认时精确 adoption,不重复 create;
  • start 已成功但 DB 未确认时观察 running/exited,不重复 start;
  • cleaned/receipt 间崩溃后由新租约补写唯一 receipt;
  • 两个 SQLiteStore/supervisor 竞争同一 operation 时只有一个 owner 产生 daemon 副作用;
  • 旧 generation 在 takeover 后的 action、transition、receipt 和 Docker 写入全部被拒绝;
  • timeout/cancel 重启沿用 sticky outcome,TERM、KILL、DELETE 和 receipt 均保持幂等;
  • partial/extra/legacy/wrong-generation/wrong-intent/config-mismatch 容器全部零修改;
  • connection failure 和 inspect disagreement 全部稳定 fail closed。

CI 基线依赖修复

PR 首轮 GitHub Actions 中,生命周期相关的 Go、Rust 与 Windows job 均通过,但 TypeScript console 在最后的 npm audit --audit-level=high 阶段失败。该失败与 lifecycle diff 无关:仓库基线通过 override 将间接依赖 nanoid 固定在 3.3.17,而更新后的 GHSA-2v37-7h3g-55p8 要求该版本线至少升级到 3.3.18。

为恢复所有 PR 共用的依赖安全门禁,本 PR 增加一个独立且最小的后续提交:

  • web/package.json:仅将 nanoid override 从 3.3.17 升级到 3.3.18;
  • web/package-lock.json:仅同步 nanoid 的 version、resolved URL 与 integrity;
  • 不包含其他 package、Web 源码、OpenAPI 或 Provider 改动。

验证结果

已在只包含本 issue 改动的隔离工作树中完成:

  • go mod verify
  • go test -timeout 20m -count=1 ./...
  • go vet ./...
  • go test -race ./internal/sandbox ./internal/store ./internal/application -run 'Docker.*Lifecycle' -count=1
  • 本机真实 Docker opt-in lifecycle 测试:通过 TERM → KILL → exit 137 → 精确删除流程,测试后无 Prayu 管理容器残留
  • issue-only 安全差异审查:18/18 个源码/测试文件完成审查,0 个可报告安全发现
  • npm ci
  • npm ls nanoid --all:完整依赖树使用 nanoid@3.3.18
  • npm audit --audit-level=high:0 vulnerabilities
  • npm run typecheck
  • npm test:59 个测试文件、229 个测试全部通过
  • npm run build

安全边界与非目标

本 PR 只提供 package-private、非授权的生命周期持久化与恢复基础设施:

  • 不新增 CLI、HTTP、Desktop、model、runner 或 Tool 产品入口;
  • 不授予 Run 执行、进程、输出或 Artifact authority;
  • 不做 daemon-wide orphan 枚举;
  • 不把旧六标签容器升级为受管资源;
  • 不新增远程 Docker/TCP endpoint;
  • 不改变 v56 rehearsal 的安全合同。

新的 supervisor 与九标签 recovery 通过持久 fake daemon 做端到端崩溃/竞争测试;真实 Docker opt-in 测试继续验证固定本地 transport 与强制清理路径。产品接入、daemon-wide orphan 策略、secret materialization、bounded logs 和 Artifact 导出仍需后续独立 gate。

详细设计与不变量记录见 ADR 0096。

Closes #38

在 SQLite schema v97 中新增不可变启动意图、代际租约、写前动作、哈希链状态迁移和唯一清理收据。

为 Docker create/start/wait/TERM/KILL/delete 增加逐动作 fencing,并通过精确九标签、完整配置和资源指纹实现数据库主导的崩溃恢复。

补充抢占、陈旧写入、取消与超时重放、异常 daemon 响应、隐私边界及真实 Docker 生命周期验证。
@Qiyuanqiii
Qiyuanqiii marked this pull request as ready for review August 14, 2026 05:38
将 nanoid override 与锁文件从 3.3.17 升级到首个受影响版本线的修复版本 3.3.18,解除 GHSA-2v37-7h3g-55p8 导致的 TypeScript CI 阻塞。

该提交仅修改 web/package.json 与 web/package-lock.json,不包含现有工作区中的其他 Web 或 Provider 改动。
@Qiyuanqiii
Qiyuanqiii merged commit b84c9c4 into main Aug 14, 2026
4 checks passed
@NanaseInori
NanaseInori deleted the codex/issue-38-docker-lifecycle-recovery branch September 16, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sandbox): 持久化容器生命周期所有权、租约与崩溃恢复

1 participant