架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py, 生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读 registry 文件清理,不构造池、不碰 prewarm/acquire/release); cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律 ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由); worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从 接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、 R3 永不替换,#215 模块当前不存在由测试钉住)。 可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/ timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集 与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图 与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型 标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍 (删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。 前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案 (执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表 行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行 (已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本 卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个 生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生), 只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
235 lines
14 KiB
Markdown
235 lines
14 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)。
|