21 KiB
21 KiB
Cloud Hypervisor 执行器架构决策记录(ADR)
范围:cloud/chv/ 执行器链(#216 → #221 → #230 → #232 → #234 → #239 → #246 → #252 → #254 → feat-chv-arch-maint-fe → feat-chv-closeup → feat-chv-cover100 → 本轨 feat-chv-p1fix)。 本文件只记录已落代码的决策,不记录未验证为已验证;真机、压测、动态安全 仍见 docs/chv-production-readiness.md §11。
ADR-1 SSH 目标:客机 IP 发现(已选) vs vsock / 纯 NAT(未选)
- 决策:显式 CHV_SSH_HOST > CHV_NET_JSON 首个带 ip 的 net 条目 > 显式 CHV_SSH_FORWARD=1 宣称下的回环;三者皆无报 ssh_target_undeclared,绝不无条件假设 127.0.0.1:2222 能通。
- 未选 vsock:需客内 agent + 宿主代理两套新代码,当前客机镜像 无此组件,写进代码仍是假保证。
- 未选纯 NAT:DNAT 是宿主防火墙外部状态,执行器代码既不能 创建也不能校验它,写进代码仍是假保证;宿主防火墙见就绪 清单 §11.4 部署清单,代码门 + 防火墙缺一不可。
ADR-2 网关 key 走 seed 投递(已选) vs argv / 宿主环境(已弃)
- 决策:provisioned 模式下密钥子集随 seed 写进客内 /etc/chv/guest-env(0600),远程命令 source 它;传输层密钥 进 argv 直接 ValueError;keyscan 模式有密钥即 fail-closed。
- 放弃 argv:宿主 ps / /proc//cmdline 可读;放弃宿主子进程 环境:Docker 线 -e 只传名、值仍在 CLI 子进程环境,CHV 比它 更严(值连宿主子进程环境都不进)。
- 残留:seed 随 stop 删,私钥在 seed 盘建好后即删, cleanup_stale_secrets 扫崩溃残留;删失败 fail-fast 或脏标记拒起。
ADR-3 磁盘 copy 默认,reuse 只作单租户实验(已选)
- 决策:CHV_DISK_MODE 默认 copy,每任务独立 disk.raw; reuse 须同时满足 CHV_REUSE_SINGLE_TENANT_EXPERIMENT=1 与 CHV_SINGLE_TENANT_PROJECT 声明,且 wake 时审计放行。
- 边界:代码无法验证数据库多租户状态,声明 + 审计即机制, 不假装已校验;生产多租户下 reuse 一律不启用。
ADR-4 restore 不重预置(纠正,见 #232 审计)
- 纠正:审计原说法“restore 须把 seed 接入 VmConfig”是错的—— restore 不走 vm.create,无 VmConfig 可挂盘;快照内已固化原 VM host key,重预置反而破坏钉死。
- 正确不变量:provisioned 下复用已钉死 known_hosts、缺失 fail-fast(host_key_not_pinned),绝不 keyscan 回落。
ADR-5 WarmPool:实验性,生产 dispatch 永远冷启动(本轨定案)
- 决策(二选一的 B 项):把 WarmPool 从生产路径宣称里摘掉, 明确标为实验/停用,不再留“半接线”悬空状态。
- 原因:预热 VM 的磁盘是干净快照盘,不是任务盘;dispatch 前 查池若要真正接线,必须在派任务前给已暂停 VM 重新供给任务 盘(热换盘 / 二盘挂载 / host.py 磁盘语义三者都未实现), 把预热盘直接当任务盘用会击穿任务盘隔离不变量。
- 落地:WarmPool.wired_to_dispatch 恒为 False;_run_with_vm 绝不查池,dispatch 永远冷启动;acquire 内 resume 只保留为 实验接口;worker 启动/关闭仍调 reconcile_orphans()(上一世 残留清理是池子保留的唯一生产价值)。
- 重启条件:将来要重新接线,必须先实现任务盘重供给并补 “任务盘来源”不变量测试,否则不得把 wired 改成 True。
ADR-6 Docker goal 解析分叉:共享正典 + 同步守卫(本轨定案)
- 决策:Local/CHV 共用 parse_goal_result 正典;Docker goal_task 内联版保留,但与正典的 8 个真实标记用例(met/unmet/ interrupted 标记/无结论异常退出/永久错误/无结论 rc0/ goal-resume 拒绝/已存在撞名)由 test_chv_arch95.py 的 DockerSyncGuardTest 行为测试钉住状态全等,任一实现改了 分叉测试即 fail,不再只靠文档清单。
- 注意:Docker 的超时证据路径仍与内联解析绑在一起,那是 有意保留的第二处差异,不在 8 用例的状态等价范围内。
ADR-7 #215 未合并时的复用边界(不复制模块)
- 现状:#215(timeouts/gates/quota/sandbox_policy)未合并, 本链 base 里无其模块,本轨不复制其代码。
- 纠正(feat-chv-arch95plus,核对过 #215 远程分支真实 函数后):上一轨 ADR-7 的“超时→timeouts、配额→quota” 泛化映射是错的。#215 的 timeouts 收敛的是审批/队列 超时(None/0=不过期),quota 是按月美元配额,与 CHV 的 api/ready 秒数(0 无效)和 VM 数/磁盘 GB/快照数 单位与语义都不同,硬套会改行为,故这两项无映射。
- 真实映射(已落代码,见 cloud/chv/compat215.py): gates.resolve_inside(base, rel) 目录门 → CHV 快照/磁盘 路径白名单已在 hardening 里使用,合并后改为 import; sandbox_policy.tmpfs_isolated → CHV 客机 /tmp 落客机 磁盘、一律不隔离(fail-closed),合并后改为 import。 其余 #215 函数(审批、控制门、合并状态门、worker 寻址) 与 CHV 无交集,不映射。替换时须重跑本链测试,不得改语义。
ADR-8 WarmPool 生产隔离:facade 而非类导入
- 决策:生产路径(worker 启动/关闭)只调 cloud/chv/pool.py 的 reconcile_orphans_for_executor() facade,该函数内部才构造 WarmPool 并只调 reconcile_orphans();生产 dispatch 绝不调 prewarm/ acquire/release,executor.py 也不 import pool。 WarmPool 类与 sandbox re-export 保留仅为兼容(旧测试 import 路径),新代码不得经类入口进实验接口。
- 测试钉住:facade 测试断言 acquire/prewarm 绝不被调; executor 不 import pool 由模块依赖检查覆盖。
- 本轨更新(feat-chv-arch-maint-fe,取代上文“保留兼容” 部分):CHV 尚未合并、尚无生产用户,“怕断 import” 不是保留理由,兼容保留已删除。WarmPool 从 pool.py 移到 experimental_pool.py,cloud/chv/init 与 cloud/sandbox.py 不再导出它,生产残留清理彻底分文件 到 cloud/chv/orphans.py(facade 不再构造 WarmPool, 直接读 registry 文件做清理)。旧 import 路径一律 ImportError,见 ADR-12;合并前即生效的破环性清理, 合并时须按 PR 描述的清单检查调用方。
ADR-9 审计失败取舍:记日志 + 计数,不中断任务
- 选项 A(失败即中断任务):审计完整性最强,但 DB 短暂 不可用会把一次审计失败放大成任务失败,且与 worker 全局 best-effort 语义冲突,CHV 单独硬中断是不一致的。
- 选项 B(失败记日志 + 计数 + 不中断任务):已选。 executor._emit 失败时记宿主 error 日志并累加 audit_failure_count(宿主可观测,非任务状态),任务 继续;审计完整性本身没有保证,文档与就绪清单明写。
- 边界:本决策只针对 VM 生命周期审计;VM 启动/关机等 门禁失败仍按原有分类失败处理,不受本条影响。
ADR-10 长函数抽不动的耦合点(本轨后已无 >50 行函数)
- 本轨后 cloud/chv/ 全部函数均 ≤50 行,上一轨的 7 个 长函数全部拆完:stop_vm(进程清理/文件清理/脏标记三 helper)、wait_guest_ready(provisioned 守卫/不匹配检查)、 restore_vm(校验/启动段)、cleanup_stale_secrets(单 VM 清理/活 VM 判定)、cleanup_stale_vms(死 socket 扫描/ 登记清理)、_unix_socket_api(读头/解析长度/读体/解析 响应)、validate_net_entries(归一化/去重/冲突检查)、 build_chv_config(组装段纯移动)。
- 残留耦合点(已拆但仍需注意):stop_vm 的三块清理共享 同一脏标记,任一块失败即整 VM 标记脏(有意,不拆成 三个标记);_unix_socket_api 的读满语义仍由四个 helper 共同保证,改任一 helper 须重跑截断/超时测试。
ADR-12 模块职责图与禁 import 清单(本轨)
依赖方向(箭头 = 可以 import,叶子在下):
- worker.py → orphans.py(生产残留清理,方法内 import) → 标准库;worker 不 import experimental_pool。
- executor.py → config/disk/hardening/hostkeys/ ssh_transport/api_client/errors(方法内按需 import sandbox 共享 helper 除外,顶层绝不 import sandbox)。
- experimental_pool.py → orphans.py(只借 registry 文件 IO 与损坏语义,方向是实验 → 生产,绝不反向)。
- orphans/config/compat215/errors → 只依赖标准库 (叶子模块,测试用 AST 钉住)。
- __init__.py / sandbox.py → 只 re-export 生产接口 (CloudHypervisorExecutor、ChvFailure、 reconcile_orphans_for_executor),不导出 WarmPool。
- config.py → executor.py(executor 只经 build_chv_config 取配置,不直接读 env 做二次解析)。
禁 import 清单(测试用 AST 逐项钉住,文档提及不算):
- orphans/executor/worker/init/sandbox 一律不得 import experimental_pool 或已删除的 cloud.chv.pool;
- executor 一律不得 import orphans(执行器本体不做 残留清理,那是 worker 启动/关闭的职责);
- 任何生产模块不得 import cloud.gates / cloud.sandbox_policy / cloud.timeouts / cloud.quota (#215 未合并,见 compat215 REPLACEMENT_POINTS)。
ADR-13 配置组装片段化(本轨,行为不变)
- build_chv_config 拆成六个片段:base(身份/资源基底)、 ssh(密钥/用户/端口/host key 模式/客内路径/命令行)、 network(net/net_json/ssh_host/ssh_forward/egress)、 quota(max_vms/warm_pool_size)、disk(磁盘模式/大小/ reuse 实验/快照上限与 URL)、timeout(任务/API/就绪 超时)。组装只做 dict 合并,键冲突即 ValueError, 绝不静默覆盖。
- 移动说明(字段集合与取值不变,测试以 38 字段集钉住): egress 从旧 quota 片段移到 network(它是出口策略, 不是 VM 配额);snapshot_url 从旧超时片段移到 disk; warm_pool_size 从旧超时片段移到 quota。唯一可观测 的校验顺序变化:warm_pool_size 非法与磁盘非法同时 出现时先报前者;单项非法时报错文案逐字一致。
- 类型:cloud/chv/ 公开 API(非下划线函数 + 公开类 init)全量标注,无 Any 逃逸;本轨顺手修正三处 旧标注错误(record_snapshot 应为 tuple、host_key_paths 应为 dict、build_seed_disk 应为 str)。私有 helper 不在“公开 API”口径内,不硬标。
ADR-14 前端只读侧扩展与验证方式(本轨)
- 本轨前端只做只读侧,不做控制按钮:后端无 VM 控制 端点且安全审计 No-Go,绝不发明控制接口。
- 列表行:此前 chv_vm_dead 只在详情展示,本轨列表行 增加“VM 已报告死亡”徽标(字段已由同一 serializer 返回,列表此前只是没渲染);无徽标时不代表 VM 活着。
- 详情:此前只有 chv_vm_dead 时才出现 VM 行,本轨对 所有 CHV 任务都显示 VM 行——已报告死亡给死亡文案, 否则给“未报告 VM 死亡(后端无 live 状态字段,不代表 VM 活着)”。非 CHV 任务不显示 VM 行;死亡文案同样 先过执行器门,防止非 CHV 行误带字段时误显示。
- 池管理与成本页:不新开页面。已有数据继续在已有 位置展示:WorkersSection 的 CHV worker 在线数/共计、 详情成本卡(token/LLM 请求数/美元明细,来自 GET /tasks/{id}/cost)。没有数据的页面绝不新开。
- 后端字段:本轨未新增第二个生命周期字段,理由写清—— server 持久化的 CHV 生命周期事实只有 executor / status / interrupted_reason 三项,chv_vm_dead 已是其 完整派生;再编一个字段要么是 interrupted_reason 的 复制,要么是猜 live 状态,两者都不做。本轨后端改动 是把派生抽成 _chv_vm_dead_from_row(行为逐字一致) 并补 goal 回落形态(纯文本“VM 中途死亡”)的测试。
- 前端测试:纯逻辑全覆盖——执行器选择谓词(精确值, 不宽松匹配)、可用性(上一轨)、附件守卫谓词(CHV + 带附件 → 后端 400,前端提示口径,后端是最终权威; 派发页提交拦截已接该谓词)、只读状态文案与列表徽标。 组件渲染不在覆盖内(无 testing-library 依赖,不临时 加依赖冒充覆盖)。
- 验证方式(仓库浏览器任务拒绝回环地址是已知限制, 本轨绝不声称已做真机点击):① tsc + vite 构建通过; ② npm run build:console 产物 console.html 经 grep 含 新文案;③ Vitest 全过;④ 用户侧 live browser 手动 检查清单(见 PR 描述):本地起 vite preview 或直接 打开部署后的控制台,建一个 CHV 任务(无附件)进详情 看 VM 行文案、带附件提交看拦截文案、列表行对已报告 死亡任务看徽标;真机点击结果由用户侧回填,不在本轨 回执里预填。
ADR-11 只读 VM 字段边界:chv_vm_dead 不是 live 状态
- 决策:server 任务序列化只在 CHV 任务且 worker 已把 VM 死亡写进 interrupted_reason 列时返回 chv_vm_dead=True, 其它情形字段不存在;worker 终局时把执行器的 vm_dead 原因写回该列(见 worker.py 终局块),否则该字段在 生产里永远不出现。
- 边界:该字段是“已报告死亡”,不是“VM 活着/死了”的 live 状态——server 进程没有 VM 登记表,live 状态编不 出来;前端无该字段时不显示 VM 状态,不猜测。VM 控制 按钮仍不做(后端无控制端点且安全审计 No-Go)。
ADR-13 CHV worker 适配模块(to10 轨)
- 决策:新建
cloud/chv/worker_adapter.py,worker.py 只留 薄包装。抽入:断点调用(原顶层函数本体)、审计回调 (原__init__闭包)、孤儿清理(原_reconcile_warm_pool本体)、两条看门狗循环(原_chv_*_watchdog本体,self 依赖全部改成回调参数)、看门狗 VM 名派生(原_run_attempttry/except 块)。worker 方法名、签名、调用点、错误文案 全部不变,旧测试继续走 worker 方法;适配器本身另有直接 行为测试。适配器只依赖标准库 + chv 叶子模块 +cloud.logging_config,不 importcloud.worker,无循环; 公共 API 全注解(#257 的类型测试扫cloud/chv/*.py, 新模块不注解会直接炸该测试——本轨已补齐)。 - 明确不抽(硬抽无收益,只会撕通用流程):cancel() 里的
cancel_vm分支(与 Dockerkill_container分支相邻的 通用取消流)、cleanup_stale_containers的 CHV 分支 (与 Docker 清理相邻的通用清理流)、网关守卫(Local / Docker / CHV 三分支共用的派发流)、_executor_name的 CHV 分支(通用执行器名映射)、_run_attempt的看门狗 线程启动(线程管理是 worker 通用逻辑,只抽了 VM 名派生)、 VM 死亡写回(通用终局流)。以上六处留在 worker.py, 不是遗漏,是评估后有理由的结论。 - 附带清理:
inspect在 worker.py 的唯一用处随断点函数 移走,import 一并删除(留着会触发 F401);worker.py 基线已有的 I001(两处 import 排序)与 F821(playwright 播种块的task)与本轨无关,不顺手重排整个文件。
ADR-14 审计失败落盘兜底(to10 轨)
- 决策:选“本地审计落盘文件”作为兜底,不选“只记日志 +
计数”(已做,仍不够:日志会轮转、计数只剩数字,事后
找不回哪次事件丢了)。实现:
executor._emit回调失败 时,除记宿主 error 日志 +audit_failure_count+1 外, 再把事件 append 到state_dir/chv-audit-spill.jsonl(每行一 JSON:ts/event/kind/detail/task_id),文件超 1MB 时轮转成.old再写。 - 取舍(写清,不当成完整修复):① 落盘本身 best-effort,
写失败只记日志,绝不中断任务;② 落盘不是 DB 替代,
任务 timeline 查询看不到它,需宿主人工查阅;③ 轮转
只留最新两代,更早的丢失事件仍会丢;④ detail 沿用
_emit契约(只含 VM 名/路径/URL/kind),绝不含密钥值, 落盘文件随 state_dir 的 0700 权限保护。审计完整性本身 仍无保证——保证需要 DB 事务级审计写入,那是 worker 全局语义的改动,不在本轨硬做。 - 不选的另一条路:“审计失败即中断任务”——ADR-9 已否决: 审计是可观测性不是门禁,DB 短暂不可用时硬中断会把 一次审计失败放大成任务失败,且与 worker 全局 best-effort 语义冲突。
ADR-15 前端只读 VM 展示四态与可用性统一文案(to10 轨)
- 决策:
chvVmDisplay(task)返回四态而非此前两态文案: dead(字段 present 且 true)、not-reported(CHV 任务但 字段 absent,只表示未报告)、unknown(任务详情本身未 加载)、not-applicable(非 CHV,不显示行)。旧入口chvVmStatusText保留为 display 的 text 包装(unknown / not-applicable 回空串),已测调用点不变。详情页 VM 行 改用chvVmDisplay(t).text并加role="status"与aria-label="VM 状态"。 - 决策:
chvAvailabilityText(availability)统一新建任务 / Workers / 环境三处可用性文案。此前三处各写各的,Workers 的未知分支恒写“仅全局管理员可见”(加载失败/未加载时 也是这句,是文案 bug),本轨统一后按 reason 分三句, known 零在线沿用派发页的完整版(不拦提交、后端是最终 权威)。无障碍打磨:可用性区与提交结果区加aria-live="polite"(结果区另加role="status"),附件 三个 input 加aria-label(此前只有 title),禁用态本身 已由disabled传达,不另造禁用文案。控制按钮仍不做 (后端无控制端点且安全审计 No-Go);组件渲染仍不在 测试覆盖内(无 testing-library 依赖,不临时加依赖冒充)。
ADR-16 to10 轨的类型与 lint 口径(诚实标注)
- mypy:
--follow-imports=silent cloud/chv/,只查 chv 本身 (不拖入无关文件)。基线该口径 115 错 → 本轨 0 错; 默认 follow-imports 口径基线 117 错 → 本轨 2 错,且 2 错全在作用域外的cloud/marker_tools.py:142(与 chv 无关,不顺手修它),chv 自身在默认口径下也是 0 错。 - ruff:作用域 F/E9/W/I(未用变量、语法错、空白、import
排序),基线 8 错 → 本轨 0 错;默认全规则基线 208 错
(含本仓库未采纳的风格意见,如百分号格式化、
best-effort 清理的 try/except-pass),本轨不声称全规则
通过。worker.py 不在 lint 作用域内:其基线 I001 两处与
F821 一处与本轨无关,未重排;本轨只删了随抽取而变多余
的
inspectimport(否则新增 F401)。 - 覆盖率:
coverage --source=cloud/chv对 CHV 套件(不含 worker/server 全量),基线 85% → 本轨 87%(1826 行, 缺 268 → 242);未覆盖大头仍是 executor 深层生命周期 分支(需假 VMM 全链路)与 disk 配额分支,不靠删测试 或改分母凑数。
ADR-17 P1 修复轨语义收紧(feat-chv-p1fix,生产级审核 §5)
本轨只修审核报告 §5 的 P1 十项(P2 不动),决策均为 “失败可见、猜测收紧、竞态加锁”,不放宽任何既有安全语义:
- 失败不算成功:孤儿清理失败项不进 cleaned、不清库、 落盘保留失败项(P1-01);残留私钥删失败不进 cleaned、 发审计并写脏标记(P1-03);单条超帽快照拒绝记录而不 是悄悄保留(P1-06);快照失败与剪枝删失败均发审计 (P1-08)。“尽力清理、不因单条失败卡住整批”的旧语义 被修正为“尽力清理、失败项留痕可重试”——整批仍不卡, 但失败不再被成功名单掩盖。
- 损坏与非法不静默:registry 损坏与非法条目均发审计并 计数(P1-02);非法条目落盘时丢弃(已审计,不再每次 重启重复审计)。
- 不猜目标:SSH 命令构造删除回环回落,失败即抛 (P1-04);显式回环(CHV_SSH_HOST / CHV_SSH_FORWARD=1) 仍可用,wake/wait 严格路径不受影响。
- 竞态用文件锁:容量/net/磁盘 check→act 持
state_dir/chv-state.lock的 flock 全程(P1-05), 拒绝“只加进程内锁就声称多 worker 已解决”的写法; 串行 wake 是有意的代价,已运行 VM 的任务执行不持锁。 - 非正配置在入口拦:CPU/内存/任务超时非正在构造层与 preflight 层双重校验(P1-07);None 任务超时 = 不限 的旧语义保留。
- 覆盖口径诚实:行覆盖与分支覆盖分开报(P1-09);
__init__.py计入 coverage.py 分母(4 行);长函数 50 行硬门禁靠提取维持,新增 helper 均有命名职责 (_snapshot_create、_tighten_snapshot_cap、_preflight_task_timeout_check、_emit_prune_events、_state_lock_ctx),不是为凑行数而拆。