feat-chv-muse-p9: 推理与遥测受限通道(Sentinel 预审 + 遥测最小化 + 训练退出开关)

- 受限推理通道:cloud/chv/inference_channel.py(origin 解析、
  allowlist、上限、Sentinel 预审),执行器 _run_with_vm 起 VM 前
  必过 _inference_gate(allow 才继续,deny/ask 审计 + fail-closed,
  任务级 extra_env 合并进预审);host.py 第二道防线(_server_command
  的 origin 结构校验 + Server.request 超时/重试走 env)。
  origin 规则:https 公网网关(只 443);http 只接受本地中继
  (字面环回/私网 IP,如 http://172.17.0.1:28084,端口保留,
  无 DNS、无重绑定,allowlist 与单次能力照常);未知 provider
  标签走默认网关(与 _server_command 一致)。
- sentinel.py:从 P5(25bcd35)取的超集版本 + 推理连接器条目
  (POST /v1/responses、GET /v1/models;scheme/端口只 https/443,
  本地中继不走 evaluate,不把 http 加进表)。
- 遥测最小化:cloud/chv/telemetry.py(字段白名单、确定性采样、
  落盘失败不影响任务、token 未回传记 null)+ 训练退出开关
  (默认开,本项目决定;关后轨迹不再产生;损坏 fail-closed;变更审计)。
- 访问边界:docs/chv-access-boundary.md(运营访问口径 + 审计钩子,
  不冒充技术不可访问;Confidential VM 明确未做)。
- 测试:cloud/tests/test_chv_muse_p9.py 51 组全过;5 个旧文件
  的 make_exec 配假 origin + 假解析器(新 fail-closed 门要求的
  测试配置);全量 cloud 套件 1918 OK(skipped=8)。
  详见 docs/chv-muse-p9-gap.md(含自定清单与真机待验证清单)。
This commit is contained in:
host committed 2026-11-06 23:03:06 +00:00
1 parent 06c3e172fe
commit c06ba50983
13 files changed
+1922 -11

No files matched your search

+89 -4
View File
@@ -45,6 +45,7 @@ CODEX_BIN=.../bin/codex(start.sh 已自动优先用 bin/codex)。
"""
import argparse
import contextlib
import ipaddress
import json
import os
import queue
@@ -56,6 +57,7 @@ import threading
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import urlparse
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_STATE = os.path.join(HERE, "host-state.json")
@@ -338,6 +340,81 @@ def _append_trace_marker(state_path, task_name, entry):
pass
def _inference_request_timeout(timeout):
"""单次请求超时(秒):显式传参优先;否则读
`CHV_INFERENCE_TIMEOUT`(默认 120,与旧默认值一致),
非整数或越界(10–600)即 ValueError。"""
if timeout is not None:
return timeout
raw = os.environ.get("CHV_INFERENCE_TIMEOUT", "").strip()
if not raw:
return 120
try:
value = int(raw)
except ValueError:
raise ValueError(
"CHV_INFERENCE_TIMEOUT 必须是整数: %r" % raw)
if not 10 <= value <= 600:
raise ValueError(
"CHV_INFERENCE_TIMEOUT 必须在 10-600 之间: %s" % value)
return value
def _inference_max_retries():
"""BrokenPipe 重连上限(次):读 `CHV_INFERENCE_MAX_RETRIES`,
默认 1(即现有行为 `range(2)`),非整数或越界(0–5)即
ValueError。模型回合级重试仍是 worker 的 max_retries 语义,
这里只 bound 传输层重连。"""
raw = os.environ.get("CHV_INFERENCE_MAX_RETRIES", "").strip()
if not raw:
return 1
try:
value = int(raw)
except ValueError:
raise ValueError(
"CHV_INFERENCE_MAX_RETRIES 必须是整数: %r" % raw)
if not 0 <= value <= 5:
raise ValueError(
"CHV_INFERENCE_MAX_RETRIES 必须在 0-5 之间: %s" % value)
return value
def _validate_inference_origin(origin):
"""sub2api origin 结构校验(feat-chv-muse-p9,第二道防线,
只用标准库):https 公网网关(端口只 443)或 http 本地中继
(字面环回/私网 IP,如 http://172.17.0.1:28084,见
cloud/README.md 的 sub2api 网关一节);无 userinfo、有主机
名;若 `CHV_INFERENCE_ALLOW_HOSTS` 已配,主机必须在其中。
第一道防线是执行器的 Sentinel 预审(起 VM 前);这里拦的
是“执行器没审就直接起 codex”的旁路(配置错了 loud fail,
不静默跑向别处)。报错不带 origin 原值。"""
parsed = urlparse(origin)
scheme = parsed.scheme.lower()
if "@" in parsed.netloc:
raise ValueError("推理 origin 不得带 userinfo(凭证禁进 URL)")
host = (parsed.hostname or "").lower()
if not host:
raise ValueError("推理 origin 无主机名")
if scheme == "https":
if parsed.port is not None and parsed.port != 443:
raise ValueError("推理 origin 端口只接受 443")
elif scheme == "http":
try:
addr = ipaddress.ip_address(host)
except ValueError:
raise ValueError("http 推理 origin 只接受本地中继"
"(环回/私网 IP)")
if not (addr.is_loopback or addr.is_private):
raise ValueError("http 推理 origin 只接受本地中继"
"(环回/私网 IP)")
else:
raise ValueError("推理 origin 必须 https(或本地中继 http)")
allowed = os.environ.get("CHV_INFERENCE_ALLOW_HOSTS", "")
hosts = {h.strip().lower() for h in allowed.split(",") if h.strip()}
if hosts and host not in hosts:
raise ValueError("推理 origin 主机不在 allowlist 内: %s" % host)
def _server_command(codex_bin, provider=None):
"""app-server 启动命令:-c 必须放在子命令之前。
@@ -361,6 +438,9 @@ def _server_command(codex_bin, provider=None):
if not origin:
raise RuntimeError(
"provider=sub2api 但 SUB2API_BASE_URL 未配置")
# feat-chv-muse-p9:origin 结构校验(第二道防线;第一道
# 是执行器起 VM 前的 Sentinel 预审)。
_validate_inference_origin(origin)
cmd += [
"-c", 'model_provider="sub2api"',
"-c", 'model_providers.sub2api.name="Sub2API"',
@@ -519,14 +599,19 @@ class Server:
self.proc.stdin.write((json.dumps(obj) + "\n").encode())
self.proc.stdin.flush()
def request(self, method, params=None, timeout=120):
# BrokenPipe / 进程已死时重拉一次再重试(只重试一次,
def request(self, method, params=None, timeout=None):
# BrokenPipe / 进程已死时重拉再重试(重试上限见
# `_inference_max_retries`,默认 1 次即现有行为;
# turn/start 这类有副作用的调用由上层 resume 后再跑,
# 这里重试的是 initialize/list/resume 等幂等请求为主;
# 即使是 turn/start,重试也只发生在“还没收到任何响应”
# 的 BrokenPipe 场景,server 死在 turn 中途时 _read_loop
# 已退出、q.get 会超时而不是走这条路)。
for attempt in range(2):
# feat-chv-muse-p9:默认超时走 CHV_INFERENCE_TIMEOUT
# (默认 120 秒,即旧默认值),显式传参优先。
timeout = _inference_request_timeout(timeout)
max_retries = _inference_max_retries()
for attempt in range(1 + max_retries):
rid = self.next_id
self.next_id += 1
q = queue.Queue()
@@ -539,7 +624,7 @@ class Server:
except (BrokenPipeError, OSError):
with self.pending_lock:
self.pending.pop(rid, None)
if attempt == 0 and self.ensure_alive():
if attempt < max_retries and self.ensure_alive():
continue
raise
finally: