feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示

架构: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 回落形态测试。
This commit is contained in:
host committed 2026-10-31 06:55:26 +00:00
1 parent 42de20da68
commit df8c64bd64
30 files changed
+1263 -331

No files matched your search

+66 -50
View File
@@ -7,7 +7,8 @@
- ssh_transport.py:SSH 命令构造、每 VM known_hosts 钉死、
ssh-keyscan 首次捕获(TOFU 边界见该模块)
- disk.py:每任务磁盘供给与磁盘配额门
- pool.py:WarmPool 预热池 + registry 持久化与孤儿清理
- orphans.py:生产残留清理唯一入口(registry 文件直读清理)
- experimental_pool.py:WarmPool 预热池(纯实验,生产禁 import)
- executor.py(本模块):执行器类本体——生命周期方法
(wake/pause/resume/snapshot/restore/hibernate/stop/
cleanup)与任务执行方法(dispatch/run/goal/execute)。
@@ -154,11 +155,11 @@ class CloudHypervisorExecutor(Executor):
- 单任务单 VM:wake → 跑 → finally 关,同一任务不共享 VM;
每任务磁盘 copy 模式独立,租户无共享路径、无东西向互访;
VM 内 root 只能影响本 VM。
- 快速启动:WarmPool(cloud/chv/pool.py)预热干净快照 VM,
- 快速启动:WarmPool(cloud/chv/experimental_pool.py)预热干净快照 VM,
acquire 消耗、release 销毁,池空回落冷启动,绝不降级;
池状态另落 state_dir/warm-pool.json registry,worker
重启后载入的条目一律当孤儿、只能 reconcile_orphans()
清理,绝不重新发给任务(见 pool.py)。
清理,绝不重新发给任务(见 orphans.py)。
- 长任务中途死亡:_run_with_vm 在客内命令抛传输异常、
或非 goal 结果已 failed 且 VM 登记进程已死时,改判
interrupted(断点可续跑)而不是笼统 failed;VM 还活
@@ -245,17 +246,30 @@ class CloudHypervisorExecutor(Executor):
开启嵌套虚拟化的实例,那是用户侧开通决策,不在本轨内。
"""
def __init__(self, binary=None, kernel=None, rootfs=None,
image=None, cpus=None, memory=None, state_dir=None,
runner=None, launcher=None, ssh_key=None,
ssh_user=None, ssh_port=None,
guest_host_py="/app/host.py", cmdline=None,
net=None, net_json=None, ssh_host=None,
kvm_path="/dev/kvm",
cpuinfo_path="/proc/cpuinfo", env=None,
model=None, ch_remote=None, api_client=None,
sleeper=None, api_timeout=None, ready_timeout=None,
ready_interval=None, host_key_mode=None):
def __init__(self, binary: str | None = None,
kernel: str | None = None,
rootfs: str | None = None,
image: str | None = None,
cpus: str | int | None = None,
memory: str | None = None,
state_dir: str | None = None,
runner: object = None, launcher: object = None,
ssh_key: str | None = None,
ssh_user: str | None = None,
ssh_port: int | str | None = None,
guest_host_py: str = "/app/host.py",
cmdline: str | None = None,
net: str | None = None, net_json: str | None = None,
ssh_host: str | None = None,
kvm_path: str = "/dev/kvm",
cpuinfo_path: str = "/proc/cpuinfo",
env: dict | None = None,
model: str | None = None, ch_remote: str | None = None,
api_client: object = None,
sleeper: object = None, api_timeout: float | None = None,
ready_timeout: float | None = None,
ready_interval: float | None = None,
host_key_mode: str | None = None) -> None:
self._env = env if env is not None else os.environ
# 配置解析已拆到 cloud/chv/config.py(参数优先于环境
# 变量,默认值与 #216 一致;显式空用户/越界端口的收紧
@@ -326,7 +340,7 @@ class CloudHypervisorExecutor(Executor):
return re.sub(r"[^A-Za-z0-9_.-]", "-",
str(task.get("id")))
def vm_name(self, task_or_name):
def vm_name(self, task_or_name: dict | str) -> str:
"""VM 名:chv-<安全字符任务名>(与 Docker 的 codex- 同款
派生,同一任务每次算出同一名字,清理才能按名找)。
幂等:传入的已是 chv- 前缀名时不再二次加前缀(清理
@@ -339,7 +353,7 @@ class CloudHypervisorExecutor(Executor):
return safe
return "chv-" + safe
def api_socket(self, task_or_name):
def api_socket(self, task_or_name: dict | str) -> str:
"""该 VM 的 api-socket 路径:state_dir/<vm 名>.sock。"""
return os.path.join(self.state_dir,
self.vm_name(task_or_name) + ".sock")
@@ -559,7 +573,7 @@ class CloudHypervisorExecutor(Executor):
validate_net_entries(_entries, [])
except ChvFailure as exc:
missing.append((exc.kind, str(exc)))
def preflight(self):
def preflight(self) -> bool:
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
供审计与失败分类用),绝不回落 Docker/Local。
@@ -651,7 +665,7 @@ class CloudHypervisorExecutor(Executor):
cmd += ["--net", self.net]
return cmd
def known_hosts_path(self, task_or_name):
def known_hosts_path(self, task_or_name: dict | str) -> str:
"""该 VM 的 known_hosts 文件:state_dir/<vm>/known_hosts
(与每任务磁盘同目录,见 ssh_transport.py 的钉死模型)。"""
return _known_hosts_path(self.state_dir,
@@ -671,7 +685,7 @@ class CloudHypervisorExecutor(Executor):
return str(entry["ip"])
return None
def resolve_ssh_host(self):
def resolve_ssh_host(self) -> str:
"""SSH 目标解析(feat-chv-fix,P0-4 选定方案:客机 IP):
显式 CHV_SSH_HOST > net 静态客机 IP > 显式转发宣称下
@@ -690,7 +704,7 @@ class CloudHypervisorExecutor(Executor):
"里给 net 条目配 ip(静态 tap 客机 IP)、或显式设 "
"CHV_SSH_FORWARD=1 宣称宿主 NAT 已转发回环端口")
def provision_host_key(self, task, secret_env=None):
def provision_host_key(self, task: dict, secret_env: dict | None = None) -> dict:
"""按 VM 预置 host key(补缺口轨强方案,见 hostkeys.py):
启动前为该 VM 生成一次性 ed25519 host key 对,公钥
立即写进该 VM 的 known_hosts(钉死),私钥只写进
@@ -741,7 +755,7 @@ class CloudHypervisorExecutor(Executor):
return {"paths": paths, "seed_dir": seed_dir,
"known_hosts": paths["known_hosts"]}
def capture_host_key(self, task):
def capture_host_key(self, task: dict) -> bool:
"""首次捕获客机 host key(TOFU,弱回落,边界见
ssh_transport.py 与 hostkeys.py):ssh-keyscan 经本机
回环端口抓 key 行,写入该 VM 的 known_hosts 文件
@@ -775,7 +789,7 @@ class CloudHypervisorExecutor(Executor):
"vm=%s host=%s" % (self.vm_name(task), ssh_host))
return True
def build_ssh_command(self, task, inner_cmd, extra_env=None):
def build_ssh_command(self, task: dict, inner_cmd: list, extra_env: dict | None = None) -> list:
"""构造 SSH 命令:ssh -p <端口> -i <key 文件> + 钉死选项
+ user@<SSH 目标> + 客内命令。argv 里只有 key 文件路径,
绝无任何 key 值。egress_proxy 已设时客内命令前加 env
@@ -821,7 +835,7 @@ class CloudHypervisorExecutor(Executor):
guest_env_file=guest_env_file)
# -- API 模式:VmConfig / API 调用 / 就绪轮询 -----------------------
def build_vm_config(self, task_disk=None, seed_disk=None):
def build_vm_config(self, task_disk: str | None = None, seed_disk: str | None = None) -> dict:
"""API 模式的 VmConfig(vm.create 请求体,纯函数):
cpus{boot_vcpus,max_vcpus}、memory{size: 字节}、
kernel{path}、cmdline{args}、disks[{path: 每任务磁盘}]、
@@ -968,7 +982,7 @@ class CloudHypervisorExecutor(Executor):
self.state_dir, self.vm_name(task), self.rootfs,
self.disk_mode)
def wait_api_ready(self, task):
def wait_api_ready(self, task: dict) -> bool:
"""等 vmm.ping 通:按次数轮询(api_timeout/1 秒取整,
至少 1 次),不用墙钟死循环——fake sleeper 不推进墙钟
时墙钟循环会空转,次数循环在单测里是确定性的。
@@ -1014,7 +1028,7 @@ class CloudHypervisorExecutor(Executor):
return True
return False
def wait_guest_ready(self, task):
def wait_guest_ready(self, task: dict) -> bool:
"""等 guest SSH 就绪:轮询 ssh "true",returncode 0 即就绪;
每轮先尽力捕获 host key(keyscan 模式),超时先关 VM
再 raise guest_not_ready,绝不在未就绪时派任务。"""
@@ -1298,7 +1312,7 @@ class CloudHypervisorExecutor(Executor):
return task_disk, socket_path
def wake_vm(self, task, extra_env=None):
def wake_vm(self, task: dict, extra_env: dict | None = None) -> dict:
"""主唤醒路径(API 模式,dispatch/run/goal 的前置步骤):
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
@@ -1335,14 +1349,14 @@ class CloudHypervisorExecutor(Executor):
return {"vm": name, "api_socket": socket_path,
"state": "running", "ready": True, "mode": "cold"}
def start_vm(self, task):
def start_vm(self, task: dict) -> dict:
"""旧名保留:即 wake_vm(API 模式)。直接启动命令
_build_vm_command() 只作手动等价写法(私有),不再是执行器
的启动路径——两条启动路径并存是上一版的半吊子,
本版只认 API 模式。"""
return self.wake_vm(task)
def pause_vm(self, task_or_name):
def pause_vm(self, task_or_name: dict | str) -> bool:
"""暂停 VM:ch-remote pause。成功审计 vm_pause。"""
self._ch_remote(task_or_name, "pause")
task = task_or_name if isinstance(task_or_name, dict) \
@@ -1351,7 +1365,7 @@ class CloudHypervisorExecutor(Executor):
"vm=%s" % self.vm_name(task_or_name))
return True
def resume_vm(self, task_or_name):
def resume_vm(self, task_or_name: dict | str) -> bool:
"""唤醒已暂停的 VM:ch-remote resume。成功审计 vm_resume。
注意:这是暂停唤醒的唯一路径,恢复后的 VM 也走它
(见 restore_vm),绝不能只 restore 就算醒。"""
@@ -1362,7 +1376,7 @@ class CloudHypervisorExecutor(Executor):
"vm=%s" % self.vm_name(task_or_name))
return True
def snapshot_vm(self, task_or_name, snapshot_url):
def snapshot_vm(self, task_or_name: dict | str, snapshot_url: str) -> str:
"""给(已暂停的)VM 做快照:ch-remote snapshot
<source_url>。snapshot_url 为空即 ValueError;成功
审计 vm_snapshot。快照本身不改变 VM 状态(仍 paused,
@@ -1454,7 +1468,7 @@ class CloudHypervisorExecutor(Executor):
self.resume_vm(task)
self.wait_guest_ready(task)
def restore_vm(self, task, snapshot_url):
def restore_vm(self, task: dict, snapshot_url: str) -> dict:
"""快照恢复唤醒:校验 → 只起 VMM → restore → 显式
resume → 等 SSH 就绪。恢复后默认 paused,不 resume
就不算醒。任一步失败都关 VM 再抛;成功返回
@@ -1474,7 +1488,7 @@ class CloudHypervisorExecutor(Executor):
return {"vm": name, "api_socket": self.api_socket(task),
"state": "running", "ready": True, "mode": "restored"}
def hibernate_vm(self, task_or_name, snapshot_url):
def hibernate_vm(self, task_or_name: dict | str, snapshot_url: str) -> dict:
"""休眠(CHV 无原生 hibernate 端点,等价实现):
pause + snapshot + shutdown。休眠后文件与 worktree
保留在快照里(快照是暂停时刻的完整状态),临时文件
@@ -1489,7 +1503,7 @@ class CloudHypervisorExecutor(Executor):
return {"vm": self.vm_name(task_or_name),
"state": "hibernated", "snapshot": snapshot_url}
def wake_from_hibernate(self, task, snapshot_url):
def wake_from_hibernate(self, task: dict, snapshot_url: str) -> dict:
"""从休眠唤醒:即 restore_vm(restore + 显式 resume
+ SSH 就绪),语义与 restore_vm 完全一致,只是名字
说明来源是休眠快照。"""
@@ -1560,7 +1574,7 @@ class CloudHypervisorExecutor(Executor):
except Exception:
pass
def stop_vm(self, task_or_name):
def stop_vm(self, task_or_name: dict | str) -> bool:
"""停止 VM(尽力而为,不抛异常):已死进程直接清登记,
已关 VM 仍尽力调一次 ch-remote(返回 False),socket
残留删 socket(返回 True)。ch-remote shutdown 失败
@@ -1597,7 +1611,7 @@ class CloudHypervisorExecutor(Executor):
_LOGGER.info("chv_vm_stop vm=%s had=%s", name, had)
return had
def gc_stale_disks(self):
def gc_stale_disks(self) -> dict:
"""残留盘 GC(H3):扫描 state_dir/*/disk.raw,VM
非 live(无 api-socket 文件且无登记活进程)即删盘
并审计;删失败审计 vm_disk_delete_failed 并计入
@@ -1675,7 +1689,7 @@ class CloudHypervisorExecutor(Executor):
pass
return removed_priv, removed_seed
def cleanup_stale_secrets(self):
def cleanup_stale_secrets(self) -> list:
"""崩溃残留扫描(H5):私钥一律删、非 live VM 的 seed
同理删并审计;返回已清理 VM 名。worker 启动与
cleanup_stale_vms 时调它,不依赖 socket 是否存在。"""
@@ -1700,18 +1714,18 @@ class CloudHypervisorExecutor(Executor):
% (entry, removed_priv, removed_seed))
return cleaned
def shutdown_vm(self, task_or_name):
def shutdown_vm(self, task_or_name: dict | str) -> bool:
"""关闭入口的正式名:即 stop_vm(ch-remote shutdown)。
任务结束/失败/取消时调它。"""
return self.stop_vm(task_or_name)
def cancel_vm(self, task_or_name):
def cancel_vm(self, task_or_name: dict | str) -> bool:
"""取消入口:即 shutdown_vm。worker 的取消路径对
Docker 调 kill_container,对 CHV 调本方法——取消绝不
只改数据库状态,必须真关 VM。"""
return self.shutdown_vm(task_or_name)
def recycle_vm(self, task):
def recycle_vm(self, task: dict) -> dict:
"""回收 VM(任务结束后的销毁语义):
先 shutdown,再返回保留/丢失清单。保留的只有宿主侧
的 task_checkpoints / task_attempts(数据库里的断点与
@@ -1767,7 +1781,7 @@ class CloudHypervisorExecutor(Executor):
if dead:
self._vms.pop(name, None)
def cleanup_stale_vms(self):
def cleanup_stale_vms(self) -> list:
"""清理残留 VM:先跑残留盘 GC 与残留密钥扫描,再走
socket 扫描与登记清理。只清 socket 残留与已死进程
对应的 VM;state_dir 不存在返回 [],绝不抛异常。"""
@@ -1907,7 +1921,7 @@ class CloudHypervisorExecutor(Executor):
"""每任务 VM 生命周期(冷启动正式路径,feat-chv-arch95
定案):wake_vm(API 模式 + SSH 就绪)→ 跑客内命令 →
finally shutdown_vm。本方法绝不查 WarmPool:预热池是
实验性的(见 pool.py 头定案与 ADR-5),dispatch 永远
实验性的(见 experimental_pool.py 头定案与 ADR-5),dispatch 永远
冷启动,warm_pool_wired_to_dispatch 恒 False。
wake 失败时不跑
任务、直接抛分类失败。本方法不做内部重试:重试是
@@ -1951,7 +1965,7 @@ class CloudHypervisorExecutor(Executor):
except Exception:
pass
def execute(self, task) -> dict:
def execute(self, task: dict) -> dict:
"""只读执行:客内 host.py status,不直接跑模型回合
(与 Local/Docker 的 execute 同款语义)。
@@ -1987,7 +2001,7 @@ class CloudHypervisorExecutor(Executor):
"SSH 失败归类为未运行:%s" % exc)
raise
def dispatch_task(self, task, extra_env=None, model_provider=None) -> dict:
def dispatch_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
"""客内 host.py dispatch:建 host 任务并跑一轮 turn。"""
if not isinstance(task, dict) or task.get("id") is None:
raise ValueError("任务缺少 id")
@@ -2004,7 +2018,7 @@ class CloudHypervisorExecutor(Executor):
model_provider=model_provider,
sandbox=("read-only" if task.get("plan") else None))
def run_task(self, task, extra_env=None, model_provider=None) -> dict:
def run_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
"""客内 host.py run:在已有 host 任务上再跑一轮 turn。"""
if not isinstance(task, dict) or task.get("id") is None:
raise ValueError("任务缺少 id")
@@ -2017,7 +2031,7 @@ class CloudHypervisorExecutor(Executor):
effort=task.get("effort"), approval=task.get("approval"),
model_provider=model_provider)
def goal_task(self, task, extra_env=None, model_provider=None) -> dict:
def goal_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
"""客内 host.py goal:目标循环,轮次规则与 Local 同款
(缺省 3 轮,>100 轮 loud fail,旧验收形态 0 轮禁用)。"""
if not isinstance(task, dict) or task.get("id") is None:
@@ -2048,8 +2062,10 @@ class CloudHypervisorExecutor(Executor):
effort=task.get("effort"), approval=task.get("approval"),
model_provider=model_provider)
def goal_resume_task(self, task, goal_text=None,
max_rounds=None, extra_env=None, model_provider=None) -> dict:
def goal_resume_task(self, task: dict, goal_text: str | None = None,
max_rounds: int | None = None,
extra_env: dict | None = None,
model_provider: str | None = None) -> dict:
"""客内 host.py goal-resume:goal_text=None 为断点续跑
(不带 --goal-text,沿用原 goal+verify),给 goal_text
为开新目标(带 --goal-text 与 --max-rounds,默认 0)。"""
@@ -2067,9 +2083,9 @@ class CloudHypervisorExecutor(Executor):
effort=task.get("effort"), approval=task.get("approval"),
model_provider=model_provider)
def goal_resume_breakpoint_task(self, task,
model_provider=None,
extra_env=None) -> dict:
def goal_resume_breakpoint_task(self, task: dict,
model_provider: str | None = None,
extra_env: dict | None = None) -> dict:
"""客内 host.py goal-resume 断点续跑:无 --goal-text、
无 --max-rounds,host 沿用原任务从断点轮次续跑。
worker 层已接线(补缺口轨):非网关的 CHV 断点任务