feat(chv-muse-round11): 执行器接 nspawn 单元(P2 差距表 §4)
客内 host.py 调用改为 chv-cell run -- python3 /app/host.py --state /home/chv/.codex/host-state.json --codex-home /home/chv/.codex --sandbox danger-full-access …,其余原参数 原样保留。--sandbox 放全局参数最后(argparse 后胜出), 单元内恒为 danger-full-access,调用方 read-only 不得盖回 bwrap;非单元路径(本地/Docker/冷启动回落)不动。 开关 CHV_CELL_ENABLED:只认 0/1,默认 1 开;非法值 ValueError(_to_switch 同款口径)。关时走原命令,给 lite10 及更早无单元的旧镜像留逃生口。 测试:新 test_chv_muse_round11_cell.py 7 项(开/关 argv、 原参数保留、sandbox 只在单元路径、非法值报错、SSH 层 接线);test_host_command_options_identical_across_executors 显式关单元测三执行器参数全等,断言本身未动。 全量 cloud 套件 2011 OK(skipped=8)。
This commit is contained in:
1 parent
304707b372
commit
73176e135c
3 files changed
+231
-3
No files matched your search
+46
-2
@@ -38,7 +38,8 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
from cloud.chv.api_client import _unix_socket_api, normalize_api_result
|
from cloud.chv.api_client import _unix_socket_api, normalize_api_result
|
||||||
from cloud.chv.atomic_file import atomic_write_text
|
from cloud.chv.atomic_file import atomic_write_text
|
||||||
from cloud.chv.config import _parse_memory_bytes, build_chv_config
|
from cloud.chv.config import _parse_memory_bytes, _to_switch
|
||||||
|
from cloud.chv.config import build_chv_config
|
||||||
from cloud.chv.disk import (
|
from cloud.chv.disk import (
|
||||||
check_disk_quota,
|
check_disk_quota,
|
||||||
check_snapshot_limits,
|
check_snapshot_limits,
|
||||||
@@ -76,6 +77,13 @@ def _default_popen_launcher(cmd):
|
|||||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||||
|
|
||||||
|
|
||||||
|
# round-11 集成补洞(P2 差距表 §4):单元内 host.py 的显式路径。
|
||||||
|
# 单元内 /app 只读,host.py 默认 state(/app/host-state.json)
|
||||||
|
# 写不进;~/.codex 是单元内 bind 的可写路径。
|
||||||
|
_CELL_STATE_PATH = "/home/chv/.codex/host-state.json"
|
||||||
|
_CELL_CODEX_HOME = "/home/chv/.codex"
|
||||||
|
|
||||||
|
|
||||||
class CloudHypervisorExecutor(Executor):
|
class CloudHypervisorExecutor(Executor):
|
||||||
"""Cloud Hypervisor 执行器 v1(命名:cloud-hypervisor,绝不用
|
"""Cloud Hypervisor 执行器 v1(命名:cloud-hypervisor,绝不用
|
||||||
cloud/ch 缩写——cloud 已是通用 HTTP runner 占位,撞名会让
|
cloud/ch 缩写——cloud 已是通用 HTTP runner 占位,撞名会让
|
||||||
@@ -240,6 +248,10 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
0/1,非法值 ValueError)
|
0/1,非法值 ValueError)
|
||||||
- CHV_VSOCK_CID:客机 vsock CID(默认 3;须在
|
- CHV_VSOCK_CID:客机 vsock CID(默认 3;须在
|
||||||
3-4294967295 内,0/1/2 为保留 CID)
|
3-4294967295 内,0/1/2 为保留 CID)
|
||||||
|
- CHV_CELL_ENABLED:nspawn 单元开关(默认 1 开;只认
|
||||||
|
0/1,非法值 ValueError)。开时客内 host.py 改走
|
||||||
|
chv-cell run(P2 差距表 §4);关时走原命令,给
|
||||||
|
lite10 及更早无单元的旧镜像留逃生口
|
||||||
|
|
||||||
前置校验(preflight,执行入口必调,缺一即 fail-fast):
|
前置校验(preflight,执行入口必调,缺一即 fail-fast):
|
||||||
收集全部缺失项一次报出具体缺哪一项,绝不笼统报错:
|
收集全部缺失项一次报出具体缺哪一项,绝不笼统报错:
|
||||||
@@ -1411,13 +1423,31 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
子命令之前,共享 helper 在 cloud/sandbox.py)。
|
子命令之前,共享 helper 在 cloud/sandbox.py)。
|
||||||
guest_host_py 默认 /app/host.py:客机镜像须把 host.py
|
guest_host_py 默认 /app/host.py:客机镜像须把 host.py
|
||||||
放在该路径(与 Docker 的 /app:ro 同款约定,只是载体
|
放在该路径(与 Docker 的 /app:ro 同款约定,只是载体
|
||||||
从容器换成 VM 镜像)。"""
|
从容器换成 VM 镜像)。
|
||||||
|
round-11 集成补洞(P2 差距表 §4):单元开时(默认)
|
||||||
|
命令改为 chv-cell run -- python3 /app/host.py --state
|
||||||
|
/home/chv/.codex/host-state.json --codex-home
|
||||||
|
/home/chv/.codex … --sandbox danger-full-access [子命令
|
||||||
|
...]。--state/--codex-home 须显式给(见模块顶 _CELL_*
|
||||||
|
注释);--sandbox 放全局参数最后(argparse 后胜出),
|
||||||
|
确保单元内恒为 danger-full-access:单元即沙箱,调用
|
||||||
|
方传的 read-only(plan 任务)与 approval=full 自带的
|
||||||
|
值都不得把它盖回 bwrap。单元关时
|
||||||
|
(CHV_CELL_ENABLED=0)走原命令,给无单元的旧镜像留
|
||||||
|
逃生口。"""
|
||||||
from cloud.sandbox import _host_command_options
|
from cloud.sandbox import _host_command_options
|
||||||
|
cell = self._cell_enabled()
|
||||||
cmd = ["python3", self.guest_host_py]
|
cmd = ["python3", self.guest_host_py]
|
||||||
|
if cell:
|
||||||
|
cmd = ["chv-cell", "run", "--"] + cmd
|
||||||
|
cmd += ["--state", _CELL_STATE_PATH,
|
||||||
|
"--codex-home", _CELL_CODEX_HOME]
|
||||||
cmd += _host_command_options(
|
cmd += _host_command_options(
|
||||||
model=model, effective_model=self.model,
|
model=model, effective_model=self.model,
|
||||||
model_provider=model_provider, effort=effort,
|
model_provider=model_provider, effort=effort,
|
||||||
approval=approval, sandbox=sandbox)
|
approval=approval, sandbox=sandbox)
|
||||||
|
if cell:
|
||||||
|
cmd += ["--sandbox", "danger-full-access"]
|
||||||
cmd += list(args)
|
cmd += list(args)
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
@@ -2519,6 +2549,20 @@ class CloudHypervisorExecutor(Executor):
|
|||||||
"interrupted_reason": reason,
|
"interrupted_reason": reason,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _cell_enabled(self):
|
||||||
|
"""nspawn 单元开关(round-11 集成补洞,P2 差距表 §4):
|
||||||
|
CHV_CELL_ENABLED 默认 1(开)。只认 "1"/"0",其它值
|
||||||
|
ValueError(_to_switch 的 CHV_BALLOON/CHV_VSOCK 同款
|
||||||
|
口径,绝不静默 fallback)。关时 _guest_host_command
|
||||||
|
走原命令,给 lite10 及更早无单元的旧镜像留逃生口;
|
||||||
|
开时走 chv-cell run -- python3 /app/host.py …。读
|
||||||
|
self._env(与 _resident_enabled 同款惰性读法),不
|
||||||
|
经 config:开关只影响命令构造,不影响建池/起 VM。"""
|
||||||
|
flag = _to_switch(
|
||||||
|
self._env.get("CHV_CELL_ENABLED", "1"),
|
||||||
|
"CHV_CELL_ENABLED")
|
||||||
|
return True if flag is None else flag
|
||||||
|
|
||||||
def _resident_enabled(self):
|
def _resident_enabled(self):
|
||||||
"""常驻默认开;CHV_RESIDENT_ENABLED=0/false/no/off 关。
|
"""常驻默认开;CHV_RESIDENT_ENABLED=0/false/no/off 关。
|
||||||
优先认执行器上已有的 resident_enabled 属性(P1-B 若
|
优先认执行器上已有的 resident_enabled 属性(P1-B 若
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""round-11 集成补洞:执行器接 nspawn 单元(P2 差距表 §4)。
|
||||||
|
|
||||||
|
只测 _guest_host_command 的命令构造与 CHV_CELL_ENABLED 开关:
|
||||||
|
单元开(默认)时 host.py 调用改为 chv-cell run -- python3
|
||||||
|
/app/host.py --state … --codex-home … --sandbox
|
||||||
|
danger-full-access …;单元关时走原命令;非法开关值即
|
||||||
|
ValueError。只用 mock,不启动真实 VM、不连真实网络。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cloud.chv import CloudHypervisorExecutor
|
||||||
|
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||||
|
from cloud.chv import CloudHypervisorExecutor
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResult:
|
||||||
|
def __init__(self, returncode=0, stdout="", stderr=""):
|
||||||
|
self.returncode = returncode
|
||||||
|
self.stdout = stdout
|
||||||
|
self.stderr = stderr
|
||||||
|
|
||||||
|
|
||||||
|
class FakeProc:
|
||||||
|
def __init__(self, alive=True):
|
||||||
|
self._alive = alive
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return None if self._alive else 1
|
||||||
|
|
||||||
|
|
||||||
|
def _write(path, data=b"x"):
|
||||||
|
with open(path, "wb") as fh:
|
||||||
|
fh.write(data)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class CellCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.dir = self.tmp.name
|
||||||
|
self.binary = _write(os.path.join(self.dir, "cloud-hypervisor"))
|
||||||
|
self.ch_remote = _write(os.path.join(self.dir, "ch-remote"))
|
||||||
|
self.kernel = _write(os.path.join(self.dir, "vmlinux"))
|
||||||
|
self.rootfs = _write(os.path.join(self.dir, "disk.raw"),
|
||||||
|
b"R" * 100)
|
||||||
|
self.ssh_key = _write(os.path.join(self.dir, "id_chv"))
|
||||||
|
self.kvm = _write(os.path.join(self.dir, "kvm"))
|
||||||
|
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
|
||||||
|
with open(self.cpuinfo, "w") as fh:
|
||||||
|
fh.write("flags : fpu vmx sse\n")
|
||||||
|
self.state_dir = os.path.join(self.dir, "state")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def make_exec(self, env_extra=None):
|
||||||
|
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
|
||||||
|
"CHV_HOST_KEY_MODE": "keyscan",
|
||||||
|
"CHV_SSH_FORWARD": "1"}
|
||||||
|
if env_extra:
|
||||||
|
env.update(env_extra)
|
||||||
|
return CloudHypervisorExecutor(
|
||||||
|
binary=self.binary, ch_remote=self.ch_remote,
|
||||||
|
kernel=self.kernel, rootfs=self.rootfs,
|
||||||
|
ssh_key=self.ssh_key, kvm_path=self.kvm,
|
||||||
|
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
|
||||||
|
runner=lambda cmd, timeout=None: FakeResult(0, "", ""),
|
||||||
|
launcher=lambda cmd: FakeProc(alive=True),
|
||||||
|
api_client=lambda *a, **k: (200, {}),
|
||||||
|
sleeper=lambda s: None, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
class CellCommandTest(CellCase):
|
||||||
|
def test_cell_on_by_default(self):
|
||||||
|
# 开关未设即默认开:完整命令与契约示例一致。
|
||||||
|
ex = self.make_exec()
|
||||||
|
cmd = ex._guest_host_command("status", "--task", "t1")
|
||||||
|
self.assertEqual(cmd, [
|
||||||
|
"chv-cell", "run", "--",
|
||||||
|
"python3", "/app/host.py",
|
||||||
|
"--state", "/home/chv/.codex/host-state.json",
|
||||||
|
"--codex-home", "/home/chv/.codex",
|
||||||
|
"--sandbox", "danger-full-access",
|
||||||
|
"status", "--task", "t1"])
|
||||||
|
|
||||||
|
def test_cell_on_explicit_1(self):
|
||||||
|
ex = self.make_exec(env_extra={"CHV_CELL_ENABLED": "1"})
|
||||||
|
cmd = ex._guest_host_command("status", "--task", "t1")
|
||||||
|
self.assertEqual(cmd[:3], ["chv-cell", "run", "--"])
|
||||||
|
self.assertIn("--sandbox", cmd)
|
||||||
|
self.assertEqual(
|
||||||
|
cmd[cmd.index("--sandbox") + 1], "danger-full-access")
|
||||||
|
|
||||||
|
def test_cell_off_returns_original_command(self):
|
||||||
|
# 关时走原命令:一个 token 也不多不少(旧镜像逃生口)。
|
||||||
|
ex = self.make_exec(env_extra={"CHV_CELL_ENABLED": "0"})
|
||||||
|
cmd = ex._guest_host_command("status", "--task", "t1")
|
||||||
|
self.assertEqual(cmd, ["python3", "/app/host.py",
|
||||||
|
"status", "--task", "t1"])
|
||||||
|
self.assertNotIn("chv-cell", cmd)
|
||||||
|
self.assertNotIn("--state", cmd)
|
||||||
|
self.assertNotIn("--codex-home", cmd)
|
||||||
|
self.assertNotIn("--sandbox", cmd)
|
||||||
|
|
||||||
|
def test_cell_on_preserves_original_params(self):
|
||||||
|
# 原参数一个不少:model/provider/effort/approval/
|
||||||
|
# 调用方 sandbox 与子命令全部保留且顺序不变;强制的
|
||||||
|
# danger-full-access 放全局参数最后,盖过调用方的
|
||||||
|
# read-only(argparse 后胜出),read-only 本身仍在。
|
||||||
|
ex = self.make_exec()
|
||||||
|
cmd = ex._guest_host_command(
|
||||||
|
"dispatch", "--name", "t", "--task", "x",
|
||||||
|
model="m1", effort="low", approval="ask",
|
||||||
|
model_provider="sub2api", sandbox="read-only")
|
||||||
|
self.assertEqual(cmd, [
|
||||||
|
"chv-cell", "run", "--",
|
||||||
|
"python3", "/app/host.py",
|
||||||
|
"--state", "/home/chv/.codex/host-state.json",
|
||||||
|
"--codex-home", "/home/chv/.codex",
|
||||||
|
"--model", "m1",
|
||||||
|
"--model-provider", "sub2api",
|
||||||
|
"--effort", "low",
|
||||||
|
"--approval-policy", "untrusted",
|
||||||
|
"--sandbox", "read-only",
|
||||||
|
"--sandbox", "danger-full-access",
|
||||||
|
"dispatch", "--name", "t", "--task", "x"])
|
||||||
|
# --state/--codex-home 紧跟 host.py 路径。
|
||||||
|
host_idx = cmd.index("/app/host.py")
|
||||||
|
self.assertEqual(
|
||||||
|
cmd[host_idx + 1:host_idx + 5],
|
||||||
|
["--state", "/home/chv/.codex/host-state.json",
|
||||||
|
"--codex-home", "/home/chv/.codex"])
|
||||||
|
|
||||||
|
def test_sandbox_only_in_cell_path(self):
|
||||||
|
# 调用方没给 sandbox、approval 也没带时:单元路径
|
||||||
|
# 有且仅有一个 --sandbox(danger-full-access),
|
||||||
|
# 非单元路径一个也没有。
|
||||||
|
cell_on = self.make_exec()
|
||||||
|
on_cmd = cell_on._guest_host_command("status", "--task", "t1")
|
||||||
|
self.assertEqual(on_cmd.count("--sandbox"), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
on_cmd[on_cmd.index("--sandbox") + 1],
|
||||||
|
"danger-full-access")
|
||||||
|
cell_off = self.make_exec(
|
||||||
|
env_extra={"CHV_CELL_ENABLED": "0"})
|
||||||
|
off_cmd = cell_off._guest_host_command(
|
||||||
|
"status", "--task", "t1")
|
||||||
|
self.assertEqual(off_cmd.count("--sandbox"), 0)
|
||||||
|
|
||||||
|
def test_cell_invalid_value_raises(self):
|
||||||
|
# 只认 0/1:非法值即 ValueError,不静默 fallback。
|
||||||
|
for bad in ("yes", "2", "", " true"):
|
||||||
|
ex = self.make_exec(
|
||||||
|
env_extra={"CHV_CELL_ENABLED": bad})
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
ex._guest_host_command("status", "--task", "t1")
|
||||||
|
|
||||||
|
def test_cell_wiring_reaches_ssh_command(self):
|
||||||
|
# 接线落到 SSH 层:build_ssh_command 的远端命令里
|
||||||
|
# chv-cell 前缀在、python3 在其后。
|
||||||
|
ex = self.make_exec()
|
||||||
|
inner = ex._guest_host_command("status", "--task", "t1")
|
||||||
|
cmd = ex.build_ssh_command({"id": "t1"}, inner)
|
||||||
|
self.assertIn("chv-cell", cmd)
|
||||||
|
self.assertIn("danger-full-access", cmd)
|
||||||
|
self.assertLess(cmd.index("chv-cell"), cmd.index("python3"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -244,7 +244,14 @@ class SharedGoalTest(unittest.TestCase):
|
|||||||
chv = ChvOptCase()
|
chv = ChvOptCase()
|
||||||
chv.setUp()
|
chv.setUp()
|
||||||
try:
|
try:
|
||||||
ex = chv.make_exec()
|
# 单元路径(CHV_CELL_ENABLED 默认开)有意加 CHV
|
||||||
|
# 专属 argv:chv-cell 前缀 + --state/--codex-home
|
||||||
|
# + 强制 --sandbox danger-full-access,三执行器
|
||||||
|
# 顶层参数全等只在非单元路径上有意义,故这里显式
|
||||||
|
# 关单元;单元路径的“原参数一个不少”由
|
||||||
|
# test_chv_muse_round11_cell.py 钉死。
|
||||||
|
ex = chv.make_exec(
|
||||||
|
env_extra={"CHV_CELL_ENABLED": "0"})
|
||||||
local = LocalExecutor("/tmp/host.py")
|
local = LocalExecutor("/tmp/host.py")
|
||||||
docker = DockerExecutor()
|
docker = DockerExecutor()
|
||||||
for kwargs in (
|
for kwargs in (
|
||||||
|
|||||||
Reference in new issue
Block a user