326 lines
19 KiB
Markdown
326 lines
19 KiB
Markdown
# Cloud Hypervisor 执行器架构决策记录(ADR)
|
||
|
||
范围:cloud/chv/ 执行器链(#216 → #221 → #230 → #232 → #234 → #239 → #246 → #252 → #254 → 本轨 feat-chv-arch-maint-fe)。
|
||
本文件只记录已落代码的决策,不记录未验证为已验证;真机、压测、动态安全
|
||
仍见 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/<pid>/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_attempt`
|
||
try/except 块)。worker 方法名、签名、调用点、错误文案
|
||
全部不变,旧测试继续走 worker 方法;适配器本身另有直接
|
||
行为测试。适配器只依赖标准库 + chv 叶子模块 +
|
||
`cloud.logging_config`,不 import `cloud.worker`,无循环;
|
||
公共 API 全注解(#257 的类型测试扫 `cloud/chv/*.py`,
|
||
新模块不注解会直接炸该测试——本轨已补齐)。
|
||
- 明确不抽(硬抽无收益,只会撕通用流程):cancel() 里的
|
||
`cancel_vm` 分支(与 Docker `kill_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 一处与本轨无关,未重排;本轨只删了随抽取而变多余
|
||
的 `inspect` import(否则新增 F401)。
|
||
- 覆盖率:`coverage --source=cloud/chv` 对 CHV 套件(不含
|
||
worker/server 全量),基线 85% → 本轨 87%(1826 行,
|
||
缺 268 → 242);未覆盖大头仍是 executor 深层生命周期
|
||
分支(需假 VMM 全链路)与 disk 配额分支,不靠删测试
|
||
或改分母凑数。
|