Files
Cognitive_Agent/src/cognitive_agent/server.py
T

1080 lines
43 KiB
Python
Executable File

"""Dependency-light HTTP server for the cabin orchestration demo."""
from __future__ import annotations
import argparse
import hmac
import ipaddress
import json
import mimetypes
import os
import re
import socket
from http.cookies import CookieError, SimpleCookie
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import ParseResult, parse_qs, quote, unquote, urlparse
from cognitive_agent.config_loader import load_local_env
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
# Repository-local launch configuration is deliberate and must not be shadowed
# by stale variables inherited from an IDE or an earlier model experiment.
load_local_env(REPOSITORY_ROOT / ".env", override=True)
load_local_env(REPOSITORY_ROOT / ".env.auth", override=True)
from cognitive_agent.auth import AuthConfig, AuthRuntime, verify_password # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.calibration.service import ( # noqa: E402
ApiError as CalibrationApiError,
CalibrationService,
)
from cognitive_agent.application.self_extension import ( # noqa: E402
ExtensionIntegrationError,
GatewayError,
SelfExtensionService,
)
from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareBusyError,
HardwareSafetyError,
)
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
HardwareAccessRequiredError,
)
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError # noqa: E402
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
mimetypes.add_type("application/manifest+json", ".webmanifest")
mimetypes.add_type("model/gltf-binary", ".glb")
REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]+$")
PUBLIC_AUTH_PATHS = {
"/login",
"/login.html",
"/login.css",
"/login.js",
"/network.js",
"/api/auth/status",
"/api/auth/login",
}
def _lan_ipv4_address() -> str | None:
"""Return the IPv4 address selected by the host's default route."""
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
probe.connect(("192.0.2.1", 80))
address = str(probe.getsockname()[0])
return None if address.startswith("127.") else address
except OSError:
return None
finally:
probe.close()
class SeatDemoHandler(BaseHTTPRequestHandler):
service = SeatDemoService()
gesture = GestureCapabilityBridge(REPOSITORY_ROOT)
extension = SelfExtensionService()
calibration = CalibrationService(
REPOSITORY_ROOT / "artifacts" / "tmp" / "self-calibration",
repository_root=REPOSITORY_ROOT,
)
repair = SelfRepairService()
auth = AuthRuntime(AuthConfig.from_env())
server_version = "CognitiveAgentCabinDemo/2.0"
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
parsed_url = urlparse(self.path)
path = unquote(parsed_url.path)
if path == "/favicon.ico":
self.send_response(HTTPStatus.NO_CONTENT)
self._send_security_headers()
self.end_headers()
return
if path == "/api/auth/status":
username = self._session_username()
self._json(
{
"enabled": self.auth.config.enabled,
"authenticated": not self.auth.config.enabled or username is not None,
"username": username,
"session_ttl_seconds": self.auth.config.session_ttl_seconds,
"requests_per_minute": self.auth.config.requests_per_minute,
}
)
return
if path in {"/login", "/login.html"}:
if self.auth.config.enabled and self._session_username() is not None:
self._redirect("/")
return
self._serve_static_file("login.html")
return
if not self._authorize(path):
return
if path in {"/hardware-control", "/hardware-control.html"}:
if not self._require_local_decision_point():
return
self._serve_static_file("hardware-control.html")
return
if path in {"/hardware-control.js", "/hardware-control.css"}:
if not self._require_local_decision_point():
return
self._serve_static_file(path.lstrip("/"))
return
if path == "/api/local/hardware-access":
if not self._require_local_decision_point():
return
self._json(self.service.hardware_access.admin_snapshot())
return
if path == "/api/hardware/access/status":
query = parse_qs(parsed_url.query)
try:
self._json(
self.service.hardware_access.status(
str(query.get("client_id", [""])[0])
)
)
except ValueError as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
return
if path == "/api/health":
self._json({"status": "ok", "service": "cabin-cognitive-orchestrator"})
return
if path == "/api/runtime":
self._json(self.service.runtime_info())
return
if path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
query = parse_qs(parsed_url.query)
conversation_id = str(query.get("conversation_id", [""])[0])
vehicle_id = str(query.get("vehicle_id", [""])[0]) or None
hardware_client_id = str(query.get("hardware_client_id", [""])[0]) or None
try:
self._json(
(
self.service.synchronize_vehicle_state
if path == "/api/hardware/vehicle-state"
else self.service.synchronize_hardware_state
)(
conversation_id,
vehicle_id,
hardware_client_id=hardware_client_id,
hardware_access_bypass=self._is_unproxied_loopback_request(),
)
)
except HardwareAccessRequiredError as exc:
self._json(
{
"error": "hardware_access_required",
"message": str(exc),
"access_state": exc.state,
},
HTTPStatus.FORBIDDEN,
)
except HardwareBusyError as exc:
self._json(
{
"error": "hardware_busy",
"message": str(exc),
"retry_after_seconds": exc.retry_after_seconds,
},
HTTPStatus.CONFLICT,
{"Retry-After": str(exc.retry_after_seconds)},
)
except ValueError as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - surface passive CAN availability
self.log_error("physical CAN state synchronization failed: %s", exc)
self._json(
{
"error": "hardware_state_unavailable",
"message": str(exc),
},
HTTPStatus.SERVICE_UNAVAILABLE,
)
return
if path == "/api/gesture/runtime":
self._json(self.gesture.runtime_info())
return
if path == "/api/gesture/model":
self._serve_gesture_model()
return
if path == "/api/extension/health":
self._json(self.extension.health())
return
if path == "/api/extension/gscu/info":
self._handle_extension_gateway("info", {})
return
if path.startswith("/api/calibration/"):
if not self._require_calibration_admin(parsed_url):
return
if path == "/api/calibration/state":
self._json(self.calibration.snapshot())
return
if path == "/api/calibration/events":
self._serve_calibration_events()
return
if path == "/api/calibration/video/stream":
self._serve_calibration_video()
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if path.startswith("/api/self-repair/"):
if not self._require_self_repair_admin(parsed_url):
return
if path == "/api/self-repair/state":
self._json(self.repair.snapshot())
return
if path == "/api/self-repair/health":
self._json(self.repair.health())
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if path.startswith("/api/requests/"):
self._serve_request(path.removeprefix("/api/requests/"))
return
self._serve_static(path)
def do_POST(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
if path == "/api/auth/login":
self._login()
return
if path == "/api/auth/logout":
self._logout()
return
if not self._authorize(path):
return
if path in {
"/api/hardware/access/request",
"/api/hardware/access/release",
"/api/local/hardware-access/approve",
"/api/local/hardware-access/deny",
"/api/local/hardware-access/revoke",
}:
self._handle_hardware_access_post(path)
return
if path.startswith("/api/extension/"):
self._handle_extension_post(path)
return
if path.startswith("/api/calibration/"):
self._handle_calibration_post(path)
return
if path.startswith("/api/self-repair/"):
self._handle_self_repair_post(path)
return
if path != "/api/requests":
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
try:
username = self._session_username() or "local"
retry_after = self.auth.api_requests.retry_after(username)
if self.auth.config.enabled and retry_after:
self._json(
{
"error": "rate_limited",
"message": "请求过于频繁,请稍后再试。",
},
HTTPStatus.TOO_MANY_REQUESTS,
{"Retry-After": str(retry_after)},
)
return
if self.auth.config.enabled:
self.auth.api_requests.record(username)
payload = self._read_json_body()
result = self.service.process(
str(payload.get("utterance", "")),
str(payload.get("provider", "auto")),
str(payload["conversation_id"]) if payload.get("conversation_id") else None,
str(payload.get("execution_mode", "dry_run")),
# Hardware authorization is a server deployment policy. The
# browser no longer renders or transmits an approval secret.
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"),
vehicle_id=(
str(payload["vehicle_id"])
if payload.get("vehicle_id")
else None
),
simulation_fault=str(payload.get("simulation_fault", "normal")),
hardware_client_id=(
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
),
hardware_access_bypass=self._is_unproxied_loopback_request(),
)
self._json(result, HTTPStatus.CREATED)
except HardwareAccessRequiredError as exc:
self._json(
{
"error": "hardware_access_required",
"message": str(exc),
"access_state": exc.state,
},
HTTPStatus.FORBIDDEN,
)
except HardwareBusyError as exc:
self._json(
{
"error": "hardware_busy",
"message": str(exc),
"retry_after_seconds": exc.retry_after_seconds,
},
HTTPStatus.CONFLICT,
{"Retry-After": str(exc.retry_after_seconds)},
)
except HardwareSafetyError as exc:
self._json(
{"error": "hardware_safety_gate", "message": str(exc)},
HTTPStatus.FORBIDDEN,
)
except (ValueError, json.JSONDecodeError) as exc:
self._json({"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST)
except OpenAIIntentError as exc:
self.log_error("OpenAI intent request failed: %s", exc)
self._json(
{
"error": "openai_request_failed",
"message": "GPT 意图解析失败,请检查 API Key、模型权限或网络状态。",
},
HTTPStatus.BAD_GATEWAY,
)
except Exception as exc: # noqa: BLE001 - keep the demo API responsive
self.log_error("request failed: %s", exc)
self._json(
{
"error": "internal_error",
"message": "请求处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def do_DELETE(self) -> None: # noqa: N802 - stdlib handler API
parsed_url = urlparse(self.path)
path = unquote(parsed_url.path)
if not self._authorize(path):
return
prefix = "/api/mode-profiles/"
if not path.startswith(prefix):
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
mode_id = path.removeprefix(prefix).strip()
vehicle_id = str(parse_qs(parsed_url.query).get("vehicle_id", [""])[0]) or None
try:
self._json(
self.service.delete_user_mode_profile(mode_id, vehicle_id),
HTTPStatus.OK,
)
except KeyError:
self._json(
{"error": "not_found", "message": "未找到可删除的用户自定义模式。"},
HTTPStatus.NOT_FOUND,
)
except PermissionError as exc:
self._json(
{"error": "preset_mode_protected", "message": str(exc)},
HTTPStatus.FORBIDDEN,
)
except ValueError as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
def _handle_extension_post(self, path: str) -> None:
action_map = {
"/api/extension/gscu/learn": "learn",
"/api/extension/gscu/cancel": "cancel",
"/api/extension/gscu/emit": "emit",
"/api/extension/gscu/read": "read",
"/api/extension/gscu/erase": "erase",
}
try:
payload = self._read_json_body(max_bytes=256 * 1024)
action = action_map.get(path)
if action:
self._handle_extension_gateway(action, payload)
return
if path == "/api/extension/assistant/plan":
self._json(self.extension.plan(payload))
return
if path == "/api/extension/vehicle/execute":
result = self.extension.execute_vehicle(
payload,
self.service,
hardware_access_bypass=self._is_unproxied_loopback_request(),
)
self._json(result)
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
except GatewayError as exc:
self._json(
{"success": False, "error": "gscu_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
except ExtensionIntegrationError as exc:
self._json(
{"success": False, "error": "extension_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
except HardwareAccessRequiredError as exc:
self._json(
{
"success": False,
"error": "hardware_access_required",
"message": str(exc),
"access_state": exc.state,
},
HTTPStatus.FORBIDDEN,
)
except HardwareBusyError as exc:
self._json(
{
"success": False,
"error": "hardware_busy",
"message": str(exc),
"retry_after_seconds": exc.retry_after_seconds,
},
HTTPStatus.CONFLICT,
{"Retry-After": str(exc.retry_after_seconds)},
)
except HardwareSafetyError as exc:
self._json(
{"success": False, "error": "hardware_safety_gate", "message": str(exc)},
HTTPStatus.FORBIDDEN,
)
except OpenAIIntentError as exc:
self.log_error("extension vehicle intent request failed: %s", exc)
self._json(
{
"success": False,
"error": "openai_request_failed",
"message": "车端 GPT 意图解析失败,请检查模型配置。",
},
HTTPStatus.BAD_GATEWAY,
)
except (ValueError, json.JSONDecodeError) as exc:
self._json(
{"success": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep extension APIs responsive
self.log_error("self-extension request failed: %s", exc)
self._json(
{
"success": False,
"error": "internal_error",
"message": "自扩展服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _handle_calibration_post(self, path: str) -> None:
if not self._require_calibration_admin():
return
try:
payload = self._read_json_body(max_bytes=256 * 1024)
action_map = {
"/api/calibration/can/connect": lambda: self.calibration.connect_can(
dict(payload.get("can", payload))
),
"/api/calibration/start": lambda: self.calibration.calibrate(
None if payload.get("axis", "all") == "all" else int(payload["axis"])
),
"/api/calibration/move": lambda: self.calibration.move(
int(payload["axis"]), int(payload["permille"])
),
"/api/calibration/seek": lambda: self.calibration.seek(
int(payload["axis"]), bool(payload.get("far"))
),
"/api/calibration/stop": self.calibration.stop,
"/api/calibration/settings": lambda: self.calibration.update_settings(
dict(payload)
),
"/api/calibration/mqtt/connect": self.calibration.connect_mqtt,
"/api/calibration/mqtt/disconnect": self.calibration.disconnect_mqtt,
"/api/calibration/mqtt/test": self.calibration.mqtt_test,
"/api/calibration/video/start": self.calibration.start_video,
"/api/calibration/video/stop": self.calibration.stop_video,
}
action = action_map.get(path)
if action is None:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
requested_mode = str(
dict(payload.get("can", {})).get(
"mode", self.calibration.snapshot()["transport"]["mode"]
)
)
motion_paths = {
"/api/calibration/can/connect",
"/api/calibration/start",
"/api/calibration/move",
"/api/calibration/seek",
"/api/calibration/stop",
}
if path in motion_paths and requested_mode == "hardware":
if not self._require_local_decision_point():
return
self._json({"ok": True, "state": action()})
except CalibrationApiError as exc:
self._json(
{"ok": False, "error": "calibration_error", "message": str(exc)},
HTTPStatus(exc.status),
)
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
self._json(
{"ok": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep calibration APIs responsive
self.log_error("self-calibration request failed: %s", exc)
self._json(
{
"ok": False,
"error": "internal_error",
"message": "自标定服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _handle_self_repair_post(self, path: str) -> None:
if not self._require_self_repair_admin():
return
try:
payload = self._read_json_body(max_bytes=256 * 1024)
physical_paths = {
"/api/self-repair/hardware/start",
"/api/self-repair/hardware/stop",
"/api/self-repair/emergency-stop",
"/api/self-repair/firmware/build-flash",
"/api/self-repair/open-log",
}
if (
path in physical_paths
or (
path in {
"/api/self-repair/healing/start",
"/api/self-repair/healing/reset",
}
and str(payload.get("execution_mode", "shadow")) == "bench"
)
) and not self._require_local_decision_point():
return
if path == "/api/self-repair/healing/start":
self._json({"ok": True, "healing": self.repair.start_healing(payload)})
return
if path == "/api/self-repair/healing/run-all":
self._json({"ok": True, "batch": self.repair.start_batch(payload)})
return
if path == "/api/self-repair/healing/reset":
self._json({"ok": True, **self.repair.reset_healing(payload)})
return
if path == "/api/self-repair/hardware/start":
self._json({"ok": True, "session": self.repair.start_hardware()})
return
if path == "/api/self-repair/hardware/stop":
self._json({"ok": True, "session": self.repair.stop_hardware()})
return
if path == "/api/self-repair/hardware/self-test":
self._json(
{"ok": True, "self_test": self.repair.run_hardware_self_test()}
)
return
if path == "/api/self-repair/emergency-stop":
self._json({"ok": True, "request": self.repair.emergency_stop()})
return
if path == "/api/self-repair/snapshot":
self._json({"ok": True, "path": str(self.repair.save_snapshot())})
return
if path == "/api/self-repair/firmware/build-flash":
self._json(
{
"ok": True,
"firmware": self.repair.build_and_flash_firmware(payload),
}
)
return
if path == "/api/self-repair/open-log":
opened = self.repair.open_log_dir()
self._json({"ok": opened})
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
self._json(
{"ok": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep self-repair API responsive
self.log_error("self-repair request failed: %s", exc)
self._json(
{
"ok": False,
"error": "internal_error",
"message": "自修复服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _require_self_repair_admin(self, parsed_url: ParseResult | None = None) -> bool:
role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower()
if not role and parsed_url is not None:
role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower()
if hmac.compare_digest(role, "admin"):
return True
self._json(
{"error": "admin_required", "message": "自修复仅对 admin 开放。"},
HTTPStatus.FORBIDDEN,
)
return False
def _require_calibration_admin(self, parsed_url: ParseResult | None = None) -> bool:
role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower()
if not role and parsed_url is not None:
role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower()
if hmac.compare_digest(role, "admin"):
return True
self._json(
{
"error": "admin_required",
"message": "自标定仅对 admin 开放。",
},
HTTPStatus.FORBIDDEN,
)
return False
def _serve_calibration_events(self) -> None:
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_header("X-Accel-Buffering", "no")
self._send_security_headers()
self.end_headers()
version = -1
try:
while True:
state = self.calibration.wait_for_version(version, 12.0)
version = state["version"]
data = json.dumps(state, ensure_ascii=False, separators=(",", ":"))
self.wfile.write(f"data: {data}\n\n".encode("utf-8"))
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def _serve_calibration_video(self) -> None:
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=n7frame")
self.send_header("Cache-Control", "no-store")
self.send_header("Connection", "close")
self._send_security_headers()
self.end_headers()
sequence = -1
try:
while True:
sequence, frame = self.calibration.video.wait_frame(sequence)
if frame is None:
continue
header = (
b"--n7frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
+ str(len(frame)).encode("ascii")
+ b"\r\n\r\n"
)
self.wfile.write(header + frame + b"\r\n")
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def _handle_extension_gateway(self, action: str, payload: dict[str, object]) -> None:
try:
self._json(self.extension.execute_gateway(action, dict(payload)))
except GatewayError as exc:
self._json(
{"success": False, "error": "gscu_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
def _handle_hardware_access_post(self, path: str) -> None:
local_path = path.startswith("/api/local/")
if local_path and not self._require_local_decision_point():
return
try:
payload = self._read_json_body(max_bytes=4096)
client_id = str(payload.get("client_id", ""))
if path == "/api/hardware/access/request":
status = self.service.hardware_access.request_access(
client_id,
str(payload.get("label", "")),
self._client_key(),
)
self._json(
status,
HTTPStatus.OK if status["state"] == "approved" else HTTPStatus.ACCEPTED,
)
return
if path == "/api/hardware/access/release":
self._json(self.service.hardware_access.release(client_id))
return
if path == "/api/local/hardware-access/approve":
self._json(self.service.hardware_access.approve(client_id))
return
if path == "/api/local/hardware-access/deny":
self._json(self.service.hardware_access.deny(client_id))
return
if path == "/api/local/hardware-access/revoke":
self._json(
self.service.hardware_access.revoke(client_id or None)
)
return
except (ValueError, json.JSONDecodeError) as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
def _read_json_body(self, max_bytes: int = 32768) -> dict[str, object]:
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > max_bytes:
raise ValueError(f"request body must be between 1 and {max_bytes} bytes")
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("request body must be a JSON object")
return payload
def _authorize(self, path: str) -> bool:
if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS:
return True
if self._session_username() is not None:
return True
if path.startswith("/api/"):
self._json(
{
"error": "authentication_required",
"message": "请登录后访问。",
},
HTTPStatus.UNAUTHORIZED,
)
else:
requested = path if path.startswith("/") else "/"
self._redirect(f"/login?next={quote(requested, safe='/')}")
return False
def _login(self) -> None:
if not self.auth.config.enabled:
self._json({"enabled": False, "authenticated": True})
return
client_key = self._client_key()
retry_after = self.auth.login_failures.retry_after(client_key)
if retry_after:
self._json(
{
"error": "login_rate_limited",
"message": "登录尝试过多,请稍后再试。",
},
HTTPStatus.TOO_MANY_REQUESTS,
{"Retry-After": str(retry_after)},
)
return
try:
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > 4096:
raise ValueError("invalid login body")
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("invalid login body")
username = str(payload.get("username", ""))[:128]
password = str(payload.get("password", ""))[:1024]
except (ValueError, json.JSONDecodeError):
self._json(
{"error": "invalid_request", "message": "登录请求格式无效。"},
HTTPStatus.BAD_REQUEST,
)
return
password_valid = verify_password(password, self.auth.config.password_hash)
username_valid = hmac.compare_digest(
username.encode("utf-8"),
self.auth.config.username.encode("utf-8"),
)
if not (username_valid and password_valid):
self.auth.login_failures.record(client_key)
self._json(
{"error": "invalid_credentials", "message": "用户名或密码错误。"},
HTTPStatus.UNAUTHORIZED,
)
return
self.auth.login_failures.clear(client_key)
token = self.auth.sessions.issue(self.auth.config.username)
attributes = [
f"{self.auth.config.cookie_name}={token}",
"Path=/",
"HttpOnly",
"SameSite=Strict",
f"Max-Age={self.auth.config.session_ttl_seconds}",
]
if self._is_https():
attributes.append("Secure")
self._json(
{
"authenticated": True,
"username": self.auth.config.username,
},
headers={"Set-Cookie": "; ".join(attributes)},
)
def _logout(self) -> None:
attributes = [
f"{self.auth.config.cookie_name}=",
"Path=/",
"HttpOnly",
"SameSite=Strict",
"Max-Age=0",
]
if self._is_https():
attributes.append("Secure")
self._json(
{"authenticated": False},
headers={"Set-Cookie": "; ".join(attributes)},
)
def _session_username(self) -> str | None:
if not self.auth.config.enabled:
return None
cookie_header = self.headers.get("Cookie", "")
if not cookie_header or len(cookie_header) > 4096:
return None
try:
cookies = SimpleCookie()
cookies.load(cookie_header)
morsel = cookies.get(self.auth.config.cookie_name)
return self.auth.sessions.verify(morsel.value if morsel else "")
except (CookieError, ValueError):
return None
def _client_key(self) -> str:
address = self.client_address[0]
direct_address = ipaddress.ip_address(address)
if self.auth.config.trust_proxy_headers and direct_address.is_loopback:
forwarded = (
self.headers.get("CF-Connecting-IP")
or self.headers.get("X-Forwarded-For", "").split(",", 1)[0]
).strip()
try:
address = str(ipaddress.ip_address(forwarded))
except ValueError:
pass
return address
def _is_unproxied_loopback_request(self) -> bool:
"""Distinguish the local decision console from traffic via the tunnel."""
try:
direct_address = ipaddress.ip_address(self.client_address[0])
except ValueError:
return False
proxy_headers = (
"Forwarded",
"X-Forwarded-For",
"X-Real-IP",
"CF-Connecting-IP",
)
return direct_address.is_loopback and not any(
self.headers.get(name, "").strip() for name in proxy_headers
)
def _require_local_decision_point(self) -> bool:
if self._is_unproxied_loopback_request():
return True
self._json(
{
"error": "local_access_required",
"message": "真实台架审批与停止权限仅在台架本机开放。",
},
HTTPStatus.FORBIDDEN,
)
return False
def _is_https(self) -> bool:
if not self.auth.config.trust_proxy_headers:
return False
if self.headers.get("X-Forwarded-Proto", "").lower() == "https":
return True
return '"scheme":"https"' in self.headers.get("CF-Visitor", "").replace(" ", "")
def _serve_request(self, request_id: str) -> None:
if not REQUEST_ID_PATTERN.fullmatch(request_id):
self._json({"error": "invalid_request_id"}, HTTPStatus.BAD_REQUEST)
return
result_path = self.service.artifact_root / request_id / "result.json"
if not result_path.is_file():
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
self._json(json.loads(result_path.read_text(encoding="utf-8")))
def _serve_static(self, path: str) -> None:
relative_path = "index.html" if path in {"", "/"} else path.lstrip("/")
self._serve_static_file(relative_path)
def _serve_static_file(self, relative_path: str) -> None:
candidate = (STATIC_ROOT / relative_path).resolve()
try:
candidate.relative_to(STATIC_ROOT.resolve())
except ValueError:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if not candidate.is_file():
candidate = STATIC_ROOT / "index.html"
content_type, _ = mimetypes.guess_type(candidate.name)
content = candidate.read_bytes()
self.send_response(HTTPStatus.OK)
resolved_content_type = content_type or "application/octet-stream"
if resolved_content_type.startswith("text/") or resolved_content_type in {
"application/javascript",
"application/json",
"application/manifest+json",
"image/svg+xml",
}:
resolved_content_type = f"{resolved_content_type}; charset=utf-8"
self.send_header(
"Content-Type",
resolved_content_type,
)
self.send_header("Content-Length", str(len(content)))
cache_control = "no-cache"
if relative_path.startswith(("vendor/", "assets/", "icons/")):
cache_control = "public, max-age=31536000, immutable"
elif candidate.name in {"index.html", "login.html", "service-worker.js"}:
cache_control = "no-cache"
self.send_header("Cache-Control", cache_control)
if candidate.name == "service-worker.js":
self.send_header("Service-Worker-Allowed", "/")
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _serve_gesture_model(self) -> None:
path = self.gesture.model_path
if not path.is_file():
self._json(
{"error": "gesture_model_unavailable", "message": "手势模型文件不可用。"},
HTTPStatus.NOT_FOUND,
)
return
content = path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "private, max-age=3600")
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _json(
self,
payload: object,
status: HTTPStatus = HTTPStatus.OK,
headers: dict[str, str] | None = None,
) -> None:
content = json.dumps(payload, 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(content)))
self.send_header("Cache-Control", "no-store")
for name, value in (headers or {}).items():
self.send_header(name, value)
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _redirect(self, location: str) -> None:
self.send_response(HTTPStatus.SEE_OTHER)
self.send_header("Location", location)
self.send_header("Cache-Control", "no-store")
self._send_security_headers()
self.end_headers()
def _send_security_headers(self) -> None:
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Permissions-Policy",
"camera=(self), microphone=(), geolocation=(), payment=(), usb=()",
)
self.send_header(
"Content-Security-Policy",
"default-src 'self'; "
"script-src 'self' 'wasm-unsafe-eval'; "
"style-src 'self'; "
"img-src 'self' data: blob:; "
"connect-src 'self' blob:; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
)
if self._is_https():
self.send_header(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
)
def main() -> None:
parser = argparse.ArgumentParser(description="Run the cabin orchestration demo")
parser.add_argument("--host", default=os.getenv("HOST", "127.0.0.1"))
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")))
args = parser.parse_args()
server = ThreadingHTTPServer((args.host, args.port), SeatDemoHandler)
SeatDemoHandler.extension.start()
SeatDemoHandler.calibration.start()
SeatDemoHandler.repair.start()
if args.host in {"0.0.0.0", "::"}:
lan_address = _lan_ipv4_address()
print(f"Cabin orchestration demo (local): http://127.0.0.1:{args.port}", flush=True)
if lan_address:
print(
f"Cabin orchestration demo (LAN): http://{lan_address}:{args.port}",
flush=True,
)
print(
"LAN mode: devices on reachable networks can use the GPT-backed API.",
flush=True,
)
else:
print(f"Cabin orchestration demo: http://{args.host}:{args.port}", flush=True)
hardware = SeatDemoHandler.service.hardware_settings
print(
"Execution modes: dry_run + simulated_hardware "
f"({'enabled' if SeatDemoHandler.service.simulated_hardware_enabled else 'locked'}) "
"+ hardware "
f"({'configured' if hardware.configured else 'locked'})",
flush=True,
)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
SeatDemoHandler.extension.close()
SeatDemoHandler.calibration.close()
SeatDemoHandler.repair.close()
if __name__ == "__main__":
main()