feat: add local hardware approval leases

This commit is contained in:
vee
2026-08-03 10:20:55 +08:00
parent 85b4740d29
commit a952f37ceb
14 changed files with 1089 additions and 25 deletions
+145 -6
View File
@@ -32,6 +32,9 @@ 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
@@ -96,6 +99,35 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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
@@ -106,13 +138,25 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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_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(
{
@@ -159,6 +203,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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 != "/api/requests":
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
@@ -178,12 +231,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return
if self.auth.config.enabled:
self.auth.api_requests.record(username)
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > 32768:
raise ValueError("request body must be between 1 and 32768 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")
payload = self._read_json_body()
result = self.service.process(
str(payload.get("utterance", "")),
str(payload.get("provider", "auto")),
@@ -200,8 +248,23 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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(
{
@@ -238,6 +301,53 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
HTTPStatus.INTERNAL_SERVER_ERROR,
)
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
@@ -363,6 +473,35 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
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