feat: add seat firmware workbench and OTA delivery
This commit is contained in:
@@ -43,6 +43,10 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402
|
||||
CodexAssistantService,
|
||||
CodexVehicleToolError,
|
||||
)
|
||||
from cognitive_agent.application.firmware_workbench import ( # noqa: E402
|
||||
FirmwareWorkbenchError,
|
||||
FirmwareWorkbenchService,
|
||||
)
|
||||
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
|
||||
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
normalize_mode_profile_name,
|
||||
@@ -103,7 +107,11 @@ PUBLIC_AUTH_PATHS = {
|
||||
"/network.js",
|
||||
"/api/auth/status",
|
||||
"/api/auth/login",
|
||||
"/api/firmware-workbench/releases/latest",
|
||||
}
|
||||
PUBLIC_OTA_DOWNLOAD_PATH = re.compile(
|
||||
r"/api/firmware-workbench/releases/[a-z0-9-]+/download"
|
||||
)
|
||||
NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"})
|
||||
|
||||
|
||||
@@ -207,6 +215,7 @@ def _lan_ipv4_address() -> str | None:
|
||||
|
||||
class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
service = SeatDemoService()
|
||||
firmware_workbench = FirmwareWorkbenchService(REPOSITORY_ROOT)
|
||||
# Codex defaults to its own empty temporary workspace. A deployment may
|
||||
# opt into another isolated directory through CODEX_ASSISTANT_WORKSPACE,
|
||||
# but it must never inherit access to this application's source tree.
|
||||
@@ -317,6 +326,63 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
if path == "/api/assistant/capabilities":
|
||||
self._json(self._assistant_capability_catalog().snapshot())
|
||||
return
|
||||
if path == "/api/firmware-workbench/state":
|
||||
self._json(self.firmware_workbench.snapshot())
|
||||
return
|
||||
if path == "/api/firmware-workbench/releases/latest":
|
||||
try:
|
||||
self._json(self.firmware_workbench.latest_release())
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json(
|
||||
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
|
||||
)
|
||||
return
|
||||
firmware_task_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/tasks/(?P<task_id>[a-z0-9-]+)", path
|
||||
)
|
||||
if firmware_task_match:
|
||||
try:
|
||||
self._json(
|
||||
self.firmware_workbench.task(firmware_task_match.group("task_id"))
|
||||
)
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json(
|
||||
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
|
||||
)
|
||||
return
|
||||
firmware_artifact_download_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)/download",
|
||||
path,
|
||||
)
|
||||
if firmware_artifact_download_match:
|
||||
self._serve_firmware_download(
|
||||
"artifact", firmware_artifact_download_match.group("artifact_id")
|
||||
)
|
||||
return
|
||||
firmware_release_download_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/releases/(?P<release_id>[a-z0-9-]+)/download",
|
||||
path,
|
||||
)
|
||||
if firmware_release_download_match:
|
||||
self._serve_firmware_download(
|
||||
"release", firmware_release_download_match.group("release_id")
|
||||
)
|
||||
return
|
||||
firmware_artifact_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)", path
|
||||
)
|
||||
if firmware_artifact_match:
|
||||
try:
|
||||
self._json(
|
||||
self.firmware_workbench.artifact(
|
||||
firmware_artifact_match.group("artifact_id")
|
||||
)
|
||||
)
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json(
|
||||
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
|
||||
)
|
||||
return
|
||||
if path == "/api/runtime":
|
||||
self._json(self.service.runtime_info())
|
||||
return
|
||||
@@ -510,6 +576,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
if not self._require_json_content_type():
|
||||
return
|
||||
if path == "/api/firmware-workbench/tasks" or path.startswith(
|
||||
"/api/firmware-workbench/"
|
||||
):
|
||||
self._handle_firmware_workbench_post(path)
|
||||
return
|
||||
if path in {
|
||||
"/api/hardware/access/request",
|
||||
"/api/hardware/access/release",
|
||||
@@ -2529,6 +2600,24 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
return
|
||||
firmware_artifact_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)",
|
||||
path,
|
||||
)
|
||||
if firmware_artifact_match is not None:
|
||||
try:
|
||||
self._json(
|
||||
self.firmware_workbench.delete_artifact(
|
||||
firmware_artifact_match.group("artifact_id")
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json(
|
||||
{"error": exc.code, "message": str(exc)},
|
||||
HTTPStatus(exc.status),
|
||||
)
|
||||
return
|
||||
prefix = "/api/mode-profiles/"
|
||||
if not path.startswith(prefix):
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
@@ -3259,6 +3348,70 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
raise ValueError("request body must be a JSON object")
|
||||
return payload
|
||||
|
||||
def _handle_firmware_workbench_post(self, path: str) -> None:
|
||||
try:
|
||||
payload = self._read_json_body(max_bytes=16 * 1024)
|
||||
if path == "/api/firmware-workbench/tasks":
|
||||
result = self.firmware_workbench.start_task(
|
||||
str(payload.get("instruction", "")),
|
||||
str(payload.get("baseline_id", "time_only")),
|
||||
)
|
||||
self._json(result, HTTPStatus.ACCEPTED)
|
||||
return
|
||||
task_action_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/tasks/(?P<task_id>[a-z0-9-]+)/(?P<action>cancel|retry)",
|
||||
path,
|
||||
)
|
||||
if task_action_match:
|
||||
task_id = task_action_match.group("task_id")
|
||||
if task_action_match.group("action") == "cancel":
|
||||
self._json(self.firmware_workbench.cancel_task(task_id))
|
||||
else:
|
||||
self._json(
|
||||
self.firmware_workbench.retry_task(task_id), HTTPStatus.ACCEPTED
|
||||
)
|
||||
return
|
||||
artifact_publish_match = re.fullmatch(
|
||||
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)/publish",
|
||||
path,
|
||||
)
|
||||
if artifact_publish_match:
|
||||
self._json(
|
||||
self.firmware_workbench.publish_artifact(
|
||||
artifact_publish_match.group("artifact_id")
|
||||
)
|
||||
)
|
||||
return
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self._json(
|
||||
{"error": "invalid_request", "message": str(exc)},
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
|
||||
|
||||
def _serve_firmware_download(self, kind: str, item_id: str) -> None:
|
||||
try:
|
||||
if kind == "release":
|
||||
path, filename = self.firmware_workbench.resolve_release_download(item_id)
|
||||
else:
|
||||
path, filename = self.firmware_workbench.resolve_artifact_download(item_id)
|
||||
content = path.read_bytes()
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/zip")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.send_header("Cache-Control", "private, no-store")
|
||||
self.send_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename*=UTF-8''{quote(filename, safe='.-_')}",
|
||||
)
|
||||
self._send_security_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
except FirmwareWorkbenchError as exc:
|
||||
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
|
||||
|
||||
def _require_json_content_type(self) -> bool:
|
||||
if is_json_content_type(self.headers.get("Content-Type", "")):
|
||||
return True
|
||||
@@ -3505,7 +3658,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
pass
|
||||
|
||||
def _authorize(self, path: str) -> bool:
|
||||
if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS:
|
||||
if (
|
||||
not self.auth.config.enabled
|
||||
or path in PUBLIC_AUTH_PATHS
|
||||
or PUBLIC_OTA_DOWNLOAD_PATH.fullmatch(path)
|
||||
):
|
||||
return True
|
||||
if self._session_username() is not None:
|
||||
return True
|
||||
@@ -3780,8 +3937,13 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def _send_security_headers(self) -> None:
|
||||
allow_same_origin_frame = (
|
||||
unquote(urlparse(self.path).path) == "/firmware-workbench.html"
|
||||
)
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.send_header("X-Frame-Options", "DENY")
|
||||
self.send_header(
|
||||
"X-Frame-Options", "SAMEORIGIN" if allow_same_origin_frame else "DENY"
|
||||
)
|
||||
self.send_header("Referrer-Policy", "no-referrer")
|
||||
self.send_header(
|
||||
"Permissions-Policy",
|
||||
@@ -3796,7 +3958,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
"media-src 'self' blob:; "
|
||||
"frame-src 'self' http://*:3001; "
|
||||
"connect-src 'self' blob: http://172.20.10.6:8889; "
|
||||
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
||||
"object-src 'none'; base-uri 'none'; frame-ancestors "
|
||||
+ ("'self'" if allow_same_origin_frame else "'none'"),
|
||||
)
|
||||
if self._is_https():
|
||||
self.send_header(
|
||||
@@ -3893,6 +4056,7 @@ def main() -> None:
|
||||
SeatDemoHandler.nfc.close()
|
||||
SeatDemoHandler.calibration.close()
|
||||
SeatDemoHandler.repair.close()
|
||||
SeatDemoHandler.firmware_workbench.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user