Files

101 lines
5.0 KiB
Python

"""What each launch surface of the one web-server stack may do.
The host serving the app records its surface once on ``app.state.ui_surface``; unset, it reads
as ``gateway`` on the gateway's own listener for native clients (the app has a
``gateway_runner``, as the lifespan choice reads) and ``dashboard`` elsewhere (the standalone
``hermes dashboard`` and ``hermes serve``). Every surface-dependent decision reads this table
instead of re-deriving it, and :func:`route_on` keeps a route off the surfaces that do not use
it. ``auth_required`` is decided at runtime by the auth gate, so the private-launch predicate is
a function over state, not a table field.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from fastapi import HTTPException, Request
@dataclass(frozen=True)
class SurfacePolicy:
#: Mounts a browser UI; ``serve`` is JSON-RPC/WS only.
serves_spa: bool
#: Public HTML may carry the session token. Where it may not, every HTML entry
#: (``/index.html`` included) gets the token-free bootstrap instead of the raw file.
html_token: bool
#: ``/api/host-terminal`` may open host shells (still gated per request).
host_terminal: bool
#: An unauthenticated start hands the operator a private launch link.
launch_link: bool
#: ``/api/webapp/socket-ticket`` mints the browser's one-use tickets for ``/api/ws`` and the voice
#: streams on this same origin (accepted by the loopback and gated upgrade gates alike).
gateway_ticket: bool
#: Behind the sign-in gate, a write that carries the session cookie (``/auth/logout`` included)
#: must come from this listener's own origin. Elsewhere the gate keeps main's rule.
own_origin_cookie_writes: bool
#: ``/api/webapp-access/*`` serves: native Desktop's Web Access pane calls the gateway's own
#: listener (the private one, or the Webapp's when it is on).
web_access: bool
#: The browser's file routes serve: ``/api/chat/file-upload`` stages a picked file,
#: ``/api/files/ticket`` mints the path-bound tickets for downloads and media links, and chat
#: upload bodies are capped before they are parsed (``web_upload_limit``).
browser_files: bool
#: ``/api/desktop-plugins/*`` serves this host's desktop plugins to the browser's plugin loader;
#: native Desktop reads its own disk.
desktop_plugins: bool
#: ``/api/ui-preferences`` serves: the browser's door to this machine's UI preferences store
#: (native Desktop reaches the same file through its preload capability, not over HTTP).
ui_preferences: bool
#: ``/api/audio/voice-config`` may hand the client provider keys to call STT/TTS itself. A
#: browser tab cannot: a provider without CORS (the Nous tool gateway) refuses its fetch, and the
#: keys would sit in the page. It relays through the host's /api/audio/* endpoints instead.
client_direct_voice: bool
POLICIES: dict[str, SurfacePolicy] = {
"serve": SurfacePolicy(
serves_spa=False, html_token=True, host_terminal=False, launch_link=False, gateway_ticket=False,
own_origin_cookie_writes=False, web_access=False, browser_files=False,
desktop_plugins=False, ui_preferences=False, client_direct_voice=True),
"dashboard": SurfacePolicy(
serves_spa=True, html_token=True, host_terminal=False, launch_link=False, gateway_ticket=False,
own_origin_cookie_writes=False, web_access=False, browser_files=False,
desktop_plugins=False, ui_preferences=False, client_direct_voice=True),
"gateway": SurfacePolicy(
serves_spa=True, html_token=True, host_terminal=False, launch_link=False, gateway_ticket=False,
own_origin_cookie_writes=False, web_access=True, browser_files=False,
desktop_plugins=False, ui_preferences=False, client_direct_voice=True),
"webapp": SurfacePolicy(
serves_spa=True, html_token=False, host_terminal=True, launch_link=True, gateway_ticket=True,
own_origin_cookie_writes=True, web_access=True, browser_files=True,
desktop_plugins=True, ui_preferences=True, client_direct_voice=False),
}
def surface(state: Any) -> str:
"""The surface *state*'s app serves (see the module docstring)."""
recorded = getattr(state, "ui_surface", None)
if recorded:
return recorded
return "gateway" if getattr(state, "gateway_runner", None) is not None else "dashboard"
def policy(state: Any) -> SurfacePolicy:
return POLICIES[surface(state)]
def route_on(field: str) -> Callable[[Request], None]:
"""A route dependency answering 404 where the serving surface's policy leaves *field* off, so
the route does not exist there for any caller."""
def surface_serves(request: Request) -> None:
if not getattr(policy(request.app.state), field):
raise HTTPException(status_code=404, detail="Not Found")
return surface_serves
def private_launch(state: Any) -> bool:
"""Webapp on an unauthenticated bind: the launch link is the only way in."""
return policy(state).launch_link and not getattr(state, "auth_required", False)