2026-08-23 04:42:08 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Exercise branded PWA responses from a running upstream WebUI server."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
2026-08-23 22:13:52 -03:00
|
|
|
import re
|
2026-08-23 04:42:08 +00:00
|
|
|
import sys
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
|
from typing import Any
|
|
|
|
|
from urllib.parse import urljoin
|
|
|
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
|
|
|
|
|
PERSONA_LINKS = (
|
|
|
|
|
"static/hermes-agent.ico",
|
|
|
|
|
"static/hermes-agent-192.png",
|
|
|
|
|
"static/hermes-agent-512.png",
|
|
|
|
|
)
|
2026-08-24 03:08:56 -03:00
|
|
|
HUX_LINKS = (
|
|
|
|
|
"static/hux/foundation.js",
|
|
|
|
|
"static/hux/runtime/wave_a_contract.js",
|
|
|
|
|
"static/hux/runtime/wave_b_runtime.js",
|
|
|
|
|
"static/hux/runtime/wave_c_multimodal_onboarding_release.js",
|
|
|
|
|
"static/hux/bootstrap.css",
|
|
|
|
|
"static/hux/bootstrap.js",
|
|
|
|
|
)
|
2026-08-23 04:42:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get(
|
|
|
|
|
url: str,
|
|
|
|
|
opener: Callable[..., Any],
|
|
|
|
|
) -> tuple[bytes, str]:
|
|
|
|
|
request = Request(url, headers={"Accept-Encoding": "identity"})
|
|
|
|
|
with opener(request, timeout=5) as response:
|
|
|
|
|
status = int(getattr(response, "status", 0))
|
|
|
|
|
if status != 200:
|
|
|
|
|
raise RuntimeError(f"GET {url} returned HTTP {status}")
|
|
|
|
|
body = response.read()
|
|
|
|
|
final_url = response.geturl()
|
|
|
|
|
return body, final_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _icon_sources(value: object) -> list[str]:
|
|
|
|
|
sources: list[str] = []
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
icons = value.get("icons")
|
|
|
|
|
if isinstance(icons, list):
|
|
|
|
|
for icon in icons:
|
|
|
|
|
if not isinstance(icon, dict) or not isinstance(icon.get("src"), str):
|
|
|
|
|
raise RuntimeError("manifest contains an invalid icon entry")
|
|
|
|
|
if icon.get("type") != "image/png":
|
|
|
|
|
raise RuntimeError("manifest contains a non-PNG icon")
|
|
|
|
|
sources.append(icon["src"])
|
|
|
|
|
for child in value.values():
|
|
|
|
|
if child is not icons:
|
|
|
|
|
sources.extend(_icon_sources(child))
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
|
for child in value:
|
|
|
|
|
sources.extend(_icon_sources(child))
|
|
|
|
|
return sources
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def smoke(
|
|
|
|
|
base_url: str,
|
|
|
|
|
*,
|
|
|
|
|
opener: Callable[..., Any] = urlopen,
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
base_url = base_url.rstrip("/") + "/"
|
|
|
|
|
manifest_body, manifest_url = _get(urljoin(base_url, "manifest.json"), opener)
|
|
|
|
|
try:
|
|
|
|
|
manifest = json.loads(manifest_body)
|
|
|
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
|
|
|
raise RuntimeError("served /manifest.json is not valid branded JSON") from exc
|
|
|
|
|
|
|
|
|
|
if manifest.get("name") != "Hermes Chat" or manifest.get("short_name") != "Hermes":
|
|
|
|
|
raise RuntimeError("served /manifest.json is not branded for Hermes Chat")
|
|
|
|
|
expected = {
|
|
|
|
|
"static/hermes-agent-192.png",
|
|
|
|
|
"static/hermes-agent-512.png",
|
|
|
|
|
}
|
|
|
|
|
top_level = manifest.get("icons")
|
|
|
|
|
if not isinstance(top_level, list) or {
|
|
|
|
|
icon.get("src") for icon in top_level if isinstance(icon, dict)
|
|
|
|
|
} != expected:
|
|
|
|
|
raise RuntimeError("served /manifest.json has the wrong Hermes icons")
|
|
|
|
|
|
|
|
|
|
icon_sources = _icon_sources(manifest)
|
|
|
|
|
if not icon_sources:
|
|
|
|
|
raise RuntimeError("served /manifest.json has no icon URLs")
|
|
|
|
|
for source in icon_sources:
|
|
|
|
|
icon_body, _icon_url = _get(urljoin(manifest_url, source), opener)
|
|
|
|
|
if not icon_body.startswith(PNG_MAGIC):
|
|
|
|
|
raise RuntimeError(f"manifest icon is not PNG: {source}")
|
|
|
|
|
|
|
|
|
|
root_body, _root_url = _get(base_url, opener)
|
|
|
|
|
try:
|
|
|
|
|
root_html = root_body.decode("utf-8")
|
|
|
|
|
except UnicodeDecodeError as exc:
|
|
|
|
|
raise RuntimeError("served / is not UTF-8 HTML") from exc
|
|
|
|
|
for source in PERSONA_LINKS:
|
|
|
|
|
if source not in root_html:
|
|
|
|
|
raise RuntimeError(f"served / omitted Hermes persona link: {source}")
|
|
|
|
|
if "static/hermes-brand.css" not in root_html:
|
|
|
|
|
raise RuntimeError("served / omitted static/hermes-brand.css")
|
2026-08-24 03:08:56 -03:00
|
|
|
for source in HUX_LINKS:
|
|
|
|
|
if source not in root_html:
|
|
|
|
|
raise RuntimeError(f"served / omitted HUX browser asset: {source}")
|
|
|
|
|
asset_body, _asset_url = _get(urljoin(base_url, source), opener)
|
|
|
|
|
if not asset_body or b"<html" in asset_body[:200].lower():
|
|
|
|
|
raise RuntimeError(f"served HUX browser asset is invalid: {source}")
|
2026-08-23 22:13:52 -03:00
|
|
|
version_match = re.search(
|
|
|
|
|
r"window\.__HERMES_WEBUI_BUNDLE_VERSION__='([^']+)'",
|
|
|
|
|
root_html,
|
|
|
|
|
)
|
|
|
|
|
if not version_match:
|
|
|
|
|
raise RuntimeError("served / omitted the WebUI bundle identity")
|
|
|
|
|
client_version = version_match.group(1)
|
|
|
|
|
|
|
|
|
|
settings_body, _settings_url = _get(
|
|
|
|
|
urljoin(base_url, "api/settings"), opener
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
settings = json.loads(settings_body)
|
|
|
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
|
|
|
raise RuntimeError("served /api/settings is not valid JSON") from exc
|
|
|
|
|
server_version = settings.get("webui_bundle_version")
|
|
|
|
|
if not isinstance(server_version, str) or server_version != client_version:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"served WebUI client/server release identities do not match"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
worker_body, _worker_url = _get(urljoin(base_url, "sw.js"), opener)
|
|
|
|
|
try:
|
|
|
|
|
worker = worker_body.decode("utf-8")
|
|
|
|
|
except UnicodeDecodeError as exc:
|
|
|
|
|
raise RuntimeError("served /sw.js is not UTF-8 JavaScript") from exc
|
|
|
|
|
if f"hermes-shell-{client_version}" not in worker:
|
|
|
|
|
raise RuntimeError("served /sw.js uses a different release identity")
|
2026-08-23 04:42:08 +00:00
|
|
|
|
|
|
|
|
direct_icon, _direct_url = _get(
|
|
|
|
|
urljoin(base_url, "static/hermes-agent-192.png"), opener
|
|
|
|
|
)
|
|
|
|
|
if not direct_icon.startswith(PNG_MAGIC):
|
|
|
|
|
raise RuntimeError("served /static/hermes-agent-192.png is not PNG")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"manifest": manifest_url,
|
|
|
|
|
"icon_requests": len(icon_sources),
|
|
|
|
|
"root": base_url,
|
|
|
|
|
"direct_icon": "static/hermes-agent-192.png",
|
2026-08-24 03:08:56 -03:00
|
|
|
"hux_assets": len(HUX_LINKS),
|
2026-08-23 22:13:52 -03:00
|
|
|
"release_identity": client_version,
|
2026-08-23 04:42:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
if len(sys.argv) != 2:
|
|
|
|
|
raise SystemExit("usage: hermes-webui-smoke.py BASE_URL")
|
|
|
|
|
result = smoke(sys.argv[1])
|
|
|
|
|
print(
|
|
|
|
|
"Hermes WebUI smoke passed: "
|
|
|
|
|
f"manifest={result['manifest']} "
|
|
|
|
|
f"icon_requests={result['icon_requests']} "
|
2026-08-24 03:08:56 -03:00
|
|
|
f"hux_assets={result['hux_assets']} "
|
2026-08-23 22:13:52 -03:00
|
|
|
f"release_identity={result['release_identity']} "
|
2026-08-23 04:42:08 +00:00
|
|
|
"root=branded direct_icon=png"
|
|
|
|
|
)
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|