Files
huni-probe-agent/main.py
huni f18eeaf334 harden probe agent v1.0.3
安全鉴权、默认只读、显式提交开关、状态接口与测试
2026-08-26 17:16:02 +08:00

268 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ModelHub XC 适配智能体安全探针 v1.0.3。
默认只做一次只读鉴权检查。只有显式设置 ALLOW_SUBMIT=1且鉴权检查成功后
才会调用 build-config 和 task/add每个进程生命周期最多提交一次。
"""
import copy
import json
import os
import signal
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Callable, Mapping, Optional, Tuple
VERSION = "1.0.3"
MAIN = os.getenv("MAIN_HOST", "https://modelhub.org.cn").rstrip("/")
STRATEGY_ID = os.getenv("STRATEGY_ID", "").strip()
PORT = int(os.getenv("PORT", "8080"))
ALLOW_SUBMIT = os.getenv("ALLOW_SUBMIT", "").strip().lower() in {"1", "true", "yes", "on"}
TEST_MODEL = os.getenv(
"TEST_MODEL",
"https://www.modelscope.cn/models/mradermacher/TrialSpace-1225-GGUF",
)
GPU = os.getenv("GPU", "MetaX_c-500")
FW = os.getenv("FW", "vllm")
TT = os.getenv("TT", "text-generation")
_TOKEN_PLACEHOLDERS = {"tmp", "placeholder", "changeme", "change-me", "example", "test"}
shutdown = threading.Event()
STATE_LOCK = threading.Lock()
def _initial_state() -> dict:
return {
"version": VERSION,
"phase": "starting",
"done": False,
"auth_ready": False,
"submit_enabled": ALLOW_SUBMIT,
"token_source": "none",
"results": {},
}
STATE = _initial_state()
def log(msg: str) -> None:
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True)
def _set_state(**changes) -> None:
with STATE_LOCK:
STATE.update(changes)
def _snapshot() -> dict:
with STATE_LOCK:
return copy.deepcopy(STATE)
def _reset_state() -> None:
"""Reset process state; kept separate to make the probe deterministically testable."""
with STATE_LOCK:
STATE.clear()
STATE.update(_initial_state())
def _valid_token(value: str) -> bool:
value = value.strip()
return bool(value) and value.lower() not in _TOKEN_PLACEHOLDERS
def _resolve_token(
env: Optional[Mapping[str, str]] = None,
read_text: Optional[Callable[[str], str]] = None,
) -> Tuple[str, str, Optional[str]]:
"""Resolve a credential without exposing its value or length.
Precedence is XC_TOKEN_FILE, XC_TOKEN, then the legacy
EXTERNAL_SERVICE_TOKEN. An explicitly configured but unreadable token file
is treated as an error instead of silently falling back.
"""
values = os.environ if env is None else env
file_reader = read_text or (lambda path: Path(path).read_text(encoding="utf-8"))
token_file = values.get("XC_TOKEN_FILE", "").strip()
if token_file:
try:
value = file_reader(token_file).strip()
except (OSError, UnicodeError):
return "", "XC_TOKEN_FILE", "token_file_unreadable"
if _valid_token(value):
return value, "XC_TOKEN_FILE", None
return "", "XC_TOKEN_FILE", "token_missing_or_placeholder"
for name in ("XC_TOKEN", "EXTERNAL_SERVICE_TOKEN"):
value = values.get(name, "").strip()
if _valid_token(value):
return value, name, None
if value:
return "", name, "token_missing_or_placeholder"
return "", "none", "token_missing_or_placeholder"
def _req(method: str, path: str, token: str, body=None, timeout: int = 30) -> dict:
headers = {"Accept": "application/json", "Xc-Token": token}
data = body.encode("utf-8") if isinstance(body, str) else body
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(MAIN + path, data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
text = response.read().decode("utf-8", "ignore")
status = response.status
except urllib.error.HTTPError as error:
text = error.read().decode("utf-8", "ignore")
status = error.code
except Exception as error: # noqa: BLE001 - network diagnostics are returned safely
return {"http": -1, "error": type(error).__name__}
try:
payload = json.loads(text)
except (TypeError, ValueError):
return {"http": status, "code": None, "message": "non_json_response"}
return {
"http": status,
"code": payload.get("code"),
"message": str(payload.get("message") or "")[:100],
"data": payload.get("data"),
}
def _api_ok(result: dict) -> bool:
try:
status = int(result.get("http", -1))
except (TypeError, ValueError):
return False
return 200 <= status < 300 and result.get("code") in {0, "0"}
def _result_summary(result: dict) -> dict:
summary = {"http": result.get("http"), "code": result.get("code")}
if result.get("message"):
summary["message"] = result["message"]
if result.get("error"):
summary["error"] = result["error"]
return summary
def _task_id(data) -> Optional[str]:
if not isinstance(data, dict):
return None
for key in ("id", "taskId", "task_id"):
if data.get(key) is not None:
return str(data[key])
return None
def probe() -> None:
"""Run one guarded probe; no retry loop and at most one task/add call."""
token, source, token_error = _resolve_token()
_set_state(token_source=source, submit_enabled=ALLOW_SUBMIT)
if not STRATEGY_ID:
_set_state(phase="blocked_missing_strategy_id", done=True)
log("probe blocked: STRATEGY_ID is missing")
return
if token_error:
_set_state(phase="blocked_missing_auth", done=True, results={"auth": token_error})
log(f"probe blocked: usable credential is unavailable (source={source})")
return
_set_state(phase="checking_auth")
page = _req("GET", "/api/adapt/task/page?current=1&pageSize=1", token)
results = {"task_page": _result_summary(page)}
if not _api_ok(page):
_set_state(phase="auth_failed", done=True, results=results)
log("read-only auth check failed: " + json.dumps(results["task_page"], ensure_ascii=False))
return
_set_state(auth_ready=True, results=results)
log(f"read-only auth check passed (source={source})")
if not ALLOW_SUBMIT:
_set_state(phase="read_only_complete", done=True)
log("probe complete in read-only mode; task/add was not called")
return
_set_state(phase="preparing_submission")
build_path = f"/api/adapt/task/build-config?gpuType={GPU}&framework={FW}&taskType={TT}"
build_config = _req("POST", build_path, token)
results["build_config"] = _result_summary(build_config)
config = build_config.get("data")
if not _api_ok(build_config) or not isinstance(config, str) or not config:
_set_state(phase="build_config_failed", done=True, results=results)
log("build-config failed; task/add was not called")
return
body = json.dumps(
{
"modelAddress": TEST_MODEL,
"taskType": TT,
"targetGpu": GPU,
"framework": FW,
"strategyId": STRATEGY_ID,
"configParams": config,
}
)
task_add = _req("POST", "/api/adapt/task/add", token, body)
results["task_add"] = _result_summary(task_add)
task_id = _task_id(task_add.get("data"))
if task_id:
results["task_add"]["task_id"] = task_id
phase = "submit_complete" if _api_ok(task_add) else "submit_failed"
_set_state(phase=phase, done=True, results=results)
log("single task/add attempt finished: " + json.dumps(results["task_add"], ensure_ascii=False))
class Handler(BaseHTTPRequestHandler):
def _json(self, obj: dict, status: int = 200) -> None:
payload = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self) -> None:
if self.path == "/health":
self._json({"status": "ok", "version": VERSION})
return
if self.path in {"/", "/status"}:
self._json({"name": "huni-probe-agent", "probe": _snapshot()})
return
self._json({"error": "not found"}, 404)
def log_message(self, *_args) -> None:
pass
def _handle_signal(signum: int, _frame) -> None:
shutdown.set()
log(f"received signal {signum}, shutting down")
def main() -> None:
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
threading.Thread(target=probe, daemon=True).start()
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
server.timeout = 1
log(f"probe agent v{VERSION} listening on 0.0.0.0:{PORT} (submit_enabled={ALLOW_SUBMIT})")
while not shutdown.is_set():
server.handle_request()
server.server_close()
log("stopped")
if __name__ == "__main__":
main()