Files
codex-app-server/docs/chv-architecture.md
T

326 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 配额分支,不靠删测试
或改分母凑数。