fix: verify served Hermes PWA identity
This commit is contained in:
parent
99a9de936b
commit
083005e44f
@ -21,8 +21,10 @@ COPY dockerfiles/hermes-webui-atlas-voice.css /opt/hermes-webui/static/atlas-voi
|
||||
COPY dockerfiles/hermes-webui-router-patch.py /tmp/hermes-webui-router-patch.py
|
||||
COPY dockerfiles/hermes-webui-router.js /opt/hermes-webui/static/atlas-router.js
|
||||
COPY dockerfiles/hermes-webui-brand-patch.py /tmp/hermes-webui-brand-patch.py
|
||||
COPY dockerfiles/hermes-webui-manifest-patch.py /tmp/hermes-webui-manifest-patch.py
|
||||
COPY dockerfiles/hermes-webui-smoke.py /tmp/hermes-webui-smoke.py
|
||||
COPY dockerfiles/hermes-webui-brand.css /opt/hermes-webui/static/hermes-brand.css
|
||||
COPY dockerfiles/hermes-webui-manifest.json /opt/hermes-webui/static/manifest.json
|
||||
COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json
|
||||
COPY dockerfiles/hermes-webui-assets/hermes-agent.ico /opt/hermes-webui/static/hermes-agent.ico
|
||||
COPY dockerfiles/hermes-webui-assets/hermes-agent-192.png /opt/hermes-webui/static/hermes-agent-192.png
|
||||
COPY dockerfiles/hermes-webui-assets/hermes-agent-512.png /opt/hermes-webui/static/hermes-agent-512.png
|
||||
@ -32,6 +34,7 @@ RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-stt-patch.py
|
||||
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-telegram-project-patch.py
|
||||
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-router-patch.py
|
||||
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-brand-patch.py
|
||||
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-manifest-patch.py
|
||||
|
||||
RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
||||
&& grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \
|
||||
@ -81,7 +84,10 @@ RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
||||
/opt/hermes-webui/api/gateway_chat.py \
|
||||
/opt/hermes/tools/transcription_tools.py
|
||||
|
||||
# Exercise the real server process in the target architecture before publish.
|
||||
# Exercise branded responses from the real upstream server process in the
|
||||
# target architecture before publish. The manifest checks resolve icon paths
|
||||
# from the URL the server actually returns; no filesystem-only assertion can
|
||||
# satisfy this gate.
|
||||
RUN set -eu; \
|
||||
mkdir -p /tmp/hermes-webui-smoke/home /tmp/hermes-webui-smoke/state /tmp/hermes-webui-smoke/workspace; \
|
||||
HERMES_HOME=/tmp/hermes-webui-smoke/home \
|
||||
@ -93,14 +99,24 @@ RUN set -eu; \
|
||||
HERMES_WEBUI_SKIP_ONBOARDING=1 \
|
||||
/opt/hermes/.venv/bin/python /opt/hermes-webui/server.py >/tmp/hermes-webui-smoke.log 2>&1 & \
|
||||
server_pid=$!; \
|
||||
cleanup() { kill "${server_pid}" 2>/dev/null || true; wait "${server_pid}" 2>/dev/null || true; }; \
|
||||
trap cleanup EXIT HUP INT TERM; \
|
||||
ready=0; \
|
||||
for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do \
|
||||
if /opt/hermes/.venv/bin/python -c 'from urllib.request import urlopen; urlopen("http://127.0.0.1:18787/health", timeout=2).read()' >/dev/null 2>&1; then ready=1; break; fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
kill "${server_pid}" 2>/dev/null || true; \
|
||||
wait "${server_pid}" 2>/dev/null || true; \
|
||||
if [ "${ready}" != "1" ]; then cat /tmp/hermes-webui-smoke.log; exit 1; fi; \
|
||||
smoke_passed=0; \
|
||||
if [ "${ready}" = "1" ] \
|
||||
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-smoke.py http://127.0.0.1:18787/; then \
|
||||
smoke_passed=1; \
|
||||
fi; \
|
||||
cleanup; \
|
||||
trap - EXIT HUP INT TERM; \
|
||||
if [ "${ready}" != "1" ] || [ "${smoke_passed}" != "1" ]; then \
|
||||
cat /tmp/hermes-webui-smoke.log; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
rm -rf /tmp/hermes-webui-smoke /tmp/hermes-webui-smoke.log
|
||||
|
||||
ENV HERMES_WEBUI_AGENT_DIR=/opt/hermes \
|
||||
|
||||
54
dockerfiles/hermes-webui-manifest-patch.py
Normal file
54
dockerfiles/hermes-webui-manifest-patch.py
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install the branded PWA manifest only over the exact pinned upstream file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(os.environ.get("HERMES_WEBUI_PATCH_ROOT", "/opt/hermes-webui"))
|
||||
SOURCE = Path(
|
||||
os.environ.get(
|
||||
"HERMES_WEBUI_MANIFEST_SOURCE", "/tmp/hermes-webui-manifest.json"
|
||||
)
|
||||
)
|
||||
UPSTREAM_SHA256 = "da3e24d84ae91fba3f8ba51f48d51d277b4f6d443506e888a178ac7d1fed1c6a"
|
||||
|
||||
|
||||
manifest_path = ROOT / "static/manifest.json"
|
||||
upstream = manifest_path.read_bytes()
|
||||
if hashlib.sha256(upstream).hexdigest() != UPSTREAM_SHA256:
|
||||
raise SystemExit(
|
||||
"Hermes manifest patch context changed in "
|
||||
f"{manifest_path}: pinned upstream SHA-256 mismatch"
|
||||
)
|
||||
|
||||
branded = SOURCE.read_bytes()
|
||||
try:
|
||||
payload = json.loads(branded)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SystemExit("Tracked Hermes manifest is not valid UTF-8 JSON") from exc
|
||||
|
||||
expected_icons = {
|
||||
("static/hermes-agent-192.png", "192x192", "image/png"),
|
||||
("static/hermes-agent-512.png", "512x512", "image/png"),
|
||||
}
|
||||
actual_icons = {
|
||||
(icon.get("src"), icon.get("sizes"), icon.get("type"))
|
||||
for icon in payload.get("icons", [])
|
||||
if isinstance(icon, dict)
|
||||
}
|
||||
if (
|
||||
payload.get("name") != "Hermes Chat"
|
||||
or payload.get("short_name") != "Hermes"
|
||||
or actual_icons != expected_icons
|
||||
):
|
||||
raise SystemExit("Tracked Hermes manifest identity contract changed")
|
||||
|
||||
# Write once, and only after both the installed upstream context and replacement
|
||||
# contract have passed. This prevents an upstream pin drift from being hidden by
|
||||
# a wholesale COPY over the served manifest.
|
||||
manifest_path.write_bytes(branded)
|
||||
127
dockerfiles/hermes-webui-smoke.py
Normal file
127
dockerfiles/hermes-webui-smoke.py
Normal file
@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise branded PWA responses from a running upstream WebUI server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
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']} "
|
||||
"root=branded direct_icon=png"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -30,6 +30,16 @@ The release fails closed when the requested revision is not the checked-out
|
||||
tag policy is absent, Kaniko and Harbor disagree on the digest, either Flux
|
||||
workload changes identity/image shape, or the evidence archive is incomplete.
|
||||
|
||||
Harbor policy bootstrap has an intentional ordering dependency. The existing
|
||||
`harbor-hermes-agent-immutability-ensure-1` Job grants the shared Jenkins
|
||||
publisher only the read-only `immutable-tag:list` permission; the WebUI policy
|
||||
Job creates and verifies the separate `hermes-webui` rule but does not edit the
|
||||
publisher robot. Flux must therefore complete the existing Hermes agent policy
|
||||
bootstrap before the WebUI Job and Jenkins release verification. This successor
|
||||
does not include a WebUI image digest: until its WebUI publisher job and policy
|
||||
are merged to `main` and bootstrapped, no image can be legitimately published
|
||||
and independently verified through this lane.
|
||||
|
||||
## PWA identity source
|
||||
|
||||
The installed application uses the tracked canonical persona at
|
||||
|
||||
53
testing/fixtures/hermes-webui-0.52.181/static/manifest.json
Normal file
53
testing/fixtures/hermes-webui-0.52.181/static/manifest.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "./",
|
||||
"name": "Hermes",
|
||||
"short_name": "Hermes",
|
||||
"description": "Hermes AI Agent Web UI",
|
||||
"start_url": "./?source=pwa",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
|
||||
"background_color": "#0D0D1A",
|
||||
"theme_color": "#0D0D1A",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "New conversation",
|
||||
"short_name": "New chat",
|
||||
"description": "Open Hermes ready for a new chat",
|
||||
"url": "./?source=pwa&action=new-chat",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/favicon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "static/favicon-32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "static/favicon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "static/favicon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@ -10,6 +11,9 @@ import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOCKERFILES = ROOT / "dockerfiles"
|
||||
@ -17,6 +21,8 @@ FIXTURE = ROOT / "testing/fixtures/hermes-webui-0.52.181"
|
||||
AGENT_FIXTURE = ROOT / "testing/fixtures/hermes-agent"
|
||||
ATLAS_PATCHER = DOCKERFILES / "hermes-webui-atlas-patch.py"
|
||||
BRAND_PATCHER = DOCKERFILES / "hermes-webui-brand-patch.py"
|
||||
MANIFEST_PATCHER = DOCKERFILES / "hermes-webui-manifest-patch.py"
|
||||
SMOKE = DOCKERFILES / "hermes-webui-smoke.py"
|
||||
ASSETS = DOCKERFILES / "hermes-webui-assets"
|
||||
MANIFEST = DOCKERFILES / "hermes-webui-manifest.json"
|
||||
BRAND_CSS = DOCKERFILES / "hermes-webui-brand.css"
|
||||
@ -28,6 +34,14 @@ EXPECTED_HASHES = {
|
||||
}
|
||||
|
||||
|
||||
def _load(path: Path, name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _patched_fixture(tmp_path: Path) -> Path:
|
||||
target = tmp_path / "hermes-webui"
|
||||
agent_target = tmp_path / "hermes-agent"
|
||||
@ -36,7 +50,8 @@ def _patched_fixture(tmp_path: Path) -> Path:
|
||||
env = os.environ.copy()
|
||||
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
||||
env["HERMES_AGENT_PATCH_ROOT"] = str(agent_target)
|
||||
for patcher in (ATLAS_PATCHER, BRAND_PATCHER):
|
||||
env["HERMES_WEBUI_MANIFEST_SOURCE"] = str(MANIFEST)
|
||||
for patcher in (ATLAS_PATCHER, BRAND_PATCHER, MANIFEST_PATCHER):
|
||||
subprocess.run(
|
||||
[sys.executable, str(patcher)],
|
||||
cwd=ROOT,
|
||||
@ -163,6 +178,109 @@ def test_brand_patch_rejects_upstream_drift_before_partial_success(
|
||||
assert "brand patch context changed" in result.stderr
|
||||
|
||||
|
||||
def test_manifest_patch_rejects_drift_before_wholesale_replacement(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""The branded file cannot hide a changed manifest in the pinned image."""
|
||||
target = tmp_path / "hermes-webui"
|
||||
shutil.copytree(FIXTURE, target)
|
||||
installed = target / "static/manifest.json"
|
||||
installed.write_text(
|
||||
installed.read_text(encoding="utf-8").replace(
|
||||
'"name": "Hermes"', '"name": "Upstream drift"', 1
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
before = installed.read_bytes()
|
||||
env = os.environ.copy()
|
||||
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
||||
env["HERMES_WEBUI_MANIFEST_SOURCE"] = str(MANIFEST)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(MANIFEST_PATCHER)],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
assert "pinned upstream SHA-256 mismatch" in result.stderr
|
||||
assert installed.read_bytes() == before
|
||||
|
||||
|
||||
class _SmokeResponse:
|
||||
status = 200
|
||||
|
||||
def __init__(self, body: bytes, url: str) -> None:
|
||||
self.body = body
|
||||
self.url = url
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return None
|
||||
|
||||
def read(self) -> bytes:
|
||||
return self.body
|
||||
|
||||
def geturl(self) -> str:
|
||||
return self.url
|
||||
|
||||
|
||||
def test_server_smoke_resolves_served_manifest_icons_and_checks_root() -> None:
|
||||
"""The image gate validates HTTP responses, not its own source files."""
|
||||
module = _load(SMOKE, "hermes_webui_smoke_contract")
|
||||
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
root = (
|
||||
b'<link href="static/hermes-agent.ico">'
|
||||
b'<link href="static/hermes-agent-192.png">'
|
||||
b'<link href="static/hermes-agent-512.png">'
|
||||
b'<link href="static/hermes-brand.css?v=reviewed">'
|
||||
)
|
||||
calls = []
|
||||
|
||||
def open_fixture(request, *, timeout):
|
||||
assert timeout == 5
|
||||
url = request.full_url
|
||||
calls.append(url)
|
||||
path = urlsplit(url).path
|
||||
if path == "/manifest.json":
|
||||
return _SmokeResponse(
|
||||
json.dumps(manifest).encode(),
|
||||
"http://assets.hermes.test/pwa/manifest.json",
|
||||
)
|
||||
if path == "/":
|
||||
return _SmokeResponse(root, url)
|
||||
if path in (
|
||||
"/pwa/static/hermes-agent-192.png",
|
||||
"/pwa/static/hermes-agent-512.png",
|
||||
"/static/hermes-agent-192.png",
|
||||
):
|
||||
return _SmokeResponse(module.PNG_MAGIC + b"fixture", url)
|
||||
raise AssertionError(f"unexpected smoke URL: {url}")
|
||||
|
||||
result = module.smoke("http://hermes.test/", opener=open_fixture)
|
||||
assert result["manifest"] == "http://assets.hermes.test/pwa/manifest.json"
|
||||
assert result["icon_requests"] == 3
|
||||
assert calls.count("http://assets.hermes.test/pwa/static/hermes-agent-192.png") == 2
|
||||
assert "http://assets.hermes.test/pwa/static/hermes-agent-512.png" in calls
|
||||
assert calls.count("http://hermes.test/static/hermes-agent-192.png") == 1
|
||||
|
||||
|
||||
def test_server_smoke_rejects_unbranded_served_manifest() -> None:
|
||||
"""A successful health check cannot mask the upstream PWA identity."""
|
||||
module = _load(SMOKE, "hermes_webui_smoke_unbranded")
|
||||
|
||||
def open_upstream(request, *, timeout):
|
||||
assert timeout == 5
|
||||
payload = (FIXTURE / "static/manifest.json").read_bytes()
|
||||
return _SmokeResponse(payload, request.full_url)
|
||||
|
||||
with pytest.raises(RuntimeError, match="not branded"):
|
||||
module.smoke("http://hermes.test/", opener=open_upstream)
|
||||
|
||||
|
||||
def test_brand_css_is_accessible_dark_and_reduced_motion_aware() -> None:
|
||||
"""Identity colors retain system controls and disable cosmetic motion."""
|
||||
css = BRAND_CSS.read_text(encoding="utf-8")
|
||||
@ -185,7 +303,17 @@ def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
|
||||
assert "/opt/hermes/web/public/favicon.ico" not in dockerfile
|
||||
assert "COPY dockerfiles/hermes-webui-brand-patch.py" in dockerfile
|
||||
assert "python /tmp/hermes-webui-brand-patch.py" in dockerfile
|
||||
assert "COPY dockerfiles/hermes-webui-manifest.json" in dockerfile
|
||||
assert "COPY dockerfiles/hermes-webui-manifest-patch.py" in dockerfile
|
||||
assert "python /tmp/hermes-webui-manifest-patch.py" in dockerfile
|
||||
assert (
|
||||
"COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json"
|
||||
in dockerfile
|
||||
)
|
||||
assert (
|
||||
"COPY dockerfiles/hermes-webui-manifest.json /opt/hermes-webui/static/manifest.json"
|
||||
not in dockerfile
|
||||
)
|
||||
assert "python /tmp/hermes-webui-smoke.py http://127.0.0.1:18787/" in dockerfile
|
||||
assert "COPY dockerfiles/hermes-webui-brand.css" in dockerfile
|
||||
for name, digest in EXPECTED_HASHES.items():
|
||||
assert f"COPY dockerfiles/hermes-webui-assets/{name}" in dockerfile
|
||||
|
||||
@ -124,9 +124,14 @@ def test_webui_dockerfile_is_kaniko_safe_and_uses_reviewed_repo_source() -> None
|
||||
assert "<<" not in source
|
||||
assert "hermes-webui-base-patch.py" in source
|
||||
assert "hermes-webui-brand-patch.py" in source
|
||||
assert "hermes-webui-manifest-patch.py" in source
|
||||
assert "hermes-webui-smoke.py" in source
|
||||
assert "hermes-webui-stt-patch.py" in source
|
||||
assert "hermes-webui-atlas-voice.js" in source
|
||||
assert "hermes-webui-manifest.json" in source
|
||||
assert "urljoin(manifest_url, source)" in (
|
||||
ROOT / "dockerfiles/hermes-webui-smoke.py"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_renderer_updates_exact_chat_and_dashboard_webui_only(tmp_path: Path) -> None:
|
||||
@ -374,6 +379,12 @@ def test_flux_tracks_webui_policy_before_jenkins() -> None:
|
||||
assert container["securityContext"]["runAsNonRoot"] is True
|
||||
assert container["securityContext"]["capabilities"]["drop"] == ["ALL"]
|
||||
|
||||
release_docs = (ROOT / "docs/hermes_webui_release.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "immutable-tag:list" in release_docs
|
||||
assert "harbor-hermes-agent-immutability-ensure-1" in release_docs
|
||||
|
||||
|
||||
class _FakePolicyClient:
|
||||
origin = "https://registry.bstein.dev/api/v2.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user