986 lines
58 KiB
Markdown
986 lines
58 KiB
Markdown
# Cloud Hypervisor 执行器生产就绪清单
|
||
|
||
> 状态图例:✅ 代码已完成 · 🧪 mock 测试已覆盖(无真 VM) ·
|
||
> ⏳ 需 KVM 宿主实测 · 🏗️ 需镜像构建 · 📈 需压测 · 🔒 需安全审计
|
||
>
|
||
> 本清单是 PR 的一部分,不是事后补的口号。凡是标 ⏳/🏗️/📈/🔒
|
||
> 的项,在本轨内一律不声称已验证、已建成、已通过。
|
||
|
||
## 0. 交付点卡在哪一关(必读,先于逐项清单)
|
||
|
||
- **CI 门禁(未过,交付点 = PR OPEN)**:CI 必须通过才能合并。
|
||
此前全仓 CI 因 GitHub 账单问题 0 步骤失败;若账单未恢复、
|
||
CI 仍失败,本 PR 只到 OPEN,不合并,更不为变绿改 CI 配置
|
||
或绕过检查。
|
||
- **真 VM 门禁(未过,执行器默认关闭)**:生产启用(把真实任务
|
||
路由到 cloud-hypervisor、部署到生产主机)必须先有带 /dev/kvm
|
||
的宿主并完成真实 VM 启动、快照恢复、任务端到端验证。
|
||
当前事实:生产主机 124.222.165.170 本身就是 CVM
|
||
(systemd-detect-virt=kvm,DMI product=CVM),/dev/kvm 不存在、
|
||
无 vmx/svm 标志、无 kvm 模块、未装 cloud-hypervisor,无嵌套
|
||
虚拟化透传;本 VM 容器内同样无 /dev/kvm。
|
||
故真 VM、快照恢复、压测在本轨内一律不声称已验证;执行器
|
||
默认关闭(server 默认仍是 docker,worker 默认仍是 local,
|
||
CHV 只在任务创建时显式选 `executor=cloud-hypervisor` 且有
|
||
CHV worker 认领时才跑),不进入生产任务路由。
|
||
- **还差什么才能真正上生产**(按顺序):① 用户侧开通带 /dev/kvm
|
||
的裸金属或开启嵌套虚拟化的实例;② 在该宿主上构建并发布
|
||
chv-guest 镜像(见 §4);③ 在该宿主上跑通真实 VM 启动、
|
||
快照恢复、任务端到端、预热池与压测;④ 安全审计通过(§6);
|
||
⑤ CI 账单恢复且本 PR CI 变绿后,按当时 main 头复核 diff 再合并、
|
||
再部署。缺任何一项,都不把 CHV 推上生产。
|
||
|
||
## 1. 单任务单 VM 隔离
|
||
|
||
- ✅🧪 一个任务一个 VM:dispatch/run/goal 均走 wake → 跑 → finally
|
||
shutdown,同一任务重复 wake 先关旧 VM;VM 名、api-socket、
|
||
每任务磁盘路径均按任务派生,互不相同(测试断言)。
|
||
- ✅🧪 每任务磁盘独立:copy 模式复制基础镜像为每任务磁盘;
|
||
reuse 模式仅单租户实验,preflight 拦截(须显式实验二次确认并审计放行,feat-chv-fix 起代码真正拦截;此前“代码与文档双写”的说法是错的,代码当时并不拦截)。
|
||
- ✅🧪 租户无共享路径:执行器不挂载宿主根、宿主代码目录或含
|
||
密钥目录;客机代码在镜像内 `/app`,不在宿主共享挂载上。
|
||
- ✅🧪 密钥不进镜像/代码/prompt:SSH argv 只有 key 文件路径;
|
||
模型 key 由客机 cloud-init 预置的环境变量名注入;测试断言
|
||
命令中不含哨兵密钥值。feat-chv-secfix 起网关密钥不再走
|
||
SSH env 前缀(旧路径值进 argv 已废除),改走 provisioned
|
||
seed 盘客内文件(/etc/chv/guest-env,0600),keyscan 模式
|
||
下密钥投递 fail-closed;审计 detail 里绝无密钥值。
|
||
- ✅🧪 SSH host key 按 VM 钉死(优化轨,#216 的 no + /dev/null
|
||
已废除):每 VM 在 state_dir/<vm>/known_hosts 有独立文件,
|
||
首次启动经 ssh-keyscan 在解析出的 SSH 目标上捕获 key 写入该文件,
|
||
此后 SSH 一律 StrictHostKeyChecking=yes + 该文件校验;
|
||
非 root 客机用户(CHV_SSH_USER,显式空串直接拒绝)与端口
|
||
(CHV_SSH_PORT,1-65535)为显式配置。mock 只验证捕获→
|
||
写文件→钉死选项的管线,不验证首次密钥真实性。
|
||
- ✅🧪 Host key 预置方案(补缺口轨,替代 TOFU 为生产路径):
|
||
宿主在启动前为每 VM 生成一次性 ed25519 host key 对,
|
||
公钥立即写进该 VM 的 known_hosts 钉死,私钥只进 per-VM
|
||
cloud-init seed(随 VM 关闭/回收而删),客机镜像硬性
|
||
要求见 chv-guest/README.md §5(cloud-init NoCloud、
|
||
首启先应用 seed 再起 sshd、不得无条件重生成 host key)。
|
||
ssh-keyscan 首次捕获只保留为弱回落(须显式设
|
||
CHV_HOST_KEY_MODE=keyscan),生产默认值已是 provisioned
|
||
(feat-chv-secfix,H4,此前默认值 keyscan 是反的);TOFU 首次
|
||
真实性仍待预置 key 方案在真机上验证,安全审计时须单独立项确认。
|
||
- 🔒 需安全审计:VM 逃逸面(virtio 设备、tap 桥接、SSH 配置)、
|
||
客机 root 提权面、租户间东西向隔离的宿主网络配置,须由
|
||
安全审计逐项确认,代码自查不能代替审计。
|
||
|
||
## 2. 快速启动:预热快照池 + 镜像缓存
|
||
|
||
- ✅🧪 快照/恢复接口:snapshot_vm / restore_vm(restore 后显式
|
||
resume,见 §3),WarmPool 接口与状态机(cold → prewarming →
|
||
warm → assigned → running → recycled/destroyed)。
|
||
- ⚠️ 定案(feat-chv-arch95,ADR-5):WarmPool 是实验性、非生产
|
||
路径。dispatch 永远冷启动,_run_with_vm 绝不查池;池空回落
|
||
冷启动的说法不再作为生产特性宣称,acquire/resume 只保留为
|
||
实验接口。
|
||
- ✅🧪 预热条目一次性:acquire 消耗、release 一律销毁,绝不跨
|
||
任务复用脏 VM。
|
||
- ✅🧪 预热池 registry 与孤儿清理(优化轨):池状态落
|
||
state_dir/warm-pool.json(原子写),worker 重启后载入的
|
||
条目一律当孤儿、只能 reconcile_orphans() 清理,绝不重新
|
||
发给任务;registry 损坏时 quarantine 成 .corrupt 并从空池
|
||
开始;registry 之外的残留 socket 仍由 cleanup_stale_vms()
|
||
扫描处理。
|
||
- ⏳ 需 KVM 宿主实测:预热池的真实冷启动耗时、resume 耗时、
|
||
池大小与命中率、快照来源一致性,均需真机数据,mock 数字
|
||
不能当性能结论。
|
||
- 📈 需压测:多 VM 并行预热/分配/销毁的宿主 CPU/内存/磁盘
|
||
压力、快照存储增长,均未测。
|
||
|
||
## 3. 完整生命周期
|
||
|
||
- ✅🧪 create / boot:API 模式 wake_vm(vmm.ping → vm.create →
|
||
vm.boot → SSH 就绪)。
|
||
- ✅🧪 pause-resume(sleep-wake):pause_vm / resume_vm(ch-remote)。
|
||
- ✅🧪 snapshot-restore:restore_vm(restore + 显式 resume +
|
||
SSH 就绪),恢复后默认 paused 的坑已在代码与测试里钉住。
|
||
- ✅🧪 hibernate:CHV 无原生 hibernate 端点,本轨用 pause +
|
||
snapshot + shutdown 等价实现,文档与代码双写“等价实现”,
|
||
不声称接了原生端点;唤醒走 restore_vm。
|
||
- ✅🧪 shutdown-destroy / recycle:shutdown_vm(ch-remote
|
||
shutdown + 杀进程 + 删 socket)、recycle_vm(保留
|
||
task_checkpoints/task_attempts,丢失 guest /tmp 与未拷出
|
||
的 worktree 临时物,绝不声称恢复临时文件)。
|
||
feat-chv-secfix 起 stop 即删任务 disk.raw(保留期 0,
|
||
需取证须在 shutdown 前经 SSH 拷出),残留盘由
|
||
gc_stale_disks 扫描清理,删失败记审计 + 脏标记。
|
||
- ✅🧪 execute 只读已运行 VM(优化轨):execute() 先查 VM
|
||
是否在运行(登记活进程或 api-socket 存在),未运行报
|
||
ChvFailure(kind=vm_not_running),不为一次 status 起 VM,
|
||
也不向不存在的端口发 SSH 再拿连接失败冒充任务状态。
|
||
- ✅🧪 长任务中途死亡判 interrupted(优化轨):客内命令抛
|
||
传输异常、或非 goal 结果已 failed 且登记 VMM 进程已死时,
|
||
_run_with_vm 改判 interrupted(断点可续跑)而非笼统
|
||
failed;VM 还活着时的异常/失败原样处理。中途心跳未做:
|
||
host.py 单次 SSH 长回合没有客内进度信号可采,另起一路
|
||
SSH 轮询只能证明 VM 活着、不能证明任务在进展,评估后
|
||
认为风险大于收益,宁可只做死亡分类。
|
||
- ⏳ 需 KVM 宿主实测:上述全部生命周期在真机上的状态转换、
|
||
快照恢复后 guest 状态一致性、休眠后文件/worktree 保留,
|
||
均未在真机验证。
|
||
|
||
## 4. 完整开发环境(客机镜像)
|
||
|
||
- ✅ 客机镜像内容清单:`chv-guest/README.md` 按
|
||
`cloud/docker/Dockerfile` 能力对齐(Python 3.12、git、
|
||
pandoc/poppler/latex、python-docx/openpyxl/pptx/PyMuPDF/
|
||
Pillow、codex 0.164.0-alpha.9 成套、Playwright + Chromium、
|
||
openssh-server、cloud-init)。
|
||
- ✅ 构建脚本实做(补缺口轨):`chv-guest/build.sh`(参数校验、
|
||
KVM 宿主检查、下载 Ubuntu 云镜像、qemu-img 转 raw、
|
||
virt-customize 装清单、拷入 /app 代码、virt-get-kernel、
|
||
BUILDINFO.json 生成与 tag 校验;--dry-run 只打印命令)。
|
||
- 🏗️ 需镜像构建:镜像本身未构建、未发布;BUILDINFO 版本核对、
|
||
浏览器版本钉死、中文 LaTeX 单独一版,均待构建时做。
|
||
- ⏳ 需 KVM 宿主实测:镜像在真 VM 里启动、codex/Playwright/
|
||
git/文档工具链逐项跑通,均未验证。
|
||
|
||
## 5. 资源与成本
|
||
|
||
- ✅🧪 每 VM 配额:CPU/内存进 VmConfig(CHV_CPU/CHV_MEM),
|
||
磁盘配额门(CHV_DISK_GB,默认 50 GB 总量预算、超限拒绝,
|
||
feat-chv-secfix 起默认开启,此前未设即不查),快照上限
|
||
(每 VM 数量默认 10、总量默认 20 GB、保留期默认 30 天,
|
||
超量最旧优先剪枝),宿主容量门
|
||
(CHV_MAX_VMS,超限明确拒绝排队,不降级;残留 socket
|
||
须真连通才计数,死 socket 不再占满容量)。
|
||
- ✅ 超时与最大轮次:SSH 单次超时(CHV_TASK_TIMEOUT)+
|
||
worker task_timeout 外层护栏;goal 最大轮次 100(与 Local
|
||
同款),执行器内部不重试(重试是 worker max_retries 语义)。
|
||
- ✅ 成本复用:不另起一套计费;worker 现有 quota/成本语义
|
||
(per-user USD 配额、quota 看门狗、成本写库)仍是唯一口径,
|
||
执行器只把 host.py 用量原样返回。
|
||
- ✅ 空闲休眠省成本:hibernate_vm(pause + snapshot +
|
||
shutdown)+ WarmPool 释放即销毁,空闲 VM 不占 vCPU/内存
|
||
(快照只占磁盘,见 §2 压测项)。
|
||
- ✅ autoscaler:按 executor 过滤队列深度(已有)+ 本轨透传
|
||
`--executor` 给拉起的 worker(否则 CHV 池永远饿死)。
|
||
- 📈 需压测:配额下的真实资源占用、超限拒绝的排队行为、
|
||
成本与配额在长任务下的实际表现,均未测。
|
||
|
||
## 6. 网络与治理
|
||
|
||
- ✅🧪 受控 egress:默认 fail-closed(CHV_EGRESS_PROXY 未设且
|
||
未显式允许直连时 preflight 直接失败);客机命令加 env
|
||
代理前缀,出口只走白名单代理(Tinyproxy 思路复用)。
|
||
feat-chv-secfix 措辞纠正:env 前缀只是代码门,不是网络层
|
||
隔离——客内 root 可 unset/裸 socket 绕过;真正的隔离是
|
||
“代码门 + 宿主防火墙缺一不可”(见 §11 部署清单),
|
||
代码不再把 fail-closed 写成已隔离。
|
||
- ⏳🔒 需宿主网络配置 + 安全审计:tap 桥接是否真的只路由到
|
||
白名单代理、VM 能否绕过代理直连外网,是宿主网络配置问题,
|
||
执行器代码不能自证,必须在 KVM 宿主上实测并审计。
|
||
代码已做:每 VM 独立 tap/mac/ip 校验(共享即拒绝并审计)、
|
||
net 条目字段白名单、egress 代理地址校验。
|
||
- ✅ 审批门:任务 approval 经 `_approval_cli_args` 进客内 host.py
|
||
顶层参数,VM 内 agent 不能自改;delete/deploy/spend 类动作
|
||
仍走 server 的 approval/merge_gate,不由 VM 内 agent 自批
|
||
(本轨不改 approval/merge_gate 语义,只复用)。
|
||
- ✅ 工具与系统调用范围收敛:VM 边界本身就是系统调用边界
|
||
(客机独立内核);plan 任务走 read-only 沙箱参数;最大步数
|
||
(goal 100 轮)、时间(task_timeout)、重试(worker 1 次)
|
||
超限即停并审计(vm_failed 带 kind)。
|
||
|
||
## 7. 可观测与审计
|
||
|
||
- ✅🧪 VM 事件审计:create/boot/ready/pause/resume/snapshot/
|
||
restore/shutdown/destroy/failed/recycled/warm_pool_acquire/
|
||
release 均经 audit_callback 发出;worker 接线后转 AuthStore
|
||
审计(_audit 本身 best-effort)。feat-chv-secfix 起补齐:
|
||
host_key_generated/host_key_captured/host_key_mismatch、
|
||
preflight_failed、wake 前置失败(容量/配额/net 冲突)、
|
||
vm_disk_deleted/vm_disk_delete_failed/vm_disk_gc、
|
||
host_secret_residue_cleaned、net_config_dual、快照剪枝;
|
||
审计回调失败记宿主 error 日志,不再双重静默。
|
||
- ✅🧪 失败分类:ChvFailure 带 kind(kvm_missing/binary_missing/
|
||
kernel_missing/image_missing/ssh_key_missing/cpu_flag_missing/
|
||
egress_not_configured/api_timeout/guest_not_ready/vm_create_failed/
|
||
vm_boot_failed/host_capacity_exceeded/disk_quota_exceeded),
|
||
不笼统 failed。
|
||
- ✅ 断点对齐(补缺口轨已接线):VM 快照/休眠与现有
|
||
task_checkpoints/task_attempts 语义对齐(recycle 保留两者,
|
||
丢失临时文件);worker 通用分支对任何提供
|
||
goal_resume_breakpoint_task 的执行器调用它(Local 仍走
|
||
原路径,网关任务仍被 loud fail 守卫拦住);WarmPool
|
||
reconcile_orphans 已由 worker 启动与关闭时显式调用,
|
||
失败不炸主流程但进审计;VM 中途死亡改判 interrupted
|
||
后走同一断点路径,不再落 failed 死信(见 §3 与新增测试)。
|
||
- ⏳ 需 KVM 宿主实测:审计事件在真机任务 timeline 里的完整
|
||
呈现、失败分类与真实故障的对应关系,均未验证。
|
||
|
||
## 8. 多 VM 并发与容量
|
||
|
||
- ✅🧪 worker 注册:`_executor_name()` 返回 cloud-hypervisor,
|
||
worker register/heartbeat 已上报 executor + capacity(现有
|
||
机制复用,无需新表)。
|
||
- ✅🧪 多 VM 并行:`_vms` 登记 + 锁保护,wake 按任务名登记,
|
||
不同任务互不覆盖;容量门在 wake 前检查。
|
||
- ✅🧪 残留 VM 清理:cleanup_stale_vms 处理已死/已关/socket
|
||
残留三种情况,只清 chv- 前缀 socket;feat-chv-secfix 起
|
||
清理前先跑残留盘 GC 与残留密钥扫描(不依赖 socket 是否
|
||
存在,覆盖 worker 崩溃后 socket 已删、文件仍在的情形)。
|
||
- ✅🧪 容量不足明确拒绝:kind=host_capacity_exceeded,调用方
|
||
排队/拒绝,绝不降级 Docker/Local(测试断言 launcher 未调用)。
|
||
- 📈 需压测:多 VM 并行下的宿主容量、VM 启动并发上限、残留
|
||
清理在高压下的表现,均未测。
|
||
|
||
## 9. 默认关闭与生产启用条件(重申)
|
||
|
||
- ✅ 默认关闭:server 默认执行器仍是 docker,worker 默认 local;
|
||
CHV 只在显式选 executor 且有 CHV worker 时才跑;无 CHV_*
|
||
配置时 preflight 直接失败。
|
||
- ⏳ 生产启用前置(全部未完成):带 /dev/kvm 的宿主开通 →
|
||
chv-guest 镜像构建发布 → 真实 VM 启动/快照恢复/任务端到端
|
||
验证 → 压测 → 安全审计 → CI 变绿后按当时 main 头复核 diff
|
||
再合并部署。缺一不可。
|
||
|
||
## 10. 审核修复轨 feat-chv-fix(堆叠于 #230 之上)
|
||
|
||
- ✅🧪 P0 已修:seed 磁盘(CIDATA ISO,第二块只读盘)挂进
|
||
VmConfig;restore 不回落 keyscan(provisioned 下复用已钉
|
||
死 known_hosts、缺失 fail-fast,快照内 host key 不重预置);
|
||
reuse 由 preflight 拦截(显式实验二次确认 + 审计放行);
|
||
SSH 目标选客机 IP 方案(显式 host > net 静态 ip > 显式
|
||
转发宣称下的回环,三者皆无 fail-fast;vsock 与纯 NAT 方案
|
||
未选,理由见 config.py)。
|
||
- ✅🧪 P1 已修或有结论:网关 extra_env 经 SSH env 前缀注入,
|
||
worker 守卫收窄到 Local/CHV;取消/清理/看门狗接 CHV 通用
|
||
路径;WarmPool acquire 内 resume(本轨定案为实验性,见 ADR-5,
|
||
不再是生产特性);容量门改持久化
|
||
计数(socket 联合计数);磁盘配额改总量+余量、已存在磁盘
|
||
校验重拷;API 客户端按 Content-Length 验完整性;快照
|
||
file:// 目标验存在且非空再 shutdown;execute 死端口归类
|
||
vm_not_running。
|
||
- ⚠️ 仍未验证(不得声称已交付):CHV + 网关真实任务;真机 wake/boot/SSH(含 seed
|
||
注入与 NAT/客机 IP 实测);快照恢复端到端;压测;安全审计;
|
||
CI 账单恢复。另:P0-2 审计原说法“restore 须把 seed 接入
|
||
VmConfig”已纠正——restore 不走 vm.create,seed 无从挂盘,
|
||
正确不变量是复用钉死 known_hosts。
|
||
- ✅🧪 P2 已做(范围已恢复为全做,上一条收窄令作废):
|
||
test_chv_gaps.py 的 grep 文本断言改成真实 Worker._run_attempt
|
||
行为断言;stop_vm 删除失败记 vm_key_cleanup_failed 审计
|
||
(同名复用旧 key 的彻底根治在本轨之前列为遗留——
|
||
feat-chv-secfix 起已根治:清理失败写脏标记,同名 VM
|
||
拒绝启动,见 §11 M2);配置校验时机统一到 preflight(CPU/内存非法
|
||
进 preflight,kind=config_invalid);build.sh 与 README 口径
|
||
对齐(章节顺序 §5 host key → §6 对接、BUILDINFO contents
|
||
纠正为名字列表无逐项版本、Node.js 不装写实);第二启动路径
|
||
清理(build_vm_command 降级为私有 _build_vm_command,手动
|
||
等价写法保留;Docker goal 内联解析顶部加指向正典
|
||
parse_goal_result 的分叉说明 + 同步检查清单);长函数只做
|
||
小提取(preflight 的 reuse/CPU/内存校验块抽成
|
||
_preflight_config_checks,语义不变;config.py 字段解析与
|
||
其余长函数深耦合,本轨如实停、不硬拆)。
|
||
|
||
## 11. 安全审计修复轨 feat-chv-secfix(堆叠于 #239 之上)
|
||
|
||
> 审计报告:`~/workspace/chv-security-audit/REPORT.md`(只读静态
|
||
> 审计,verdict No-Go,Critical 0 / High 7 / Medium 8 / Low 5)。
|
||
> 本节逐项给结论:代码已做 = 本轨代码 + mock 测试已覆盖;
|
||
> 真机未验证 = 代码改了,但动态实效仍须 KVM 宿主验证,
|
||
> 绝不把静态通过写成动态已验。审计 verdict 本身仍是 No-Go,
|
||
> 直到 §11 末的真机清单全部通过——本轨只修代码层,不改 verdict。
|
||
|
||
### 11.1 High(全部代码已做、真机未验证)
|
||
|
||
- H1 egress 网络层强制:代码能做的全部做了——egress 代理
|
||
地址校验(http/https + 主机名)、直连仍须显式允许 + 审计、
|
||
每 VM 独立 tap/mac/ip 校验(共享即拒绝并审计)、net 条目
|
||
字段白名单。代码做不到的宿主防火墙规则见 §11.4 部署清单,
|
||
文档明写“代码门 + 宿主防火墙缺一不可”;env 前缀不再被
|
||
写成已隔离。
|
||
- H2 网关 key 移出 argv:密钥子集(SUB2API_API_KEY)在
|
||
provisioned 模式下随 seed 写进客内 /etc/chv/guest-env
|
||
(0600 root),远程命令 source 该文件;传输层对密钥名
|
||
进 argv 直接 ValueError(fail-closed)。比 argv 安全的理由:
|
||
值既不在宿主进程表(ps /proc cmdline)里,也不在宿主子
|
||
进程环境(/proc environ)里,只活在 seed.iso(0600,随
|
||
VM 销毁而删)与客机内——客内 agent 本来就有该 key,修
|
||
的是“宿主暴露面”,不是“客内可见性”。与 Docker 线的差异:
|
||
Docker 是“-e 只传名、值由 CLI 子进程环境提供”(值仍在
|
||
宿主进程环境里),CHV 是“值只进 seed 客内文件”(宿主
|
||
进程环境里也没有),比 Docker 线更严。keyscan 模式无
|
||
seed 可投递,有密钥即 fail-closed(secret_env_requires_
|
||
provisioned),不再将就跑。残留清理:seed 目录与 seed.iso
|
||
随 stop 而删,崩溃残留由 cleanup_stale_secrets 扫描。
|
||
- H3 任务盘删除:stop/recycle/cleanup 路径删 disk.raw
|
||
(保留期 0,需取证须在 shutdown 前经 SSH 拷出),删失败
|
||
记 vm_disk_delete_failed 审计 + 脏标记;gc_stale_disks
|
||
扫描非 live VM 的残留盘并审计。
|
||
- H4 默认值改 provisioned:CHV_HOST_KEY_MODE 默认
|
||
provisioned,keyscan 只作显式 opt-in;旧测试的
|
||
CHV_SSH_FORWARD=1 等实验室宣称已明确标注为实验室路径,
|
||
不再写成生产默认。
|
||
- H5 seed.iso 权限与私钥清理:seed.iso 生成即 0600;宿主
|
||
私钥在 seed 盘建好后即删(只留公钥 + known_hosts),删
|
||
失败 fail-fast(VM 尚未启动);私钥 chmod 失败不再静默;
|
||
启动/清理时 cleanup_stale_secrets 扫描残留私钥与 seed。
|
||
- H6 state_dir 出 /tmp:默认
|
||
~/.local/share/codex-app-server/chv-vms,preflight 建目录
|
||
并保证 0700(失败即 state_dir_insecure);api-socket 组/
|
||
其它可写即 fail-fast(socket_permissions_insecure;仅禁
|
||
写位,VMM 默认 0755 类权限不误杀——VMM 默认值代码控制
|
||
不了,真机权限仍须按清单核对)。
|
||
- H7 tap 隔离校验:NET_JSON 透传前校验 tap/mac/ip 跨 VM
|
||
不重复(wake 成功后登记 net.json,stop 时释放预约),
|
||
共用即拒绝并审计;东西向隔离的宿主防火墙部分见 §11.4,
|
||
不写进代码假装已防。
|
||
|
||
### 11.2 Medium(每项有修复或有理由的结论)
|
||
|
||
- M1 SSH 远程命令转义:远程 token 全部 shlex.quote 后仍以
|
||
列表返回(ssh 拼接后远程解析结果与本地 list 语义一致),
|
||
prompt 里的 ;/$()/引号不再改写执行语义。边界写清:客内
|
||
已是 root,本项防的是语义篡改与审计失真,不是客内提权。
|
||
- M2 清理失败同名复用旧 key:彻底根治,选“清理失败即拒起”
|
||
(脏标记 + provision/wake 双重拒绝),不再写“已审计即可”。
|
||
- M3 磁盘配额默认与快照上限:CHV_DISK_GB 默认 50 GB
|
||
(未设不再不查,0/负数拒绝);快照每 VM 数量默认 10、
|
||
总量默认 20 GB(超帽最旧优先剪枝)、保留期默认 30 天。
|
||
- M4 供应链版本钉死:已钉死——云镜像 SHA256 必填(干跑验
|
||
“已设”、全量验哈希)、Ubuntu 版本与 codex 版本均为必填
|
||
环境变量;BUILDINFO 新增 versions(构建时从客机读回的
|
||
dpkg + pip freeze 实际版本,可审计、可复现比对)。未钉死
|
||
的清单:apt/pip 事前精确版本 lockfile(须在构建宿主上
|
||
生成一次、再固化进脚本,本轨无构建宿主、未编造版本号);
|
||
Playwright Chromium 二进制版本同样只随 playwright pip
|
||
版本记录,未单独钉死。
|
||
- M5 审计补齐:host_key_generated/host_key_captured/
|
||
host_key_mismatch(stderr 含钉死拒绝标记时立即 fail-fast,
|
||
不轮询成普通超时)、preflight_failed、wake 前置失败
|
||
(容量/配额/net 冲突)均独立审计;审计回调失败记宿主
|
||
error 日志,不再双重静默。worker 层 _audit 本身仍
|
||
best-effort(未改,见 §7 既定语义)。
|
||
- M6 reuse 实验标志单租户校验:代码能校验的已校验——
|
||
实验放行须同时声明 CHV_SINGLE_TENANT_PROJECT,否则
|
||
preflight 拒绝(reuse_tenant_undeclared);不能校验的
|
||
部分明写:代码无法验证数据库里是否真的只有一个租户,
|
||
声明 + 审计就是本项的机制,不假装已校验。
|
||
- M7 容量计数残留 socket 误伤:只有真能连通的 socket 才
|
||
计数,空文件/死 socket 不计入 CHV_MAX_VMS;竞态窗口
|
||
写清(刚启动未 listen 的 VMM 会被判死,容量门是护栏
|
||
不是分布式锁)。
|
||
- M8 sshd 加固进构建:build.sh 写入
|
||
/etc/ssh/sshd_config.d/99-chv.conf(PasswordAuthentication no、
|
||
PermitRootLogin prohibit-password、ChallengeResponseAuthentication no、
|
||
KbdInteractiveAuthentication no、PubkeyAuthentication yes),
|
||
并经 sshd -t + grep 断言;README 声称与构建一致。
|
||
|
||
### 11.3 Low(顺手做)
|
||
|
||
- L1 磁盘拷贝符号链接:目标是链接时先删链接本身再拷贝,
|
||
不跟随写入目标文件(TOCTOU 窗口已在 disk.py 写清,
|
||
state_dir 0700 是前提)。
|
||
- L2 磁盘/快照路径白名单:task_disk/seed_disk 须在
|
||
state_dir 内(reuse 的基础镜像本身除外);file:// 快照
|
||
须在 state_dir 或 CHV_SNAPSHOT_DIR 内,远端协议须显式
|
||
CHV_ALLOW_REMOTE_SNAPSHOT=1。
|
||
- L3 失败审计:preflight/容量/配额/net 冲突失败均发
|
||
vm_failed 审计(见 M5)。
|
||
- L4 CHV_NET 与 CHV_NET_JSON 双变量:双设时发
|
||
net_config_dual 警告审计(不硬拒,CHV_NET 是手动直接
|
||
启动命令的透传值);NET_JSON 条目字段白名单
|
||
(tap/mac/ip/mask),未知字段直接拒绝。
|
||
- L5 WarmPool 预热盘与任务盘对应关系:定义已写进
|
||
pool.acquire 文档——预热盘是干净快照盘,不是任务盘;
|
||
本轨定案为实验性(ADR-5),不再作为接线待办。
|
||
|
||
### 11.4 宿主防火墙部署清单(代码做不到的部分,缺一不可)
|
||
|
||
- 每 VM 独立 tap + 独立 mac + 独立静态 ip(运维分配,
|
||
代码在 wake 时校验冲突,但分配本身是运维责任);
|
||
- tap 桥接默认 DROP,只放行到 egress 代理的流量
|
||
(iptables/nftables:默认 DROP + 仅代理地址/端口 ACCEPT,
|
||
不给 tap 直连外网的 NAT,不把 tap 桥接到宿主内网网段);
|
||
- 东西向隔离:VM 间默认禁通(每 VM 独立 tap + 桥接隔离,
|
||
或每 tap 独立网段 + 转发禁令),ARP spoof 场景下 SSH
|
||
钉死仍是第二道门,不是唯一门;
|
||
- 宿主进程隔离:worker 用户最小化、VMM api-socket 所在
|
||
state_dir 0700 且仅 worker 用户可访、宿主启用 hidepid
|
||
收紧 /proc 可见性、进程表采集不记录完整 cmdline;
|
||
- 部署后核对:state_dir 权限、socket 实际权限、
|
||
tap 规则实测(见 §11.5 否定测试)。
|
||
|
||
### 11.5 仍需真机验证的清单(本轨一律未做,不声称已验)
|
||
|
||
1. 否定测试:客内 unset 代理后 curl 外网必须不通;
|
||
VM A 扫 VM B / spoof B 的 ip 后 SSH 必须被钉死拒绝;
|
||
2. provisioned 首启时序:seed 先应用再起 sshd、不重生成
|
||
host key;sshd -T 实际输出与密码登录关闭;
|
||
3. 宿主 SSH argv 实测:真机 ps 下网关 key 不可见;
|
||
4. 长跑/崩溃注入:残留盘总量、快照增长、残留 socket 与
|
||
seed/私钥残留的实际表现、GC 覆盖;
|
||
5. 压测:配额与容量门在并发下的实际表现;
|
||
6. 渗透类:任何以“拿到宿主 shell / 读到别租户数据”为
|
||
目标的真实攻击尝试——本轨一律未做,静态通过不等于
|
||
已证明不可逃逸;
|
||
7. 审计 verdict:以上全部通过后,才能把 verdict 从 No-Go
|
||
改为“有条件启用(默认关闭 + 灰度单租户/受信项目)”,
|
||
不直接全量多租户。
|
||
|
||
### 11.6 本轨明确不做
|
||
|
||
- VM 控制按钮、预热池管理页、成本/配额新页面(前端零改动);
|
||
- 真机动态渗透、压测(无 KVM,做不了,只列清单);
|
||
- WarmPool dispatch 前查池接线(已定案不做,见 ADR-5;重启须先
|
||
实现任务盘重供给并补不变量测试)。
|
||
|
||
## 12. 架构 9.5+ 提升轨 feat-chv-arch95plus(堆叠于 #252 之上)
|
||
|
||
> 验证方式标注(本轨新增,逐条不混淆):静态 = 代码 + mock
|
||
> 测试已覆盖;真机 = 须 KVM 宿主验证;压测 = 须并发负载验证。
|
||
> 凡写“代码已做”而未标真机/压测的,一律只代表静态通过。
|
||
|
||
- 长函数:cloud/chv/ 全部函数 ≤50 行(静态,mock 测试覆盖
|
||
提取后的 stop/wait/restore/清理/接收/校验/组装段)。
|
||
- WarmPool 生产隔离:生产路径只调 reconcile facade(静态);
|
||
预热收益仍是实验性,真机收益未验证(真机)。
|
||
- #215 对齐:目录门与 tmpfs 口径已落代码(静态);超时与
|
||
美元配额无映射,#215 合并后替换为 import 仍须重跑测试
|
||
(合并后,静态)。
|
||
- 审计失败:记日志 + 计数 + 不中断(静态);审计完整性在
|
||
DB 故障下的实际表现仍须真机/故障注入验证(真机)。
|
||
- 供应链:BUILDINFO 事后 lockfile 校验脚本已落(静态);
|
||
事前 apt/pip 精确 lockfile 仍缺(须构建宿主,外部)。
|
||
- 只读 VM 字段:chv_vm_dead 派生自 worker 已写回的
|
||
interrupted_reason(静态,mock 测试覆盖);live VM 状态
|
||
仍无字段,VM 控制按钮仍不做(后端无控制端点 + 安全 No-Go)。
|
||
- 合并前 diff 复核清单(合并时逐项打勾,不在本轨内做):
|
||
server 白名单、worker 断点分支、sandbox re-export、
|
||
console.html 构建产物与 src 一致、#215 合并后的 import
|
||
替换点。
|
||
- 真机验证清单(沿用 §11.5,另加本轨项):VM 死亡横幅在
|
||
真任务中断后是否出现、审计失败计数在 DB 故障时是否
|
||
累加且任务不中断、快照白名单目录门在符号链接逃逸时
|
||
是否拒绝。
|
||
- 灰度启用清单(启用时逐项打勾):CI 变绿 → 按 #216→#252→
|
||
本 PR 顺序合并 → KVM 宿主 + 防火墙清单 → 镜像构建发布
|
||
→ §11.5 + 本节真机清单全过 → 默认关闭下单租户/受信项目
|
||
灰度 → 全量多租户(须另行决策,不在本轨内默认开启)。
|
||
|
||
## 13. to10 提升轨 feat-chv-to10(堆叠于 #257 之上)
|
||
|
||
本节是准备工作,不是启用结论:代码侧的 to10 项已做,
|
||
生产就绪与动态安全仍未到 10 分(见 §13.4 外部关卡)。
|
||
|
||
### 13.1 合并前 diff 复核清单(以当前 main 头预填)
|
||
|
||
- 当前 main 头(本轨核查时):`aebc6238a613066316ec18796b83ea8fc2d8835e`
|
||
(Merge PR #201)。链上 10 个 PR 在核查时全部
|
||
OPEN / MERGEABLE(#216 base main,#221→#257 依次堆叠,
|
||
本 PR base 为 feat-chv-arch-maint-fe)。合并时必须重新
|
||
核查:main 头若已前进,整条链须逐个 rebase 并重跑测试,
|
||
绝不直接合并旧基底。
|
||
- 逐项复核点(合并执行者逐项打勾,任一项不符即停):
|
||
1. server 任务创建白名单含 `cloud-hypervisor`,环境创建
|
||
白名单为 local/docker/cloud-hypervisor(不含 cloud),
|
||
两处白名单差异仍在前端文案里写清。
|
||
2. worker 断点分支:Local 原路径不变,CHV 经
|
||
`worker_adapter.call_goal_resume_breakpoint`(薄包装名
|
||
`_call_goal_resume_breakpoint` 仍在 worker.py,旧测试
|
||
导入点不变)。
|
||
3. sandbox re-export:`cloud/chv` 不再 export WarmPool
|
||
(#257 已移实验模块),`cloud/sandbox.py` 的 CHV
|
||
re-export 与 `cloud/chv/__init__.py` 一致。
|
||
4. 前端产物:`cloud/console.html` 与 `dist/index.html`
|
||
的 sha256 一致(本轨:`269820d1…f389`,353163 字节),
|
||
产物内含新文案(“无法判断 VM 状态”1 处、
|
||
“CHV worker 在线数未知”3 处);合并时重建后复核,
|
||
不直接沿用本轨哈希。
|
||
5. `package.json` 的 vitest 测试脚本与 lockfile 增量只含
|
||
vitest 新条目,无旧包版本变动。
|
||
6. 本轨新增文件:`cloud/chv/worker_adapter.py`、
|
||
`cloud/tests/test_chv_to10.py`、
|
||
`chv-guest/generate_lockfile.py`;本轨改动文件:
|
||
`cloud/worker.py`(薄包装 + 删多余 `inspect` import)、
|
||
`cloud/chv/executor.py`(审计落盘 + 两函数抽 helper)、
|
||
`cloud/chv/config.py`、`disk.py`、`hardening.py`、
|
||
`hostkeys.py`、`ssh_transport.py`、`experimental_pool.py`、
|
||
`compat215.py`(类型注解 + ruff 整理)、前端 6 文件。
|
||
7. CI:链上全部 PR 的 `verify` 仍是账单故障秒级失败模式
|
||
(本轨核查时 #257 等均为 UNSTABLE,非代码失败);
|
||
CI 变绿前不合并,绝不改 CI 配置绕过。
|
||
8. 旧链 #199→#205→#207 与 #209–#215 仍全部 OPEN:
|
||
合并前复核它们与本链的交集(server 白名单、worker
|
||
断点分支、sandbox re-export),有冲突先解决冲突方,
|
||
不静默覆盖。
|
||
|
||
### 13.2 真机验证清单(用例与验收标准细化)
|
||
|
||
以下用例全部待 KVM 宿主,验收标准是“通过才算”,不是
|
||
“跑过就算”;任一用例未跑即记为未验证,不写进已验。
|
||
|
||
- V-1 冷启动:CHV worker 在 KVM 宿主上起 VM → guest
|
||
SSH 就绪(钉死 known_hosts,provisioned 模式)→ 跑通
|
||
一个真实任务(host.py 在客机内执行)→ 任务完成且 VM
|
||
已销毁、任务盘已删。验收:任务状态 completed,宿主
|
||
state_dir 无该任务残留盘与 seed。
|
||
- V-2 seed 首启时序:provisioned 模式下客机 cloud-init
|
||
在 sshd 启动前应用 seed(host key 不再生、客内密钥文件
|
||
0600)。验收:`sshd -T` 显示 PasswordAuthentication no
|
||
等加固项,known_hosts 钉死校验通过,非 TOFU 回落。
|
||
- V-3 快照恢复:任务中途 hibernate(pause + snapshot +
|
||
shutdown)→ restore 后显式 resume → 断点续跑完成。
|
||
验收:恢复后 known_hosts 不变(不重预置),快照文件
|
||
落盘非空且经 `snapshot_not_persisted` 校验。
|
||
- V-4 网关真实任务:sub2api 网关任务在 CHV 上跑通,
|
||
密钥只走 seed 客内文件,宿主 `ps` / `/proc/*/cmdline`
|
||
里看不到密钥值。验收:任务完成 + 宿主进程表抽查无密钥。
|
||
- V-5 取消与配额真杀:同步 SSH 阻塞中取消任务、配额超限
|
||
时,看门狗调 `cancel_vm` 真关 VM。验收:VM 进程消失,
|
||
任务状态 interrupted 且原因写回正确。
|
||
- V-6 审计落盘:DB 审计回调失败时,`chv-audit-spill.jsonl`
|
||
里有该事件行、`audit_failure_count` +1、任务不中断。
|
||
验收:落盘行 event/task_id/detail 与丢失事件一致。
|
||
- V-7 动态安全负向:客机 root unset 代理后直连(应被
|
||
tap/防火墙挡住)、跨 VM 探活与 IP 冒用(SSH 钉死应拒)、
|
||
崩溃注入后残留扫描(私钥/seed/盘应被清理或脏标记拒起)。
|
||
验收:每一项都有“被挡住”的证据,不是有代码就过。
|
||
- V-8 负载:4 VM 并发(`CHV_MAX_VMS` 默认值)下容量门
|
||
计数含重启后残留 socket 扫描,死 socket 不占名额、活
|
||
VM 不被超发。验收:并发任务全部完成或明确排队拒绝,
|
||
无静默降级到 Docker/Local。
|
||
- V-9 供应链清单:`generate_lockfile.py` 在构建宿主上跑
|
||
出真实 lockfile,`--verify` 通过且 unresolved 为空;
|
||
再用 `check_lockfile.py` 对 BUILDINFO.versions 复核。
|
||
验收:lockfile 由构建宿主产出,不是本轨合成数据。
|
||
|
||
### 13.3 灰度启用清单
|
||
|
||
- 阶段 0(合并后、默认关闭):合并整条链,CHV 仍默认
|
||
关闭,不进生产任务路由;先跑 §13.2 的 V-1~V-9。
|
||
- 阶段 1(单租户可信项目):只对明确指定的单租户项目
|
||
开 CHV(`CHV_REUSE_SINGLE_TENANT_EXPERIMENT` 与
|
||
`CHV_SINGLE_TENANT_PROJECT` 的声明口径不变,复用
|
||
实验仍需同时声明,两者缺一即 preflight 拒绝)。
|
||
- 阶段 2(受限多租户):放开到受邀项目,监控 VM 残留盘、
|
||
审计落盘、容量门与快照配额三项指标一周无异常。
|
||
- 阶段 3(全量多租户):仅在阶段 2 指标干净 + 动态安全
|
||
审计结论由 No-Go 改为有条件启用后,才可讨论;绝不
|
||
从阶段 0 直跳全量。
|
||
|
||
### 13.4 外部关卡(本轨代码做不到,逐项列出)
|
||
|
||
1. CI 账单恢复变绿(硬门禁,合并前提)。
|
||
2. 链上 10 个 PR 按顺序合并 + 本 PR 合并(需先过 §13.1)。
|
||
3. 用户侧 provision KVM 宿主(裸金属或嵌套虚拟化实例,
|
||
须见 `/dev/kvm`;生产宿主与本 VM 均无 KVM,无法替代)。
|
||
4. 客机镜像真实构建与发布(`build.sh` 全量跑通 + V-9)。
|
||
5. §13.2 真机验证全部通过(含 V-7 动态安全负向)。
|
||
6. 负载测试(V-8)与安全审计结论更新(No-Go → 有条件启用)。
|
||
7. 前端 live 点击验证(用户侧 live browser 检查清单见
|
||
本 PR 描述;回环地址限制下浏览器任务无法代劳)。
|
||
|
||
## 14. 收尾轨 feat-chv-closeup(堆叠于 #264 之上)
|
||
|
||
本节是准备工作,不是启用结论。本轨只到 PR OPEN,不合并、
|
||
不部署;生产就绪与动态安全的外部关卡(§13.4)原样未动。
|
||
|
||
### 14.1 本轨量化结果(实测,非上一轨回执转述)
|
||
|
||
(本节数字为 closeup 轨快照,历史数字保留不改;
|
||
P1 修复轨后的最新数字见 §15,§14.1 不再是最新口径。)
|
||
|
||
- 后端:232 项 → 317 项(新增 `cloud/tests/test_chv_closeup.py`
|
||
85 项),全过。命令(仓库根目录,cwd 须为根目录——
|
||
在 `cloud/` 下跑会撞 `cloud/queue.py` 遮蔽 stdlib queue,
|
||
mypy 同理):
|
||
`python3 -m unittest cloud.tests.test_chv_arch95
|
||
cloud.tests.test_chv_arch95plus cloud.tests.test_chv_arch_maint_fe
|
||
cloud.tests.test_chv_fix cloud.tests.test_chv_gaps
|
||
cloud.tests.test_chv_optimize cloud.tests.test_chv_secfix
|
||
cloud.tests.test_chv_to10 cloud.tests.test_cloud_hypervisor
|
||
cloud.tests.test_chv_closeup`
|
||
- 覆盖率(同命令 + `coverage run --source=cloud/chv`):
|
||
chv 总 87%(1925 行,缺 258)→ 96%(缺 80);
|
||
`executor.py` 85%(缺 157)→ 100%(缺 0);
|
||
`worker_adapter.py` 82%(缺 16)→ 100%(缺 0)。
|
||
本轨基线与上一轨回执的行数口径略有出入(上一轨称
|
||
1826 行/缺 242,本轨同命令实测 1925 行/缺 258),
|
||
本轨前后对比只用本轨同一命令,不混用上一轨数字。
|
||
- 剩余未覆 80 行(按模块,全部在 executor/worker_adapter
|
||
之外,本轨未声称覆盖):
|
||
`api_client.py` 11 行(socket 层 chunked/超时/异常分支,
|
||
需 socket 级故障注入);`compat215.py` 2 行(旧兼容回落);
|
||
`config.py` 5 行(非法 env 构造分支);`disk.py` 22 行
|
||
(快照登记/剪枝边角与配额异常分支);`experimental_pool.py`
|
||
16 行(实验池,非生产路径);`hardening.py` 11 行
|
||
(真实 socket 权限与 net 校验边角);`hostkeys.py` 13 行
|
||
(ssh-keygen/genisoimage 子进程失败分支)。以上均非死
|
||
代码的断言——未逐行证伪,留待后续轨按模块补,不在本
|
||
轨凑数。
|
||
- mypy:silent 口径 `cloud/chv/` 0 错(不变);
|
||
默认口径 `cloud/marker_tools.py` 仍 2 错(见 §14.3,
|
||
本轨未改该文件)。
|
||
- ruff:`--select F,E9,W,I` 口径 `cloud/chv/` +
|
||
本轨新测试 0 错(不变;默认全规则基线不声称通过)。
|
||
- 前端:Vitest 23 项 → 36 项(新增
|
||
`src/api/chv.closeup.test.ts` 13 项),全过。
|
||
|
||
### 14.2 审计落盘文件运维 runbook(ADR-14 取舍不变,只补可操作性)
|
||
|
||
- 定位:`<state_dir>/chv-audit-spill.jsonl` 与轮转文件
|
||
`<state_dir>/chv-audit-spill.jsonl.old`。`state_dir`
|
||
默认 `~/.local/share/codex-app-server/chv-vms`,
|
||
可由 `CHV_STATE_DIR` 改址;以部署的实际值为准,
|
||
不要猜。目录本身 0700(见 §13.1 的 H6 门)。
|
||
- 触发:只有 `_emit` 的审计回调抛错时才写一行;回调
|
||
成功、无回调(只记日志)时不写。触发时宿主日志必有
|
||
`chv_audit_failed`,且 `audit_failure_count` +1——
|
||
落盘行是“哪次事件丢了”的唯一取证,计数只是数字。
|
||
- 格式:JSONL,每行 `{ts, event, kind, detail, task_id}`。
|
||
`detail` 沿用 `_emit` 契约,只含 VM 名/路径/URL/kind,
|
||
绝不含密钥值;读文件时仍按敏感文件对待(0700 目录内)。
|
||
- 轮转:当前文件 >1MB 时,先 `os.replace` 成 `.old`
|
||
再写新行;只留最新两代,更早的丢失事件会丢——
|
||
这是 ADR-14 已定的取舍,不是 bug,不要指望它当
|
||
完整审计账本。
|
||
- 读取(宿主侧,只读):
|
||
`cat <state_dir>/chv-audit-spill.jsonl`;
|
||
按事件过滤:`jq -c 'select(.event=="vm_failed")'
|
||
<state_dir>/chv-audit-spill.jsonl`;
|
||
两代一起看时先 `.old` 后当前文件(时间顺序)。
|
||
- 清理:`.old` 可随时删(删的是已被轮转覆盖的旧代);
|
||
当前 `.jsonl` 只有在已人工核对、且确认丢失事件已
|
||
无需追查时才删——删了就再也找不回那批事件。
|
||
落盘文件不是 DB 替代:任务 timeline 查询看不到它,
|
||
不要把它当成审计表去对账;审计完整性本身仍无保证
|
||
(见 ADR-14),保证需要 DB 事务级审计写入,那是
|
||
worker 全局语义改动,不在本轨。
|
||
- 落盘写失败:只记 `chv_audit_spill_failed` 日志,
|
||
绝不中断任务;此时连落盘都没有,唯一信号就是这条
|
||
日志与计数。
|
||
|
||
### 14.3 mypy 默认口径剩余 2 错评估(结论:本轨不修)
|
||
|
||
- 位置:`cloud/marker_tools.py:142`,`try: from zoneinfo
|
||
import ZoneInfo / except Exception: ZoneInfo = None`。
|
||
mypy 把 import 推断为 `type[ZoneInfo]`,再把 `None`
|
||
赋值判为两错(misc + assignment)。
|
||
- 仅标注修法(沙箱验证过,未进仓库):在 import 区后加
|
||
`from typing import Any` 与 `ZoneInfo: Any`(标注语句
|
||
本身不产生绑定),默认口径 2 错 → 0 错;运行时
|
||
`ZoneInfo` 仍是 stdlib `zoneinfo.ZoneInfo` 同一对象,
|
||
行为不变。验证在 `/tmp` 副本上做,不碰仓库文件。
|
||
- 不修的理由(两条,任一条已足够):① 该文件属标记
|
||
工具链(git 历史为标记工具提交),不在本链 §13.1
|
||
文件清单内,本轨改它就是动其它链的文件,徒增与
|
||
仍 OPEN 的审计/标记 PR 的 rebase 冲突面;② CHV 链
|
||
自己的门是 silent 口径(`--follow-imports=silent
|
||
cloud/chv/`),该口径已 0 错,默认口径的 2 错是
|
||
作用域外既有问题。修法留给 marker_tools 的归属轨,
|
||
本轨只把配方写在这里,不为清零越界改文件。
|
||
|
||
### 14.4 合并前检查脚本(只读)
|
||
|
||
- 脚本:`tools/chv_merge_precheck.py`,用法见文件头。
|
||
只读:`gh pr view` / `gh api` 查询 + 本地 `git` 读
|
||
remote,不 fetch、不 checkout、不 merge、不改 CI。
|
||
退出码:0 就绪、1 未就绪、2 查询失败(结论不可信)。
|
||
- 本轨实测(未含本轨新 PR,建好后须追加新 PR 号重跑):
|
||
`python3 tools/chv_merge_precheck.py` → 退出码 1,
|
||
结论 NOT-READY:11 个 PR 全部 OPEN / MERGEABLE,
|
||
链衔接全部 OK,当前 main 头仍 `aebc6238`(与 §13.1
|
||
一致),但 11 个 PR 的 CI 全部 `FAIL [verify:FAILURE]`、
|
||
mergeState 全部 UNSTABLE。脚本不诊断失败原因,
|
||
不把秒级失败改判——账单恢复变绿前结论保持 NOT-READY。
|
||
|
||
### 14.5 灰度三阶段可执行清单(准备工作,未执行、未验证)
|
||
|
||
通用停止条件(任一触发即停在该阶段,不进下一阶段):
|
||
`vm_failed` 审计激增、脏标记(`dirty_marker`)出现、
|
||
`chv-audit-spill.jsonl` 持续增长、残留盘/残留密钥扫描
|
||
清出本阶段不应有的项、任一任务静默降级到 Docker/Local。
|
||
通用回滚:停止向 CHV 路由新任务(改回 docker/local
|
||
执行器选择),已起 VM 走取消/关闭路径(`cancel_vm` /
|
||
`shutdown_vm`);代码回滚另议(revert 合并),不与
|
||
“停路由”混为一谈。
|
||
|
||
- 阶段 0(合并后、默认关闭):
|
||
1. 按 §14.4 脚本逐项核对,退出码 0 前不合并;合并
|
||
顺序即 §14.4 的链序,不得跳号。
|
||
2. 合并后确认 CHV 仍默认关闭(无生产任务路由到
|
||
`cloud-hypervisor`,任务创建白名单外的执行器值
|
||
仍被后端拒绝)。
|
||
3. 在 KVM 宿主(须见 `/dev/kvm`,本 VM 与生产宿主
|
||
均无 KVM,不能替代)上跑 §14.6 的 V-1~V-9,
|
||
全部通过才可谈阶段 1;任一未跑即记未验证。
|
||
回滚点:合并本身可 revert;未进生产路由前无任务影响。
|
||
- 阶段 1(单租户可信项目):
|
||
1. 只对明确指定的单租户项目派发 CHV 任务;复用
|
||
实验仍需 `CHV_REUSE_SINGLE_TENANT_EXPERIMENT` 与
|
||
`CHV_SINGLE_TENANT_PROJECT` 同时声明,缺一即
|
||
preflight 拒绝(代码行为,不放宽)。
|
||
2. 检查点:每任务 VM 销毁后 `state_dir` 无该任务
|
||
残留盘与 seed;`chv-audit-spill.jsonl` 无新增;
|
||
取消路径真关 VM(V-5 口径)。
|
||
3. 停止条件见通用条件;回滚:该项目任务改回原
|
||
执行器,CHV worker 停接新任务。
|
||
- 阶段 2(受限多租户):
|
||
1. 放开到受邀项目,连续观察一周:VM 残留盘、
|
||
审计落盘、容量门(`CHV_MAX_VMS`,含重启后残留
|
||
socket 扫描)、快照配额四项指标。
|
||
2. 检查点:每周复核四项指标零异常 + §14.2 落盘
|
||
文件复核;任一指标异常即退回阶段 1。
|
||
3. 停止条件见通用条件;回滚:收紧到阶段 1 项目集。
|
||
- 阶段 3(全量多租户):仅在阶段 2 指标干净 +
|
||
动态安全审计结论由 No-Go 改为有条件启用后,才可
|
||
讨论;绝不从阶段 0 直跳全量。本轨不给阶段 3 的
|
||
时间表,不写成已就绪。
|
||
|
||
### 14.6 真机验证用例 V-1~V-9 验收检查表(全部未验证)
|
||
|
||
每行格式:用例 — 验收标准(§13.2 细化)— 证据栏(待填)。
|
||
状态栏一律“未验证”,跑过才改,跑过≠通过。
|
||
|
||
- [ ] V-1 冷启动 — 任务 completed,宿主 state_dir 无该
|
||
任务残留盘与 seed。证据:______ 状态:未验证
|
||
- [ ] V-2 seed 首启时序 — `sshd -T` 显示
|
||
PasswordAuthentication no 等加固项,known_hosts 钉死
|
||
校验通过,非 TOFU 回落。证据:______ 状态:未验证
|
||
- [ ] V-3 快照恢复 — 恢复后 known_hosts 不变,快照文件
|
||
落盘非空且经 `snapshot_not_persisted` 校验。证据:______
|
||
状态:未验证
|
||
- [ ] V-4 网关真实任务 — 任务完成 + 宿主进程表抽查无密钥
|
||
(密钥只走 seed 客内文件)。证据:______ 状态:未验证
|
||
- [ ] V-5 取消与配额真杀 — VM 进程消失,任务 interrupted
|
||
且原因写回正确。证据:______ 状态:未验证
|
||
- [ ] V-6 审计落盘 — 落盘行 event/task_id/detail 与丢失
|
||
事件一致,`audit_failure_count` +1,任务不中断。
|
||
证据:______ 状态:未验证
|
||
- [ ] V-7 动态安全负向 — 直连被 tap/防火墙挡、跨 VM 探活
|
||
与 IP 冒用被 SSH 钉死拒、崩溃注入后残留被清理或脏
|
||
标记拒起,每项都有“被挡住”的证据。证据:______
|
||
状态:未验证
|
||
- [ ] V-8 负载 — 4 VM 并发下容量门计数含重启后残留
|
||
socket 扫描,死 socket 不占名额、活 VM 不被超发,
|
||
无静默降级。证据:______ 状态:未验证
|
||
- [ ] V-9 供应链清单 — 构建宿主产出真实 lockfile,
|
||
`--verify` 通过且 unresolved 为空,再用
|
||
`check_lockfile.py` 对 BUILDINFO.versions 复核。
|
||
证据:______ 状态:未验证
|
||
|
||
### 14.7 本轨逐项评分(目标 10,无一项硬写 10)
|
||
|
||
(本节评分为 closeup 轨快照;P1 修复轨后的评分更新见
|
||
§15.7,其中“80 行未覆”“317 项/36 项”扣分理由已过期,
|
||
不再作为当前扣分依据。)
|
||
|
||
- 代码质量 9.8 → 9.8,未达 10:executor/worker_adapter
|
||
覆盖到 100%,但 chv 仍有 80 行未覆(非 executor
|
||
模块),且默认口径 mypy 余 2 错在作用域外。差:代码内。
|
||
- 架构 9.7 → 9.7,未达 10:本轨未改架构,ADR-13/14
|
||
取舍不变。差:外部(生产验证后才可复评)。
|
||
- 可维护性 9.7 → 9.8,未达 10:新增合并检查脚本与
|
||
§14 运维/灰度文档,落盘可操作性补齐;但跨链文件
|
||
的 mypy 归属未落。差:代码内(小)。
|
||
- 文档 9.8 → 9.9,未达 10:§14 补齐 runbook、灰度
|
||
清单、V 检查表、评分表;仍缺真机证据填表。差:外部。
|
||
- 测试 9.7 → 9.8,未达 10:后端 317 项、前端 36 项全过,
|
||
executor 100%;但 chv 总覆盖 96% 未满,且真机用例
|
||
0 项已跑。差:代码内(80 行)+ 外部(真机)。
|
||
- 静态安全 9.6 → 9.6,未达 10:取舍不变,只补落盘
|
||
运维;lockfile 仍待构建宿主产出,绝不编造。差:外部。
|
||
- 前端 9.5 → 9.6,未达 10:只读四态与可用性文案边角
|
||
全覆盖(显式 false、undefined、优先级、旧 bug 文案
|
||
不复发、120 秒边界);控制按钮与新页面仍不做,
|
||
live 点击未验。差:代码内(有意不做)+ 外部(live)。
|
||
- 生产就绪 5.0 → 5.5,未达 10:仅因合并/灰度准备
|
||
工作(可跑脚本 + 可执行清单)小幅上升;CI 仍
|
||
UNSTABLE、未合并、未部署、真机 0 项。差:外部为主。
|
||
- 动态安全 2.0 → 2.0,未达 10:本轨无任何动态验证,
|
||
V-7 未跑。差:外部(KVM 宿主 + 审计结论)。
|
||
## 15. P1 修复轨 feat-chv-p1fix(堆叠于 #270 之上)
|
||
|
||
本节是工程生产级审核(`~/workspace/chv-prod-audit/REPORT.md`,
|
||
审核对象 #270,提交 `8c8d62f`)§5 的 P1-01~P1-10 修复轨。
|
||
本轨只到 PR OPEN,不合并、不部署、不做合并试验(P0-01
|
||
仍未做,见 §15.6);P2-01~P2-21 一律不动。
|
||
|
||
### 15.1 本轨量化结果(实测)
|
||
|
||
- 后端:362 项 → 389 项(新增 `cloud/tests/test_chv_p1fix.py`
|
||
27 项),全过。命令(仓库根目录):
|
||
`python3 -m unittest cloud.tests.test_chv_arch95
|
||
cloud.tests.test_chv_arch95plus cloud.tests.test_chv_arch_maint_fe
|
||
cloud.tests.test_chv_fix cloud.tests.test_chv_gaps
|
||
cloud.tests.test_chv_optimize cloud.tests.test_chv_secfix
|
||
cloud.tests.test_chv_to10 cloud.tests.test_cloud_hypervisor
|
||
cloud.tests.test_chv_closeup cloud.tests.test_chv_cover100
|
||
cloud.tests.test_chv_p1fix`
|
||
- 覆盖率(同命令 + `coverage run --source=cloud/chv`,再
|
||
`--append test_cloud_hypervisor`,`.coverage` 写 /tmp):
|
||
行覆盖 100%(2097 行,缺 0);分支覆盖 99%
|
||
(714 分支,28 处部分分支未全走——行 100% 不等于
|
||
分支 100%,PR 描述按此口径写,不写裸“100%”)。
|
||
`cloud/chv/__init__.py` 计入分母:4 行,100%(仅
|
||
docstring + 3 个 re-export,coverage.py 口径;审核报告
|
||
的 trace 口径把它排除在外,两口径差在此说明,不混用)。
|
||
审核报告 P1-09 点名的 4 处三元 else 分支(str 形态
|
||
pause/resume/snapshot + 非 dict spill task)已由
|
||
`test_chv_p1fix.py` 的 BranchCover 测试覆盖。
|
||
- mypy:silent 口径 `cloud/chv/` 0 错(不变);
|
||
默认口径 `cloud/marker_tools.py` 仍 2 错(作用域外,
|
||
本轨未改该文件)。
|
||
- ruff:`--select F,E9,W,I` 口径 `cloud/chv/` +
|
||
本轨新测试 0 错(不变;默认全规则基线不声称通过)。
|
||
- 前端:Vitest 37 项不变(`chv.test.ts` 23 +
|
||
`chv.closeup.test.ts` 14),本轨未改前端代码;三轮
|
||
连跑 37/37/37,无抖动回归。
|
||
- 全量:`python3 -m unittest discover -s cloud/tests`
|
||
(仓库根目录)1819 项全过(skipped=8),392 秒;
|
||
上一轨引用口径 1792 项的差额来自本轨新增 27 项。
|
||
- 长函数:AST 扫描 `cloud/chv/`,>50 行函数 0 个
|
||
(本轨新增的 snapshot_vm / record_snapshot /
|
||
_preflight_numeric_checks 均经提取守住 50 行门禁,
|
||
见 ADR-17)。
|
||
- §13.1 的 main 头 `aebc6238` 与前端产物哈希
|
||
`269820d1…f389` 仍是 to10 轨快照,本轨不更新它们:
|
||
合并前必须按 §13.1 逐项重跑重核,绝不把旧快照写成
|
||
新数字(审核报告 P1-10 的要求原样保留)。
|
||
|
||
### 15.2 P1 逐项修法(证据:文件与测试名)
|
||
|
||
- P1-01(orphans.py + experimental_pool.py):失败项不进
|
||
cleaned、不触发清库;生产 reconcile 落盘只写回失败项
|
||
原始条目并审计 `warm_pool_reconcile_failed`,下次可重试;
|
||
实验池失败条目留在内存条目里(文件落盘语义属 P2-16,
|
||
本轨不改)。同步改旧测试:`test_chv_arch_maint_fe.py`
|
||
的 shutdown 失败用例与 `test_chv_cover100.py` 的
|
||
`test_reconcile_shutdown_failure_still_cleans`(已改名
|
||
`test_reconcile_shutdown_failure_not_cleaned`)——代码
|
||
与测试同改,不存在“只改一侧”。
|
||
新测试:`ReconcileP1FixTest.test_failure_not_cleaned_registry_retained_and_retryable`。
|
||
- P1-02(orphans.py):registry 损坏发
|
||
`warm_pool_registry_corrupt` 审计(quarantine 后仍审计,
|
||
不静默返回空);非法条目发
|
||
`warm_pool_registry_invalid_entries` 审计并计数,落盘
|
||
时丢弃已审计条目(不再每次重启重复审计)。
|
||
新测试:`test_corrupt_registry_audited_not_silent`、
|
||
`test_invalid_entries_audited_and_counted`。
|
||
- P1-03(executor.py `_clean_one_vm_secrets` /
|
||
`cleanup_stale_secrets`):返回值由 2 元组改为
|
||
(removed_priv, removed_seed, failed),删失败发
|
||
`host_secret_residue_delete_failed` 审计并写脏标记
|
||
(下次 provision 见标记即 `vm_dirty_refused` 拒起),
|
||
删失败的 VM 不进 cleaned(即使部分删掉也不算已清理)。
|
||
旧测试 `test_clean_one_vm_secrets_remove_failures` 已
|
||
同步改 3 元组解包与 failed 断言。
|
||
- P1-04(executor.py):删除 `build_ssh_command` 的回环
|
||
回落(构造失败即抛 `ssh_target_undeclared`),不设
|
||
dry-run 例外参数——干跑同样不许猜目标;同款回落
|
||
`_write_provisioned_known_hosts` 一并删除(wake 路径
|
||
在 provision 前已在 `_wake_prechecks` 里严格解析,
|
||
不受影响;wait 路径的 `capture_host_key` 本来就
|
||
解析失败返回 False,同样严格)。显式回环仍可用:
|
||
`CHV_SSH_HOST=127.0.0.1` 或 `CHV_SSH_FORWARD=1`。
|
||
旧测试 `test_build_ssh_command_fallback_and_secret_not_delivered`
|
||
与 `test_write_provisioned_known_hosts_fallback` 已
|
||
同步改为抛错断言。
|
||
- P1-05(新增 `cloud/chv/locking.py` + executor.py):
|
||
选文件锁方案(非单 worker 声明):`StateLock` =
|
||
`flock`(`state_dir/chv-state.lock`,永不删除)+
|
||
进程内 RLock 可重入(同一实例嵌套不自锁;多实例/
|
||
多进程靠 flock 互斥)。持有范围:wake_vm 全程、
|
||
restore_vm 全程、cleanup_stale_vms 全程、orphans
|
||
reconcile 全程;`_purge_dead_registry` 改为锁内快照
|
||
键列表、逐项锁内读/删,`_stop_dead_socket_vms` 的
|
||
登记读持锁。串行 wake 是有意的代价(容量门只有
|
||
“查完立即占位”才成立,提前释放会在 socket 未绑定
|
||
前留出计数窗口);已运行 VM 的任务执行不持此锁。
|
||
新测试:`StateLockP1FixTest`(线程互斥、跨进程互斥、
|
||
purge 并发变更加锁)、`WakeSerializationP1FixTest`
|
||
(两路并发 wake 实测串行化)。
|
||
- P1-06(disk.py `record_snapshot`):单条快照本身大于
|
||
总量帽时,在任何落盘/删旧动作之前直接拒绝
|
||
(`snapshot_quota_exceeded`),不悄悄保留超大快照;
|
||
大小未知(size 为 None,如远端目标)时无法判定,
|
||
沿用旧口径。新测试:
|
||
`SingleSnapshotCapP1FixTest`(拒绝无副作用:旧文件
|
||
与旧登记均在)。
|
||
- P1-07(config.py + executor.py preflight):构造层
|
||
拒绝能解析出的 CPU/内存非正值与显式任务超时非正值
|
||
(`CHV_CPU`/`CHV_MEM`/`CHV_TASK_TIMEOUT`);preflight
|
||
再验属性事后改写(CPU 0/负、内存 0、任务超时 0/负
|
||
一律 `config_invalid` 干净失败,不进 VmConfig)。
|
||
`None` 任务超时 = 不限(旧语义保留);解析不出的
|
||
格式仍留给 preflight(构造时机分工不变)。
|
||
新测试:`PositiveConfigP1FixTest`。
|
||
- P1-08(disk.py + executor.py `snapshot_vm`):剪枝
|
||
事件外发(`prune_snapshots` / `check_snapshot_limits` /
|
||
`record_snapshot` 均接受 `events` 列表:deleted /
|
||
delete_failed / pruned_no_file);`snapshot_vm` 据此发
|
||
`vm_snapshot_pruned` / `vm_snapshot_prune_failed`
|
||
审计,失败路径(白名单/上限/ch-remote/落盘校验/
|
||
登记拒绝)统一发 `vm_snapshot_failed` 审计再抛。
|
||
新测试:`SnapshotAuditP1FixTest`。
|
||
- P1-09:见 §15.1(行 100% / 分支 99% 分开写;
|
||
`__init__.py` 计入分母说明;4 分支测试在
|
||
`BranchCoverP1FixTest`)。
|
||
- P1-10:本节即同步结果;§14.1/§14.7 已加“已被 §15
|
||
取代”指针(历史数字保留不改);架构文档头范围与
|
||
ADR-17 见 `docs/chv-architecture.md`;§13.1 快照数
|
||
字按要求保留、合并前重跑(见 §15.1 末)。
|
||
|
||
### 15.3 安全语义未放宽清单
|
||
|
||
egress 默认 fail-closed、key seed 投递(provisioned 才
|
||
投递、keyscan 下密钥 fail-closed)、磁盘删除(删失败
|
||
审计 + 脏标记)、默认 provisioned、tap/mac/ip 校验、
|
||
StrictHostKeyChecking 钉死、网关 loud fail 守卫——
|
||
本轨一律未改动,只收紧(回落删除、失败可见性、
|
||
check→act 加锁)。前端无改动,不存在绕后端白名单
|
||
的新路径。
|
||
|
||
### 15.4 新增与改动文件
|
||
|
||
- 新增:`cloud/chv/locking.py`(StateLock)、
|
||
`cloud/tests/test_chv_p1fix.py`(27 项)。
|
||
- 改动:`cloud/chv/orphans.py`、`cloud/chv/disk.py`、
|
||
`cloud/chv/executor.py`、`cloud/chv/experimental_pool.py`、
|
||
`cloud/chv/config.py`;旧测试同步改:
|
||
`cloud/tests/test_chv_arch_maint_fe.py`、
|
||
`cloud/tests/test_chv_closeup.py`、
|
||
`cloud/tests/test_chv_cover100.py`;
|
||
文档:本节 + `docs/chv-architecture.md`(头范围 +
|
||
ADR-17)。
|
||
|
||
### 15.5 逐项完成情况
|
||
|
||
P1-01~P1-10 全部修完(每项均有新测试或同步改的旧
|
||
测试,见 §15.2;无“修不动”的项)。未修完的部分:
|
||
无。但“修完”只等于代码层完成,不等于生产就绪,
|
||
见 §15.6。
|
||
|
||
### 15.6 仍未验证项(原样保留,不冒充)
|
||
|
||
- 合并试验 P0-01 仍未做(本轨禁令):隔离演练区以
|
||
当前远端 HEAD 为目标的三方合并 + 全量测试未跑,
|
||
直接合入仍可能以链侧文件覆盖 main 新增功能。
|
||
- CI 仍 UNSTABLE(账单秒级失败,本轨未改 CI 配置)。
|
||
- KVM 宿主 provision、客机镜像真实构建与发布未做;
|
||
真机验证 V-1~V-9 全部未跑。
|
||
- 前端 live 点击验证未做;渲染测试未做。
|
||
- 旧安全审计 No-Go 未复审、未推翻。
|
||
|
||
### 15.7 本轨逐项评分更新(目标 10,无一项硬写 10)
|
||
|
||
- 代码质量 9.8 → 9.8,未达 10:P1 语义缺陷已修,但
|
||
分支覆盖 99% 未满、长函数门禁靠提取维持(边界脆弱)。
|
||
差:代码内(小)。
|
||
- 测试 9.8 → 9.9,未达 10:后端 389 项、前端 37 项
|
||
全过,行覆盖 100% 恢复且口径诚实分开写;但真机
|
||
用例 0 项已跑、分支 28 处部分。差:外部(真机)为主。
|
||
- 文档 9.9 → 9.9,未达 10:§15 与 ADR-17 已同步,但
|
||
§13.1 仍是旧快照、真机证据表仍空。差:外部。
|
||
- 架构 9.7 → 9.7,未达 10:新增 flock 锁是并发语义
|
||
收紧而非架构重构,ADR 取舍不变。差:外部。
|
||
- 前端 9.6 → 9.6,未达 10:本轨未改前端。差:代码内
|
||
(有意不做)+ 外部(live)。
|
||
- 生产就绪 5.5 → 5.5,未达 10:P0-01 未做、CI 未绿、
|
||
未合并、未部署、真机 0 项。差:外部为主。
|
||
- 动态安全 2.0 → 2.0,未达 10:本轨无任何动态验证。
|
||
差:外部。
|