"""对齐 Muse P9:推理与遥测受限通道。 全部用标准库与 mock,不启动真实 VM、不联网、不碰 WSL/真机; 解析器一律用假解析器(公网 IP / 私网 IP 两档)。 """ import importlib.util import json import os import sys import tempfile import unittest from pathlib import Path from unittest import mock try: from cloud.chv.executor import CloudHypervisorExecutor from cloud.chv.inference_channel import ( InferenceLimits, check_inference_call, inference_allow_hosts, inference_limits, resolve_inference_origin, ) from cloud.chv.sentinel import SentinelPolicy from cloud.chv.telemetry import ( build_task_telemetry, build_trajectory_event, emit_telemetry, sanitize_telemetry_event, sampled, set_training_enabled, telemetry_sample_rate, training_enabled, ) from cloud.sandbox import ChvFailure except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.chv.executor import CloudHypervisorExecutor from cloud.chv.inference_channel import ( InferenceLimits, check_inference_call, inference_allow_hosts, inference_limits, resolve_inference_origin, ) from cloud.chv.sentinel import SentinelPolicy from cloud.chv.telemetry import ( build_task_telemetry, build_trajectory_event, emit_telemetry, sanitize_telemetry_event, sampled, set_training_enabled, telemetry_sample_rate, training_enabled, ) from cloud.sandbox import ChvFailure def _public_resolver(host, port): """假解析器:公网 IP。""" return ["93.184.216.34"] def _private_resolver(host, port): """假解析器:私网 IP(SSRF 场景)。""" return ["10.0.0.5"] def _load_host_module(): """以独立模块名加载仓库根的 host.py(只用标准库)。""" path = Path(__file__).resolve().parents[2] / "host.py" spec = importlib.util.spec_from_file_location("host_under_test", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class OriginTest(unittest.TestCase): def test_valid_sub2api_origin(self): env = {"SUB2API_BASE_URL": "https://xgapi.lol/"} self.assertEqual( resolve_inference_origin("sub2api", env), "https://xgapi.lol") def test_missing_origin_returns_empty(self): self.assertEqual(resolve_inference_origin("sub2api", {}), "") self.assertEqual(resolve_inference_origin(None, {}), "") def test_http_origin_rejected(self): env = {"SUB2API_BASE_URL": "http://xgapi.lol"} with self.assertRaises(ValueError): resolve_inference_origin("sub2api", env) def test_http_public_ip_rejected(self): env = {"SUB2API_BASE_URL": "http://8.8.8.8:28084"} with self.assertRaises(ValueError): resolve_inference_origin("sub2api", env) def test_http_hostname_rejected(self): env = {"SUB2API_BASE_URL": "http://relay.local:28084"} with self.assertRaises(ValueError): resolve_inference_origin("sub2api", env) def test_http_local_relay_allowed(self): # 文档形状:本地 sub2api 中继走 Docker 桥接 IP + http, # 端口保留;环回同款。 env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084/"} self.assertEqual( resolve_inference_origin("sub2api", env), "http://172.17.0.1:28084") env2 = {"SUB2API_BASE_URL": "http://127.0.0.1:28084"} self.assertEqual( resolve_inference_origin("sub2api", env2), "http://127.0.0.1:28084") def test_userinfo_in_origin_rejected(self): env = {"SUB2API_BASE_URL": "https://user:pass@xgapi.lol"} with self.assertRaises(ValueError): resolve_inference_origin("sub2api", env) def test_non_443_port_rejected(self): env = {"SUB2API_BASE_URL": "https://xgapi.lol:8443"} with self.assertRaises(ValueError): resolve_inference_origin("sub2api", env) def test_unknown_provider_falls_back_to_default_origin(self): # 未知标签与 host.py `_server_command` 一致走默认网关; # 未配 CHV_INFERENCE_ORIGIN 时返回 ""(调用方按 ask 处理)。 env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol"} self.assertEqual( resolve_inference_origin("openai", env), "https://xgapi.lol") self.assertEqual(resolve_inference_origin("openai", {}), "") def test_xcmapi_origin_from_explicit_env(self): env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol"} self.assertEqual( resolve_inference_origin(None, env), "https://xgapi.lol") class AllowlistTest(unittest.TestCase): def test_explicit_allowlist_wins(self): env = {"SUB2API_BASE_URL": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "a.example, B.example "} hosts = inference_allow_hosts("sub2api", env) self.assertEqual(hosts, frozenset({"a.example", "b.example"})) def test_default_allowlist_is_origin_host(self): env = {"SUB2API_BASE_URL": "https://xgapi.lol"} self.assertEqual(inference_allow_hosts("sub2api", env), frozenset({"xgapi.lol"})) def test_no_origin_means_empty_allowlist(self): self.assertEqual(inference_allow_hosts("sub2api", {}), frozenset()) class LimitsTest(unittest.TestCase): def test_defaults(self): self.assertEqual(inference_limits({}), InferenceLimits(timeout_seconds=120, max_retries=1)) def test_env_override(self): env = {"CHV_INFERENCE_TIMEOUT": "60", "CHV_INFERENCE_MAX_RETRIES": "3"} self.assertEqual(inference_limits(env), InferenceLimits(timeout_seconds=60, max_retries=3)) def test_invalid_timeout_rejected(self): for bad in ("abc", "5", "601", "-1"): with self.assertRaises(ValueError): inference_limits({"CHV_INFERENCE_TIMEOUT": bad}) def test_invalid_retries_rejected(self): for bad in ("x", "-1", "6"): with self.assertRaises(ValueError): inference_limits({"CHV_INFERENCE_MAX_RETRIES": bad}) class PreflightTest(unittest.TestCase): def setUp(self): self.env = {"SUB2API_BASE_URL": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"} def test_allow_with_public_ip(self): check = check_inference_call( "sub2api", self.env, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "allow") self.assertIsNotNone(check.decision.capability) self.assertTrue(check.decision.capability.matches(check.request)) self.assertEqual(check.request.host, "xgapi.lol") self.assertEqual(check.request.path, "/v1/responses") def test_capability_single_use_300s(self): check = check_inference_call( "sub2api", self.env, resolve=_public_resolver, now=1000.0) cap = check.decision.capability self.assertTrue(cap.use(check.request, now=1000.0)) self.assertFalse(cap.use(check.request, now=1000.0)) self.assertFalse(cap.matches(check.request, now=2000.0)) def test_local_relay_allow_without_dns(self): # 本地中继是字面私网 IP:不查 DNS(解析器被调即错), # allowlist 默认即 origin 主机,直接 allow + 单次能力。 def _must_not_resolve(host, port): raise AssertionError("local relay must not resolve") env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084"} check = check_inference_call( "sub2api", env, resolve=_must_not_resolve) self.assertEqual(check.decision.verdict, "allow") self.assertIn("local_relay", check.decision.reasons) self.assertIsNotNone(check.decision.capability) self.assertEqual(check.request.host, "172.17.0.1") self.assertEqual(check.request.port, 28084) self.assertEqual(check.request.scheme, "http") def test_local_relay_ask_when_not_allowlisted(self): env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"} check = check_inference_call( "sub2api", env, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "ask") def test_deny_on_private_ip(self): check = check_inference_call( "sub2api", self.env, resolve=_private_resolver) self.assertEqual(check.decision.verdict, "deny") self.assertIn("ssrf_private_ip", check.decision.reasons[0]) def test_ask_on_non_allowlisted_host(self): # 显式 allowlist 已配、origin 主机不在其中 → ask。 # (未配 allowlist 时默认即 origin 主机自身,不会 ask—— # 见 test_default_allowlist_is_origin_host。) env = {"SUB2API_BASE_URL": "https://unknown.example", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"} check = check_inference_call( "sub2api", env, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "ask") def test_ask_when_origin_not_configured(self): check = check_inference_call( None, {}, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "ask") self.assertIn("inference_origin_not_configured", check.decision.reasons) def test_xcmapi_origin_allow(self): env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"} check = check_inference_call( None, env, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "allow") def test_unknown_provider_falls_back_to_default_origin(self): # 未知标签不 deny:与 _server_command 一致走默认网关; # 未配 CHV_INFERENCE_ORIGIN 时仍是 ask(fail-closed)。 env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"} check = check_inference_call( "prov-x", env, resolve=_public_resolver) self.assertEqual(check.decision.verdict, "allow") check2 = check_inference_call( "prov-x", {}, resolve=_public_resolver) self.assertEqual(check2.decision.verdict, "ask") def test_deny_path_out_of_scope(self): policy = SentinelPolicy( allow_hosts={"xgapi.lol"}, connector_paths={"inference": (("POST", "prefix", "/v1/responses"),)}) from cloud.chv.sentinel import EgressRequest request = EgressRequest("inference", "GET", "https", "xgapi.lol", 443, "/v1/admin") decision = policy.evaluate(request, resolve=_public_resolver) self.assertEqual(decision.verdict, "deny") def test_audit_record_has_no_values(self): check = check_inference_call( "sub2api", self.env, resolve=_public_resolver) record = check.decision.audit(check.request) blob = json.dumps(record) self.assertNotIn("xgapi.lol".replace("xgapi", "SECRET"), blob) self.assertEqual(record["host"], "xgapi.lol") self.assertEqual(record["verdict"], "allow") class FakeResult: def __init__(self, returncode=0, stdout="", stderr=""): self.returncode = returncode self.stdout = stdout self.stderr = stderr def _write(path, content=b"x"): with open(path, "wb") as fh: fh.write(content) os.chmod(path, 0o755) return path class GateTest(unittest.TestCase): """执行器预审:deny/ask 时不起 VM(wake 未被调用);allow 才继续。""" 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") self.wake_calls = [] self.audit = [] def tearDown(self): self.tmp.cleanup() def _make_exec(self, env_extra): env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888", "CHV_HOST_KEY_MODE": "keyscan", "CHV_SSH_FORWARD": "1"} env.update(env_extra) ex = 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: FakeResult(0, "", ""), api_client=lambda *a, **k: FakeResult(0, "", ""), sleeper=lambda s: None, env=env) ex.audit_callback = lambda e, t, d, k=None: self.audit.append( (e, t.get("id") if isinstance(t, dict) else t, d, k)) return ex def test_deny_blocks_wake(self): ex = self._make_exec( {"SUB2API_BASE_URL": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}) ex.sentinel_resolver = _private_resolver ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake")) task = {"id": "t1", "prompt": "hi"} with self.assertRaises(ChvFailure) as ctx: ex.dispatch_task(task, model_provider="sub2api") self.assertEqual(ctx.exception.kind, "inference_egress_denied") ex.wake_vm.assert_not_called() kinds = [e[0] for e in self.audit] self.assertIn("inference_egress_deny", kinds) def test_ask_blocks_wake_when_origin_missing(self): ex = self._make_exec({}) ex.sentinel_resolver = _public_resolver ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake")) task = {"id": "t2", "prompt": "hi"} with self.assertRaises(ChvFailure) as ctx: ex.dispatch_task(task, model_provider=None) self.assertEqual(ctx.exception.kind, "inference_egress_denied") ex.wake_vm.assert_not_called() def test_extra_env_origin_is_honored(self): # 网关配置随任务来(worker 的 extra_env),执行器 # 进程 env 里没有 origin 也要放行。 ex = self._make_exec({}) ex.sentinel_resolver = _public_resolver ex.wake_vm = mock.Mock() task = {"id": "t3", "prompt": "hi"} with mock.patch.object(ex, "_run_guest", return_value={"status": "completed"}): with mock.patch.object(ex, "shutdown_vm"): ex.dispatch_task( task, model_provider="sub2api", extra_env={"SUB2API_BASE_URL": "https://xgapi.lol"}) ex.wake_vm.assert_called_once() def test_bad_origin_becomes_deny_not_value_error(self): ex = self._make_exec({}) ex.sentinel_resolver = _public_resolver ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake")) task = {"id": "t4", "prompt": "hi"} with self.assertRaises(ChvFailure) as ctx: ex.dispatch_task( task, model_provider="sub2api", extra_env={"SUB2API_BASE_URL": "http://xgapi.lol"}) self.assertEqual(ctx.exception.kind, "inference_egress_denied") ex.wake_vm.assert_not_called() kinds = [e[0] for e in self.audit] self.assertIn("inference_egress_deny", kinds) class TelemetryTest(unittest.TestCase): def test_allowlisted_fields_only(self): event = build_task_telemetry( task_id="t1", vm_id="vm1", provider="sub2api", model="gpt-6.1-terra", status="completed", started_at=1000.0, ended_at=1005.0, duration_ms=5000, tokens={"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}, turns=2, tool_calls=3, env={"CHV_TELEMETRY_SAMPLE_RATE": "1"}) self.assertIsNotNone(event) self.assertEqual(event["tokens_in"], 10) self.assertEqual(event["tokens_total"], 15) self.assertEqual(event["turns"], 2) def test_secret_and_text_keys_dropped(self): event = {"task_id": "t1", "vm_id": "v", "provider": "sub2api", "model": "m", "status": "completed", "started_at": 1.0, "ended_at": 2.0, "duration_ms": 1, "prompt": "do evil", "content": "secret file", "api_key": "sk-123", "user_email": "a@b.c", "extra_blob": {"nested": "data"}} clean = sanitize_telemetry_event(event) for bad in ("prompt", "content", "api_key", "user_email", "extra_blob"): self.assertNotIn(bad, clean) self.assertEqual(clean["task_id"], "t1") def test_tokens_none_when_not_reported(self): event = build_task_telemetry( task_id="t1", vm_id="vm1", provider="sub2api", model=None, status="failed", started_at=1.0, ended_at=2.0, duration_ms=1, env={"CHV_TELEMETRY_SAMPLE_RATE": "1"}) self.assertIsNone(event["tokens_in"]) self.assertIsNone(event["tokens_out"]) self.assertIsNone(event["tokens_total"]) def test_sampling_deterministic(self): self.assertEqual(sampled("task-abc", 1.0), True) self.assertEqual(sampled("task-abc", 0.0), False) self.assertEqual(sampled("task-abc", 0.5), sampled("task-abc", 0.5)) event = build_task_telemetry( task_id="t1", vm_id="v", provider="sub2api", model=None, status="completed", started_at=1.0, ended_at=2.0, duration_ms=1, env={"CHV_TELEMETRY_SAMPLE_RATE": "0"}) self.assertIsNone(event) def test_invalid_sample_rate_rejected(self): with self.assertRaises(ValueError): telemetry_sample_rate({"CHV_TELEMETRY_SAMPLE_RATE": "2"}) with self.assertRaises(ValueError): telemetry_sample_rate({"CHV_TELEMETRY_SAMPLE_RATE": "x"}) def test_emit_never_raises(self): self.assertFalse(emit_telemetry("/nonexistent/dir/xyz", {"task_id": "t1"})) self.assertFalse(emit_telemetry("", {"task_id": "t1"})) def test_emit_writes_jsonl(self): with tempfile.TemporaryDirectory() as d: event = {"task_id": "t9", "vm_id": "v", "status": "completed"} self.assertTrue(emit_telemetry(d, event)) with open(os.path.join(d, "telemetry.jsonl")) as fh: row = json.loads(fh.read().strip()) self.assertEqual(row["task_id"], "t9") self.assertNotIn("prompt", row) class TrainingOptTest(unittest.TestCase): def test_default_enabled(self): with tempfile.TemporaryDirectory() as d: self.assertTrue(training_enabled(d)) def test_set_off_persists_and_blocks_trajectory(self): with tempfile.TemporaryDirectory() as d: self.assertTrue(set_training_enabled(d, False)) self.assertFalse(training_enabled(d)) event = build_trajectory_event( "t1", 3, ["read", "write"], 100, d) self.assertIsNone(event) # 任务级遥测不受开关影响 task_event = build_task_telemetry( "t1", "v", "sub2api", None, "completed", 1.0, 2.0, 1, env={"CHV_TELEMETRY_SAMPLE_RATE": "1"}) self.assertIsNotNone(task_event) def test_trajectory_has_metadata_only(self): with tempfile.TemporaryDirectory() as d: event = build_trajectory_event( "t1", 2, ["read", "exec"], 50, d) self.assertIsNotNone(event) self.assertEqual(event["tools"], ["read", "exec"]) self.assertNotIn("prompt", event) self.assertNotIn("content", event) def test_corrupt_state_fails_closed(self): with tempfile.TemporaryDirectory() as d: with open(os.path.join(d, "training-opt.json"), "w") as fh: fh.write("{not json") self.assertFalse(training_enabled(d)) def test_non_bool_rejected(self): with tempfile.TemporaryDirectory() as d: with self.assertRaises(TypeError): set_training_enabled(d, "off") def test_switch_change_is_audited(self): with tempfile.TemporaryDirectory() as d: set_training_enabled(d, False, actor="tester") audit_path = os.path.join(d, "training-opt-audit.jsonl") with open(audit_path) as fh: row = json.loads(fh.read().strip()) self.assertEqual(row["event"], "training_opt_changed") self.assertFalse(row["enabled"]) self.assertEqual(row["actor"], "tester") class HostPyTest(unittest.TestCase): def setUp(self): self.host = _load_host_module() def test_server_command_valid_origin(self): with mock.patch.dict(os.environ, {"SUB2API_BASE_URL": "https://xgapi.lol"}): cmd = self.host._server_command("codex", provider="sub2api") self.assertIn("app-server", cmd) self.assertTrue(any("xgapi.lol/v1" in c for c in cmd)) def test_server_command_http_origin_rejected(self): with mock.patch.dict(os.environ, {"SUB2API_BASE_URL": "http://xgapi.lol"}): with self.assertRaises(ValueError): self.host._server_command("codex", provider="sub2api") def test_server_command_local_relay_allowed(self): # 文档形状的本地中继(http + 桥接 IP)必须通过第二道防线。 with mock.patch.dict(os.environ, {"SUB2API_BASE_URL": "http://172.17.0.1:28084"}): cmd = self.host._server_command("codex", provider="sub2api") self.assertTrue(any("172.17.0.1:28084/v1" in c for c in cmd)) def test_server_command_allowlist_mismatch_rejected(self): with mock.patch.dict(os.environ, {"SUB2API_BASE_URL": "https://xgapi.lol", "CHV_INFERENCE_ALLOW_HOSTS": "other.example"}): with self.assertRaises(ValueError): self.host._server_command("codex", provider="sub2api") def test_request_timeout_default_and_env(self): self.assertEqual( self.host._inference_request_timeout(None), 120) self.assertEqual( self.host._inference_request_timeout(60), 60) with mock.patch.dict(os.environ, {"CHV_INFERENCE_TIMEOUT": "30"}): self.assertEqual( self.host._inference_request_timeout(None), 30) with mock.patch.dict(os.environ, {"CHV_INFERENCE_TIMEOUT": "5"}): with self.assertRaises(ValueError): self.host._inference_request_timeout(None) def test_request_retries_default_and_env(self): self.assertEqual(self.host._inference_max_retries(), 1) with mock.patch.dict(os.environ, {"CHV_INFERENCE_MAX_RETRIES": "0"}): self.assertEqual(self.host._inference_max_retries(), 0) with mock.patch.dict(os.environ, {"CHV_INFERENCE_MAX_RETRIES": "9"}): with self.assertRaises(ValueError): self.host._inference_max_retries() if __name__ == "__main__": unittest.main()