Merge pull request 'Hermes WebUI release lane v2' (#48) from feature/t_8cbe6a55-hermes-webui-release-v2 into main

Reviewed-on: atlas/titan-iac#48
Reviewed-by: bstein <bstein@noreply.scm.bstein.dev>
This commit is contained in:
bstein 2026-08-23 12:19:10 +00:00
commit ced9a24cd9
25 changed files with 2743 additions and 81 deletions

View File

@ -0,0 +1,314 @@
pipeline {
agent {
kubernetes {
defaultContainer 'python'
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
atlas.bstein.dev/workload: hermes-webui-image-builder
spec:
serviceAccountName: hermes-image-builder
automountServiceAccountToken: false
enableServiceLinks: false
restartPolicy: Never
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
nodeSelector:
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- titan-04
- titan-14
- titan-18
- titan-19
- titan-22
- titan-24
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: hardware
operator: In
values:
- rpi5
imagePullSecrets:
- name: harbor-bstein-robot
containers:
- name: jnlp
image: jenkins/inbound-agent@sha256:8eda4fe2a66bcf6a5e43436d9918fc14c306204dc8fcd75f4e15e0e6e5dc759a
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 25m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
- name: python
image: registry.bstein.dev/bstein/python@sha256:269541d3387baae008df4608ead893dba2b5cdaad1a5a380731a88992d34b808
command: ["sleep"]
args: ["99d"]
tty: true
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
- name: kaniko
image: gcr.io/kaniko-project/executor@sha256:c3109d5926a997b100c4343944e06c6b30a6804b2f9abe0994d3de6ef92b028e
command: ["/busybox/sh", "-c"]
args: ["/busybox/sleep 99d"]
tty: true
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["CHOWN", "FOWNER", "DAC_OVERRIDE", "SETGID", "SETUID"]
privileged: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
"""
}
}
parameters {
booleanParam(
name: 'PUBLISH_IMAGE',
defaultValue: false,
description: 'Publish the reviewed main revision to Harbor.'
)
string(
name: 'EXPECTED_SOURCE_REVISION',
defaultValue: '',
description: 'Exact 40-character commit on atlas/titan-iac main.'
)
string(
name: 'CONFIRM_PUBLISH',
defaultValue: '',
description: 'Enter PUBLISH HERMES WEBUI to confirm the release.'
)
}
environment {
HERMES_IMAGE = 'registry.bstein.dev/bstein/hermes-webui'
}
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '100', artifactDaysToKeepStr: '30', artifactNumToKeepStr: '100'))
skipDefaultCheckout(true)
timeout(time: 150, unit: 'MINUTES')
}
stages {
stage('Checkout reviewed source') {
steps {
checkout scm
}
}
stage('Enforce release boundary') {
steps {
container('jnlp') {
sh '''
set -eu
mkdir -p build
test "${PUBLISH_IMAGE}" = "true"
test "${CONFIRM_PUBLISH}" = "PUBLISH HERMES WEBUI"
case "${EXPECTED_SOURCE_REVISION}" in
*[!0-9a-f]*|'')
echo "EXPECTED_SOURCE_REVISION must be a lowercase full commit" >&2
exit 2
;;
esac
test "${#EXPECTED_SOURCE_REVISION}" -eq 40
actual_revision="$(git rev-parse HEAD)"
test "${actual_revision}" = "${EXPECTED_SOURCE_REVISION}"
test "${actual_revision}" = "$(git rev-parse origin/main)"
test -z "$(git status --porcelain)"
test -f dockerfiles/Dockerfile.hermes-webui
case "${BUILD_NUMBER}" in
''|0*|*[!0-9]*)
echo "BUILD_NUMBER must be a positive decimal integer" >&2
exit 2
;;
esac
printf '%s\n' \
"${HERMES_IMAGE}:git-${actual_revision}-build-${BUILD_NUMBER}" \
> build/hermes-webui.destination
'''
}
}
}
stage('Validate reviewed WebUI source') {
steps {
container('python') {
sh '''
set -eu
python3 -m pip install --disable-pip-version-check --no-cache-dir \
--target=/tmp/hermes-webui-release-test-deps \
pytest==8.3.4 PyYAML==6.0.2
PYTHONPATH=/tmp/hermes-webui-release-test-deps \
python3 -m pytest -q \
testing/tests/test_hermes_webui_brand.py \
testing/tests/test_hermes_webui_release.py
'''
}
}
}
stage('Reject replay before publish') {
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-robot',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASSWORD'
)]) {
sh '''
set -eu
set +x
destination="$(cat build/hermes-webui.destination)"
python3 ci/scripts/hermes_webui_release.py assert-absent \
--source-revision "${EXPECTED_SOURCE_REVISION}" \
--build-number "${BUILD_NUMBER}" \
--destination "${destination}"
'''
}
}
}
stage('Build and publish without a daemon') {
steps {
container('kaniko') {
withCredentials([usernamePassword(
credentialsId: 'harbor-robot',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASSWORD'
)]) {
sh '''#!/busybox/sh
set -eu
set +x
config_path=/kaniko/.docker/config.json
destination="$(cat build/hermes-webui.destination)"
umask 077
auth="$(printf '%s:%s' "${HARBOR_USER}" "${HARBOR_PASSWORD}" | /busybox/base64 | /busybox/tr -d '\n')"
/busybox/mkdir -p /kaniko/.docker
/busybox/printf '{"auths":{"registry.bstein.dev":{"auth":"%s"}}}\n' "${auth}" > "${config_path}"
unset HARBOR_USER HARBOR_PASSWORD auth
trap '/busybox/rm -f "${config_path}"' EXIT HUP INT TERM
umask 022
/kaniko/executor \
--registry-mirror=harbor-core.harbor.svc.cluster.local \
--insecure-registry=harbor-core.harbor.svc.cluster.local \
--context="dir://${WORKSPACE}" \
--dockerfile="${WORKSPACE}/dockerfiles/Dockerfile.hermes-webui" \
--destination="${destination}" \
--digest-file="${WORKSPACE}/build/hermes-webui.digest" \
--image-name-tag-with-digest-file="${WORKSPACE}/build/hermes-webui.image" \
--label="org.opencontainers.image.revision=${EXPECTED_SOURCE_REVISION}" \
--label="org.opencontainers.image.source=https://scm.bstein.dev/atlas/titan-iac" \
--label="org.opencontainers.image.title=hermes-webui" \
--cleanup \
--push-retry=3
/busybox/chmod 644 build/hermes-webui.digest build/hermes-webui.image
'''
}
}
}
}
stage('Render reviewed Flux handoff') {
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-robot',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASSWORD'
)]) {
sh '''
set -eu
set +x
destination="$(cat build/hermes-webui.destination)"
python3 ci/scripts/hermes_webui_release.py render \
--digest-file build/hermes-webui.digest \
--image-file build/hermes-webui.image \
--source-revision "${EXPECTED_SOURCE_REVISION}" \
--build-number "${BUILD_NUMBER}" \
--destination "${destination}" \
--chat-manifest services/hermes/chat-statefulset.yaml \
--dashboard-manifest services/hermes/deployment.yaml \
--output-dir build/hermes-webui-release
test -s build/hermes-webui-release/hermes-webui-image-update.patch
test -s build/hermes-webui-release/hermes-webui-image.json
'''
}
}
}
}
post {
success {
sh '''
set -eu
expected_files="$(printf '%s\n' \
build/hermes-webui.destination \
build/hermes-webui.digest \
build/hermes-webui.image \
build/hermes-webui-release/hermes-chat-statefulset.yaml \
build/hermes-webui-release/hermes-dashboard-deployment.yaml \
build/hermes-webui-release/hermes-webui-image.json \
build/hermes-webui-release/hermes-webui-image-update.patch \
| LC_ALL=C sort)"
actual_files="$(find build -type f -print | LC_ALL=C sort)"
test "${actual_files}" = "${expected_files}"
destination="$(cat build/hermes-webui.destination)"
python3 ci/scripts/hermes_webui_release.py verify-evidence \
--digest-file build/hermes-webui.digest \
--image-file build/hermes-webui.image \
--source-revision "${EXPECTED_SOURCE_REVISION}" \
--build-number "${BUILD_NUMBER}" \
--destination "${destination}" \
--chat-manifest services/hermes/chat-statefulset.yaml \
--dashboard-manifest services/hermes/deployment.yaml \
--output-dir build/hermes-webui-release
'''
archiveArtifacts(
artifacts: 'build/hermes-webui.destination,build/hermes-webui.digest,build/hermes-webui.image,build/hermes-webui-release/hermes-chat-statefulset.yaml,build/hermes-webui-release/hermes-dashboard-deployment.yaml,build/hermes-webui-release/hermes-webui-image.json,build/hermes-webui-release/hermes-webui-image-update.patch',
allowEmptyArchive: false,
fingerprint: true
)
}
cleanup {
container('kaniko') {
sh '''#!/busybox/sh
/busybox/rm -f /kaniko/.docker/config.json
'''
}
}
}
}

View File

@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Render and revalidate the two-workload Hermes WebUI Flux handoff."""
from __future__ import annotations
import difflib
import json
import re
from pathlib import Path
from typing import Any
DEFAULT_IMAGE = "registry.bstein.dev/bstein/hermes-webui"
DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
BUILD_PATTERN = re.compile(r"^[1-9][0-9]*$")
DESTINATION_PATTERN = re.compile(
r"^registry\.bstein\.dev/bstein/hermes-webui:"
r"git-([0-9a-f]{40})-build-([1-9][0-9]*)$"
)
def validated(value: str, pattern: re.Pattern[str], label: str) -> str:
"""Return a normalized value when it matches the release contract."""
normalized = value.strip()
if not pattern.fullmatch(normalized):
raise ValueError(f"invalid {label}: expected {pattern.pattern}")
return normalized
def validate_destination(
destination: str, source_revision: str, build_number: str
) -> tuple[str, str]:
"""Bind one unique build tag to the reviewed revision and Jenkins build."""
revision = validated(source_revision, REVISION_PATTERN, "source revision")
build = validated(build_number, BUILD_PATTERN, "build number")
match = DESTINATION_PATTERN.fullmatch(destination.strip())
if not match or match.groups() != (revision, build):
raise ValueError(
"destination must bind the reviewed revision and unique Jenkins build"
)
return revision, build
def render_workload(
source: str,
digest: str,
*,
kind: str,
name: str,
image: str = DEFAULT_IMAGE,
) -> str:
"""Replace one WebUI image in one exact Flux workload without reformatting."""
digest = validated(digest, DIGEST_PATTERN, "image digest")
identity = re.compile(
rf"\A(?:#[^\n]*\n)*apiVersion: apps/v1\nkind: {re.escape(kind)}\n"
rf"metadata:\n name: {re.escape(name)}\n"
)
if not identity.search(source):
raise ValueError(f"Flux target identity changed: expected {kind}/{name}")
lines = source.splitlines(keepends=True)
matches: list[int] = []
for index, line in enumerate(lines):
stripped = line.strip()
if not stripped.startswith(f"image: {image}@"):
continue
current_digest = stripped.removeprefix(f"image: {image}@")
validated(current_digest, DIGEST_PATTERN, "current Flux image digest")
matches.append(index)
if len(matches) != 1:
raise ValueError(
f"expected exactly one {image!r} image in {kind}/{name}; "
f"found {len(matches)}"
)
index = matches[0]
newline = "\n" if lines[index].endswith("\n") else ""
prefix = lines[index][: len(lines[index]) - len(lines[index].lstrip())]
lines[index] = f"{prefix}image: {image}@{digest}{newline}"
return "".join(lines)
def _targets(chat_manifest: Path, dashboard_manifest: Path):
return (
(
chat_manifest,
"StatefulSet",
"hermes-chat-tenant",
"hermes-chat-statefulset.yaml",
),
(
dashboard_manifest,
"Deployment",
"hermes",
"hermes-dashboard-deployment.yaml",
),
)
def _metadata(
digest: str, source_revision: str, build_number: str, destination: str
) -> dict[str, Any]:
return {
"build_number": build_number,
"digest": digest,
"flux_image": f"{DEFAULT_IMAGE}@{digest}",
"flux_targets": [
"apps/StatefulSet/hermes/hermes-chat-tenant",
"apps/Deployment/hermes/hermes",
],
"image": DEFAULT_IMAGE,
"published_tag": destination,
"source_revision": source_revision,
}
def _rendered_and_patch(
digest: str, chat_manifest: Path, dashboard_manifest: Path
) -> tuple[dict[str, str], str]:
rendered_targets: dict[str, str] = {}
patch_parts: list[str] = []
for path, kind, name, artifact_name in _targets(chat_manifest, dashboard_manifest):
source = path.read_text(encoding="utf-8")
rendered = render_workload(source, digest, kind=kind, name=name)
rendered_targets[artifact_name] = rendered
patch_parts.append(
"".join(
difflib.unified_diff(
source.splitlines(keepends=True),
rendered.splitlines(keepends=True),
fromfile=f"a/services/hermes/{path.name}",
tofile=f"b/services/hermes/{path.name}",
)
)
)
return rendered_targets, "".join(patch_parts)
def write_release_artifacts(
*,
digest: str,
source_revision: str,
build_number: str,
destination: str,
chat_manifest: Path,
dashboard_manifest: Path,
output_dir: Path,
) -> dict[str, Any]:
"""Write two rendered Flux targets, one patch, and credential-free evidence."""
digest = validated(digest, DIGEST_PATTERN, "image digest")
source_revision, build_number = validate_destination(
destination, source_revision, build_number
)
rendered_targets, patch = _rendered_and_patch(
digest, chat_manifest, dashboard_manifest
)
if not patch:
raise ValueError("published digest already matches every Flux target")
output_dir.mkdir(parents=True, exist_ok=True)
for artifact_name, rendered in rendered_targets.items():
(output_dir / artifact_name).write_text(rendered, encoding="utf-8")
(output_dir / "hermes-webui-image-update.patch").write_text(patch, encoding="utf-8")
metadata = _metadata(digest, source_revision, build_number, destination)
(output_dir / "hermes-webui-image.json").write_text(
json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return metadata
def validate_release_artifacts(
*,
digest: str,
source_revision: str,
build_number: str,
destination: str,
chat_manifest: Path,
dashboard_manifest: Path,
output_dir: Path,
) -> None:
"""Revalidate the exact successful-build evidence without rewriting it."""
digest = validated(digest, DIGEST_PATTERN, "image digest")
source_revision, build_number = validate_destination(
destination, source_revision, build_number
)
expected_names = {
"hermes-webui-image.json",
"hermes-webui-image-update.patch",
"hermes-chat-statefulset.yaml",
"hermes-dashboard-deployment.yaml",
}
entries = list(output_dir.iterdir())
if {entry.name for entry in entries} != expected_names or not all(
entry.is_file() and not entry.is_symlink() for entry in entries
):
raise ValueError("release output must contain exactly four evidence files")
rendered_targets, patch = _rendered_and_patch(
digest, chat_manifest, dashboard_manifest
)
metadata = _metadata(digest, source_revision, build_number, destination)
expected = {
"hermes-webui-image.json": json.dumps(metadata, indent=2, sort_keys=True)
+ "\n",
"hermes-webui-image-update.patch": patch,
**rendered_targets,
}
for name, expected_text in expected.items():
if (output_dir / name).read_text(encoding="utf-8") != expected_text:
raise ValueError(f"release evidence is incomplete or mismatched: {name}")

View File

@ -0,0 +1,388 @@
#!/usr/bin/env python3
"""Verify and render a reviewable Hermes WebUI image release."""
from __future__ import annotations
import argparse
import base64
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Callable
from hermes_webui_flux_release import (
DEFAULT_IMAGE as DEFAULT_IMAGE,
DESTINATION_PATTERN,
DIGEST_PATTERN,
REVISION_PATTERN,
render_workload as render_workload,
validate_destination,
validate_release_artifacts as validate_flux_release_artifacts,
validated as _validated,
write_release_artifacts,
)
HARBOR_API_ORIGIN = "https://registry.bstein.dev/api/v2.0"
HARBOR_PROJECT = "bstein"
HARBOR_REPOSITORY = "hermes-webui"
IMMUTABLE_REPOSITORY_PATTERN = "hermes-webui"
IMMUTABLE_TAG_PATTERN = "git-*-build-*"
class _NoRedirect(urllib.request.HTTPRedirectHandler):
"""Never send registry credentials to a redirect target."""
def redirect_request(self, _request, _file, _code, _message, _headers, _url):
return None
def validate_kaniko_evidence(
*, digest_text: str, image_text: str, destination: str
) -> str:
"""Cross-check both independent Kaniko output files against the destination."""
digest_lines = digest_text.splitlines()
image_lines = image_text.splitlines()
if len(digest_lines) != 1:
raise ValueError("Kaniko digest evidence must contain exactly one line")
if len(image_lines) != 1:
raise ValueError("Kaniko image evidence must contain exactly one line")
digest = _validated(digest_lines[0], DIGEST_PATTERN, "image digest")
if image_lines[0].strip() != f"{destination}@{digest}":
raise ValueError("Kaniko image evidence does not match destination and digest")
return digest
def _registry_request(request: urllib.request.Request, timeout: int) -> Any:
"""Make a registry request without following redirects."""
opener = urllib.request.build_opener(_NoRedirect())
try:
return opener.open(request, timeout=timeout)
except urllib.error.HTTPError as exc:
return exc
def _artifact_response(
destination: str,
*,
username: str,
password: str,
opener: Callable[[urllib.request.Request, int], Any] = _registry_request,
) -> tuple[int, bytes]:
"""Read one exact Harbor artifact by tag with bounded response size."""
match = DESTINATION_PATTERN.fullmatch(destination)
if not match:
raise ValueError("invalid destination")
if not username or not password:
raise RuntimeError("Harbor credentials are empty")
tag = destination.rsplit(":", 1)[1]
encoded_tag = urllib.parse.quote(tag, safe="")
auth = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
request = urllib.request.Request(
f"{HARBOR_API_ORIGIN}/projects/{HARBOR_PROJECT}/repositories/"
f"{HARBOR_REPOSITORY}/artifacts/{encoded_tag}"
"?with_immutable_status=true",
headers={"Accept": "application/json", "Authorization": f"Basic {auth}"},
method="GET",
)
with opener(request, 20) as response:
body = response.read(1_048_577)
if len(body) > 1_048_576:
raise RuntimeError("Harbor artifact response exceeded the size limit")
return int(response.status), body
def _immutable_rules_response(
*,
username: str,
password: str,
opener: Callable[[urllib.request.Request, int], Any] = _registry_request,
) -> tuple[int, bytes, dict[str, str]]:
"""Read the project policy with the same least-privilege publish identity."""
if not username or not password:
raise RuntimeError("Harbor credentials are empty")
auth = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
request = urllib.request.Request(
f"{HARBOR_API_ORIGIN}/projects/{HARBOR_PROJECT}/immutabletagrules"
"?page=1&page_size=100",
headers={"Accept": "application/json", "Authorization": f"Basic {auth}"},
method="GET",
)
with opener(request, 20) as response:
body = response.read(1_048_577)
if len(body) > 1_048_576:
raise RuntimeError("Harbor immutable rule response exceeded the size limit")
return int(response.status), body, dict(response.headers)
def _require_complete_rule_page(
rules: list[dict[str, Any]], headers: dict[str, str]
) -> None:
"""Require proof that the bounded first page contains every rule."""
raw_total = next(
(value for key, value in headers.items() if key.lower() == "x-total-count"),
None,
)
if raw_total is None or not str(raw_total).isdecimal():
raise RuntimeError("Harbor immutable rule list omitted a valid total count")
if int(raw_total) != len(rules):
raise RuntimeError("Harbor immutable rule list was truncated")
def _normalized_immutable_rule(rule: dict[str, Any]) -> dict[str, Any]:
"""Select only fields that bind the server-side build-tag policy."""
return {
"disabled": bool(rule.get("disabled", False)),
"action": rule.get("action"),
"template": rule.get("template"),
"tag_selectors": [
{
"kind": item.get("kind"),
"decoration": item.get("decoration"),
"pattern": item.get("pattern"),
}
for item in rule.get("tag_selectors") or []
if isinstance(item, dict)
],
"scope_selectors": {
"repository": [
{
"kind": item.get("kind"),
"decoration": item.get("decoration"),
"pattern": item.get("pattern"),
}
for item in (rule.get("scope_selectors") or {}).get("repository", [])
if isinstance(item, dict)
]
},
}
def verify_immutable_policy(
*,
username: str,
password: str,
opener: Callable[[urllib.request.Request, int], Any] = _registry_request,
) -> None:
"""Fail closed before build unless the exact Harbor rule is active."""
status, body, headers = _immutable_rules_response(
username=username, password=password, opener=opener
)
if status != 200:
raise RuntimeError(f"Harbor immutable policy preflight returned HTTP {status}")
try:
rules = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("Harbor returned invalid immutable rule JSON") from exc
if not isinstance(rules, list) or not all(isinstance(item, dict) for item in rules):
raise RuntimeError("Harbor immutable rule list has an invalid shape")
_require_complete_rule_page(rules, headers)
expected = {
"disabled": False,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": IMMUTABLE_TAG_PATTERN,
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": IMMUTABLE_REPOSITORY_PATTERN,
}
]
},
}
matches = [
_normalized_immutable_rule(item)
for item in rules
if _normalized_immutable_rule(item)["tag_selectors"]
== expected["tag_selectors"]
and _normalized_immutable_rule(item)["scope_selectors"]
== expected["scope_selectors"]
]
if matches != [expected]:
raise RuntimeError("Harbor immutable build-tag policy is absent or not exact")
def assert_tag_absent(
destination: str,
*,
username: str,
password: str,
opener: Callable[[urllib.request.Request, int], Any] = _registry_request,
) -> None:
"""Reject replay before Kaniko can push an already-used immutable identity."""
status, _body = _artifact_response(
destination, username=username, password=password, opener=opener
)
if status == 404:
return
if status == 200:
raise RuntimeError("Harbor destination tag already exists; refusing overwrite")
raise RuntimeError(f"Harbor destination preflight returned HTTP {status}")
def verify_registry_digest(
destination: str,
digest: str,
source_revision: str,
*,
username: str,
password: str,
opener: Callable[[urllib.request.Request, int], Any] = _registry_request,
) -> None:
"""Verify Harbor resolves the tag, digest, and persisted source revision."""
digest = _validated(digest, DIGEST_PATTERN, "image digest")
source_revision = _validated(
source_revision, REVISION_PATTERN, "source revision"
)
status, body = _artifact_response(
destination, username=username, password=password, opener=opener
)
if status != 200:
raise RuntimeError(f"Harbor manifest verification returned HTTP {status}")
try:
artifact = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("Harbor returned invalid artifact JSON") from exc
harbor_digest = str(artifact.get("digest") or "").strip()
if not DIGEST_PATTERN.fullmatch(harbor_digest):
raise RuntimeError("Harbor response omitted a valid artifact digest")
if harbor_digest != digest:
raise RuntimeError("Harbor digest does not match Kaniko evidence")
expected_tag = destination.rsplit(":", 1)[1]
matching_tags = [
item
for item in artifact.get("tags") or []
if isinstance(item, dict) and item.get("name") == expected_tag
]
if len(matching_tags) != 1:
raise RuntimeError("Harbor artifact does not contain the expected tag")
if matching_tags[0].get("immutable") is not True:
raise RuntimeError("Harbor did not enforce the expected tag as immutable")
labels = ((artifact.get("extra_attrs") or {}).get("config") or {}).get("Labels")
if not isinstance(labels, dict):
raise RuntimeError("Harbor artifact omitted OCI image labels")
if labels.get("org.opencontainers.image.revision") != source_revision:
raise RuntimeError("Harbor OCI source-revision label does not match")
def validate_release_artifacts(
*,
digest_file: Path,
image_file: Path,
source_revision: str,
build_number: str,
destination: str,
chat_manifest: Path,
dashboard_manifest: Path,
output_dir: Path,
) -> None:
"""Revalidate the exact successful-build evidence without rewriting it."""
digest = validate_kaniko_evidence(
digest_text=digest_file.read_text(encoding="utf-8"),
image_text=image_file.read_text(encoding="utf-8"),
destination=destination,
)
validate_flux_release_artifacts(
digest=digest,
source_revision=source_revision,
build_number=build_number,
destination=destination,
chat_manifest=chat_manifest,
dashboard_manifest=dashboard_manifest,
output_dir=output_dir,
)
def _credentials() -> tuple[str, str]:
"""Read the masked, runtime-only Jenkins credential environment."""
username = os.environ.get("HARBOR_USER", "")
password = os.environ.get("HARBOR_PASSWORD", "")
if not username or not password:
raise RuntimeError("Harbor credentials are unavailable")
return username, password
def _common_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--source-revision", required=True)
parser.add_argument("--build-number", required=True)
parser.add_argument("--destination", required=True)
def main() -> int:
"""Fail closed around the unique tag, then verify and render after push."""
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
absent = commands.add_parser("assert-absent")
_common_arguments(absent)
render = commands.add_parser("render")
_common_arguments(render)
render.add_argument("--digest-file", required=True, type=Path)
render.add_argument("--image-file", required=True, type=Path)
render.add_argument("--chat-manifest", required=True, type=Path)
render.add_argument("--dashboard-manifest", required=True, type=Path)
render.add_argument("--output-dir", required=True, type=Path)
verify = commands.add_parser("verify-evidence")
_common_arguments(verify)
verify.add_argument("--digest-file", required=True, type=Path)
verify.add_argument("--image-file", required=True, type=Path)
verify.add_argument("--chat-manifest", required=True, type=Path)
verify.add_argument("--dashboard-manifest", required=True, type=Path)
verify.add_argument("--output-dir", required=True, type=Path)
args = parser.parse_args()
validate_destination(args.destination, args.source_revision, args.build_number)
if args.command == "verify-evidence":
validate_release_artifacts(
digest_file=args.digest_file,
image_file=args.image_file,
source_revision=args.source_revision,
build_number=args.build_number,
destination=args.destination,
chat_manifest=args.chat_manifest,
dashboard_manifest=args.dashboard_manifest,
output_dir=args.output_dir,
)
return 0
username, password = _credentials()
if args.command == "assert-absent":
verify_immutable_policy(username=username, password=password)
assert_tag_absent(args.destination, username=username, password=password)
return 0
digest = validate_kaniko_evidence(
digest_text=args.digest_file.read_text(encoding="utf-8"),
image_text=args.image_file.read_text(encoding="utf-8"),
destination=args.destination,
)
verify_registry_digest(
args.destination,
digest,
args.source_revision,
username=username,
password=password,
)
write_release_artifacts(
digest=digest,
source_revision=args.source_revision,
build_number=args.build_number,
destination=args.destination,
chat_manifest=args.chat_manifest,
dashboard_manifest=args.dashboard_manifest,
output_dir=args.output_dir,
)
return 0
if __name__ == "__main__": # pragma: no cover - exercised through main()
raise SystemExit(main())

View File

@ -22,6 +22,10 @@ spec:
kind: Job
name: harbor-hermes-agent-immutability-ensure-1
namespace: harbor
- apiVersion: batch/v1
kind: Job
name: harbor-hermes-webui-immutability-ensure-1
namespace: harbor
dependsOn:
- name: core
- name: longhorn

View File

@ -10,85 +10,9 @@ USER root
# while the gateway remains the only process that owns an agent conversation.
COPY --from=webui /apptoo /opt/hermes-webui
# The account policy caps user-selected reasoning at xhigh even when a provider
# advertises a newer, more expensive level.
RUN /opt/hermes/.venv/bin/python - <<'PY'
from pathlib import Path
config = Path("/opt/hermes-webui/api/config.py")
source = config.read_text(encoding="utf-8")
before = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")'
after = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")'
if before not in source:
raise SystemExit("Hermes WebUI reasoning-effort patch context changed")
config.write_text(source.replace(before, after, 1), encoding="utf-8")
index = Path("/opt/hermes-webui/static/index.html")
source = index.read_text(encoding="utf-8")
before = ' <div class="reasoning-option" data-effort="max">Max</div>\n'
if before not in source:
raise SystemExit("Hermes WebUI xhigh UI patch context changed")
index.write_text(source.replace(before, "", 1), encoding="utf-8")
# oauth2-proxy returns 401 for browser API and health probes when the secure
# session expires. Re-enter OIDC with the complete return path instead of
# presenting an endless, inaccurate "connection lost" loop.
ui = Path("/opt/hermes-webui/static/ui.js")
source = ui.read_text(encoding="utf-8")
before = ''' const res=await fetcher(_offlineHealthUrl(),opts);
return !!(res&&res.ok);
'''
after = ''' const res=await fetcher(_offlineHealthUrl(),opts);
if(res&&(res.status===401||res.status===403)){
const rd=window.location.pathname+window.location.search+window.location.hash;
window.location.assign('/oauth2/start?rd='+encodeURIComponent(rd));
return false;
}
return !!(res&&res.ok);
'''
if source.count(before) != 1:
raise SystemExit("Hermes WebUI auth-recovery patch context changed")
ui.write_text(source.replace(before, after, 1), encoding="utf-8")
# Make delegated session hierarchy obvious and collapsible in the sidebar.
sessions = Path("/opt/hermes-webui/static/sessions.js")
source = sessions.read_text(encoding="utf-8")
before = ''' const childLabel=t('session_meta_children', childCount);
childCountEl.textContent=childLabel;
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
'''
after = ''' const childLabel=t('session_meta_children', childCount);
const childrenExpanded=_expandedChildSessionKeys.has(lineageKey)||!!searchQueryRaw;
childCountEl.textContent=(childrenExpanded?'▾ ':'▸ ')+childLabel;
childCountEl.setAttribute('aria-expanded',childrenExpanded?'true':'false');
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
'''
if source.count(before) != 1:
raise SystemExit("Hermes WebUI child-session toggle patch context changed")
sessions.write_text(source.replace(before, after, 1), encoding="utf-8")
# A profile's model is only its default; a session-level selector can override
# it. Label the scope so the dropdown does not contradict the effective model.
panels = Path("/opt/hermes-webui/static/panels.js")
source = panels.read_text(encoding="utf-8")
before = " if (typeof p.model === 'string' && p.model) meta.push(p.model.split('/').pop());\n"
after = ''' if (typeof p.model === 'string' && p.model) {
const routeLabels = {
'atlas/auto/fast': 'Automatic · Fast',
'atlas/auto/balanced': 'Automatic · Balanced',
'atlas/auto/deep': 'Automatic · Deep',
'atlas/auto/maximum': 'Automatic · Maximum',
};
meta.push('profile default: ' + (routeLabels[p.model] || p.model.split('/').pop()));
}
'''
if source.count(before) != 2:
raise SystemExit("Hermes WebUI profile-model label patch context changed")
panels.write_text(source.replace(before, after, 2), encoding="utf-8")
PY
# Add the Atlas voice bridge as a narrow integration layer. It activates only
# when a tenant's server-side STT capability reports the private Jetson route.
COPY dockerfiles/hermes-webui-base-patch.py /tmp/hermes-webui-base-patch.py
COPY dockerfiles/hermes-webui-atlas-patch.py /tmp/hermes-webui-atlas-patch.py
COPY dockerfiles/hermes-webui-stt-patch.py /tmp/hermes-webui-stt-patch.py
COPY dockerfiles/hermes-webui-telegram-project-patch.py /tmp/hermes-webui-telegram-project-patch.py
@ -96,15 +20,27 @@ COPY dockerfiles/hermes-webui-atlas-voice.js /opt/hermes-webui/static/atlas-voic
COPY dockerfiles/hermes-webui-atlas-voice.css /opt/hermes-webui/static/atlas-voice.css
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 /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
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-base-patch.py
RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-atlas-patch.py
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")' \
/opt/hermes-webui/api/config.py \
&& ! grep -Fq 'data-effort="max"' /opt/hermes-webui/static/index.html \
&& grep -Fq 'res.status===401||res.status===403' /opt/hermes-webui/static/ui.js \
&& grep -Fq "window.location.assign('/oauth2/start?rd='" /opt/hermes-webui/static/ui.js \
&& grep -Fq "childrenExpanded?'▾ ':'▸ '" /opt/hermes-webui/static/sessions.js \
&& grep -Fq "TELEGRAM_PROJECT_NAME = 'Telegram'" /opt/hermes-webui/api/models.py \
@ -124,13 +60,34 @@ RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
&& grep -Fq 'def _atlas_tts_language(body):' /opt/hermes-webui/api/routes.py \
&& grep -Fq 'request_payload["language"] = _atlas_language' /opt/hermes-webui/api/routes.py \
&& grep -Fq 'takeSttLanguage(token)' /opt/hermes-webui/static/atlas-voice.js \
&& grep -Fq '<title>Hermes Chat</title>' /opt/hermes-webui/static/index.html \
&& grep -Fq 'id="hermesBrandStyles"' /opt/hermes-webui/static/index.html \
&& grep -Fq 'static/hermes-agent-512.png' /opt/hermes-webui/static/index.html \
&& grep -Fq 'prefers-reduced-motion: reduce' /opt/hermes-webui/static/hermes-brand.css \
&& grep -Fq '"name": "Hermes Chat"' /opt/hermes-webui/static/manifest.json \
&& grep -Fq "'./static/hermes-agent-512.png'" /opt/hermes-webui/static/sw.js \
&& printf '%s %s\n' \
'aefe65e6574e6f46d3382588f46c508e1b3f3b3c9ce3dec6d335403a5374add9' \
'/opt/hermes-webui/static/hermes-agent.ico' \
| sha256sum -c - \
&& printf '%s %s\n' \
'0e4102cc715372058dd6ab55e9cde2567e46fee5ab6557b564cdd212ccd2616f' \
'/opt/hermes-webui/static/hermes-agent-192.png' \
| sha256sum -c - \
&& printf '%s %s\n' \
'6661e5ca0ecc690af213b9f85f961541e6d3d0e36946ede9d3c2c97dfbd3c23d' \
'/opt/hermes-webui/static/hermes-agent-512.png' \
| sha256sum -c - \
&& /opt/hermes/.venv/bin/python -m py_compile \
/opt/hermes-webui/api/routes.py \
/opt/hermes-webui/api/upload.py \
/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 \
@ -142,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 \

View File

@ -0,0 +1,18 @@
# Hermes WebUI persona icon provenance
`hermes-agent.ico` is a byte-for-byte tracked copy of the canonical Hermes
Agent dashboard icon from `/opt/hermes/web/public/favicon.ico`. The same bytes
were independently present at `/opt/hermes/hermes_cli/web_dist/favicon.ico` on
the Atlas coordinator when this asset was imported on 2026-08-23.
- Canonical ICO SHA-256: `aefe65e6574e6f46d3382588f46c508e1b3f3b3c9ce3dec6d335403a5374add9`
- ICO payloads: PNG-encoded RGBA variants at 16x16, 32x32, and 48x48
- `hermes-agent-192.png`: 48px canonical variant resized to 192x192 with
Pillow 12.2.0 LANCZOS resampling; SHA-256
`0e4102cc715372058dd6ab55e9cde2567e46fee5ab6557b564cdd212ccd2616f`
- `hermes-agent-512.png`: 48px canonical variant resized to 512x512 with
Pillow 12.2.0 LANCZOS resampling; SHA-256
`6661e5ca0ecc690af213b9f85f961541e6d3d0e36946ede9d3c2c97dfbd3c23d`
The larger files are faithful format/size derivatives for PWA installation;
they do not redraw or replace the supplied persona.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Apply Atlas baseline UI policy patches to the pinned Hermes WebUI."""
from pathlib import Path
config = Path("/opt/hermes-webui/api/config.py")
source = config.read_text(encoding="utf-8")
before = (
'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max")'
)
after = 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")'
if source.count(before) != 1:
raise SystemExit("Hermes WebUI reasoning-effort patch context changed")
config.write_text(source.replace(before, after, 1), encoding="utf-8")
index = Path("/opt/hermes-webui/static/index.html")
source = index.read_text(encoding="utf-8")
before = ' <div class="reasoning-option" data-effort="max">Max</div>\n'
if source.count(before) != 1:
raise SystemExit("Hermes WebUI xhigh UI patch context changed")
index.write_text(source.replace(before, "", 1), encoding="utf-8")
# oauth2-proxy returns 401 for browser API and health probes when the secure
# session expires. Re-enter OIDC with the complete return path.
ui = Path("/opt/hermes-webui/static/ui.js")
source = ui.read_text(encoding="utf-8")
before = """ const res=await fetcher(_offlineHealthUrl(),opts);
return !!(res&&res.ok);
"""
after = """ const res=await fetcher(_offlineHealthUrl(),opts);
if(res&&(res.status===401||res.status===403)){
const rd=window.location.pathname+window.location.search+window.location.hash;
window.location.assign('/oauth2/start?rd='+encodeURIComponent(rd));
return false;
}
return !!(res&&res.ok);
"""
if source.count(before) != 1:
raise SystemExit("Hermes WebUI auth-recovery patch context changed")
ui.write_text(source.replace(before, after, 1), encoding="utf-8")
# Make delegated session hierarchy obvious and collapsible in the sidebar.
sessions = Path("/opt/hermes-webui/static/sessions.js")
source = sessions.read_text(encoding="utf-8")
before = """ const childLabel=t('session_meta_children', childCount);
childCountEl.textContent=childLabel;
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
"""
after = """ const childLabel=t('session_meta_children', childCount);
const childrenExpanded=_expandedChildSessionKeys.has(lineageKey)||!!searchQueryRaw;
childCountEl.textContent=(childrenExpanded?'':'')+childLabel;
childCountEl.setAttribute('aria-expanded',childrenExpanded?'true':'false');
childCountEl.title=_sessionChildBadgeTooltip(childLabel);
"""
if source.count(before) != 1:
raise SystemExit("Hermes WebUI child-session toggle patch context changed")
sessions.write_text(source.replace(before, after, 1), encoding="utf-8")
# A profile's model is only its default; label that scope in both render paths.
panels = Path("/opt/hermes-webui/static/panels.js")
source = panels.read_text(encoding="utf-8")
before = " if (typeof p.model === 'string' && p.model) meta.push(p.model.split('/').pop());\n"
after = """ if (typeof p.model === 'string' && p.model) {
const routeLabels = {
'atlas/auto/fast': 'Automatic · Fast',
'atlas/auto/balanced': 'Automatic · Balanced',
'atlas/auto/deep': 'Automatic · Deep',
'atlas/auto/maximum': 'Automatic · Maximum',
};
meta.push('profile default: ' + (routeLabels[p.model] || p.model.split('/').pop()));
}
"""
if source.count(before) != 2:
raise SystemExit("Hermes WebUI profile-model label patch context changed")
panels.write_text(source.replace(before, after, 2), encoding="utf-8")

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Apply fail-closed Hermes identity and PWA patches to pinned WebUI source."""
from __future__ import annotations
import os
from pathlib import Path
ROOT = Path(os.environ.get("HERMES_WEBUI_PATCH_ROOT", "/opt/hermes-webui"))
def replace_exact(path: Path, before: str, after: str, count: int = 1) -> None:
"""Replace one exact upstream fragment and reject pin drift."""
source = path.read_text(encoding="utf-8")
if source.count(before) != count:
raise SystemExit(
f"Hermes brand patch context changed in {path}: {before[:80]!r}"
)
path.write_text(source.replace(before, after, count), encoding="utf-8")
def replace_between_exact(path: Path, start: str, end: str, after: str) -> None:
"""Replace one uniquely bounded upstream region and reject ambiguous input."""
source = path.read_text(encoding="utf-8")
if source.count(start) != 1 or source.count(end) != 1:
raise SystemExit(
f"Hermes brand patch context changed in {path}: {start[:80]!r}"
)
start_index = source.index(start)
end_index = source.index(end, start_index)
path.write_text(source[:start_index] + after + source[end_index:], encoding="utf-8")
index = ROOT / "static/index.html"
replace_exact(index, "<title>Hermes</title>", "<title>Hermes Chat</title>")
replace_exact(
index,
"""<link rel="icon" type="image/svg+xml" href="static/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32.png">
<link rel="shortcut icon" href="static/favicon.ico">""",
"""<link rel="icon" type="image/x-icon" href="static/hermes-agent.ico">
<link rel="icon" type="image/png" sizes="192x192" href="static/hermes-agent-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="static/hermes-agent-512.png">""",
)
replace_exact(
index,
'<meta name="apple-mobile-web-app-title" content="Hermes">',
'<meta name="apple-mobile-web-app-title" content="Hermes Chat">',
)
replace_exact(
index,
'<link rel="apple-touch-icon" sizes="512x512" href="static/apple-touch-icon.png">',
'<link rel="apple-touch-icon" sizes="192x192" href="static/hermes-agent-192.png">',
)
replace_exact(
index,
'<meta name="theme-color" content="#FAF7F0" media="(prefers-color-scheme: light)">',
'<meta name="theme-color" content="#E8F1F2" media="(prefers-color-scheme: light)">',
)
replace_exact(
index,
'<meta name="theme-color" content="#141425" media="(prefers-color-scheme: dark)">',
'<meta name="theme-color" content="#0D1420" media="(prefers-color-scheme: dark)">',
)
replace_exact(
index,
'<meta name="theme-color" id="hermes-theme-color" content="#0D0D1A">',
'<meta name="theme-color" id="hermes-theme-color" content="#0D1420">',
)
replace_exact(
index,
"var c=t==='dark'?'#141425':'#FAF7F0';",
"var c=t==='dark'?'#0D1420':'#E8F1F2';",
)
replace_exact(
index,
'<link id="voiceInstrumentStyles" rel="stylesheet" href="static/atlas-voice.css?v=__WEBUI_VERSION__">',
'<link id="voiceInstrumentStyles" rel="stylesheet" href="static/atlas-voice.css?v=__WEBUI_VERSION__">\n'
'<link id="hermesBrandStyles" rel="stylesheet" href="static/hermes-brand.css?v=__WEBUI_VERSION__">',
)
replace_between_exact(
index,
' <span class="app-titlebar-icon" aria-hidden="true">\n',
' <span class="app-titlebar-title" id="appTitlebarTitle">Hermes</span>',
' <span class="app-titlebar-icon" aria-hidden="true">\n'
' <img src="static/hermes-agent-192.png" alt="">\n'
" </span>\n",
)
replace_exact(
index,
' <span class="app-titlebar-title" id="appTitlebarTitle">Hermes</span>',
' <span class="app-titlebar-title" id="appTitlebarTitle">Hermes Chat</span>',
)
service_worker = ROOT / "static/sw.js"
replace_exact(
service_worker,
" './static/style.css' + VQ,\n",
" './static/style.css' + VQ,\n './static/hermes-brand.css' + VQ,\n",
)
replace_exact(
service_worker,
" './static/favicon.svg',\n './static/favicon-32.png',\n './manifest.json',\n",
" './static/hermes-agent.ico',\n"
" './static/hermes-agent-192.png',\n"
" './static/hermes-agent-512.png',\n"
" './manifest.json',\n",
)

View File

@ -0,0 +1,105 @@
/* Restrained Hermes/Atlas identity layered after the pinned upstream theme. */
:root {
--accent: #187f8b;
--accent-hover: #126a74;
--accent-bg: rgba(24, 127, 139, 0.09);
--accent-bg-strong: rgba(24, 127, 139, 0.17);
--accent-text: #126f7a;
--blue: #187f9f;
--gold: #9a661f;
--focus-ring: rgba(24, 127, 139, 0.38);
--focus-glow: rgba(24, 127, 139, 0.12);
}
:root.dark {
color-scheme: dark;
--bg: #070a12;
--sidebar: #0d1420;
--surface: #111b29;
--surface-subtle: rgba(116, 202, 214, 0.035);
--surface-subtle-hover: rgba(116, 202, 214, 0.075);
--border: #203044;
--border2: rgba(174, 218, 224, 0.18);
--border-subtle: rgba(174, 218, 224, 0.08);
--border-muted: rgba(174, 218, 224, 0.13);
--text: #e8f1f4;
--strong: #f8fcfd;
--muted: #91a5b3;
--em: #c5d2d8;
--accent: #48cfcc;
--accent-hover: #75dedb;
--accent-bg: rgba(72, 207, 204, 0.09);
--accent-bg-strong: rgba(72, 207, 204, 0.17);
--accent-text: #6bd8d4;
--blue: #4ca4cd;
--gold: #f0b66b;
--code-bg: #09111c;
--code-inline-bg: rgba(4, 10, 17, 0.72);
--code-text: #b9e5e4;
--pre-text: #dce8ec;
--input-bg: rgba(193, 229, 233, 0.045);
--hover-bg: rgba(193, 229, 233, 0.07);
--topbar-bg: rgba(9, 14, 24, 0.96);
--main-bg: rgba(7, 10, 18, 0.72);
--focus-ring: rgba(72, 207, 204, 0.38);
--focus-glow: rgba(72, 207, 204, 0.12);
--error: #f08b79;
--success: #65c9a6;
--warning: #f0b66b;
--info: #69b9dc;
}
:root.dark body {
background:
radial-gradient(circle at 78% 8%, rgba(72, 164, 205, 0.07), transparent 31rem),
linear-gradient(145deg, #070a12, #080d17 55%, #071017);
}
:root.dark .app-titlebar,
:root.dark .rail,
:root.dark .sidebar,
:root.dark .rightpanel,
:root.dark .topbar,
:root.dark .composer-wrap {
border-color: var(--border);
background-color: rgba(13, 20, 32, 0.94);
}
.app-titlebar-icon img {
display: block;
width: 22px;
height: 22px;
border: 1px solid rgba(72, 207, 204, 0.24);
border-radius: 7px;
box-shadow: 0 0 0 2px rgba(72, 207, 204, 0.05);
}
.app-titlebar-title {
letter-spacing: 0.025em;
}
:root.dark .composer-box:focus-within {
border-color: rgba(72, 207, 204, 0.66);
box-shadow: 0 0 0 2px var(--focus-glow), 0 10px 34px rgba(0, 0, 0, 0.2);
}
/* Keep the conversation instrument inside the same cyan/blue/gold family. */
:root.dark .voice-mode-bar {
--voice-accent: 72, 207, 204;
--voice-accent-secondary: 76, 164, 205;
border-bottom-color: rgba(174, 218, 224, 0.11);
background:
radial-gradient(circle at 50% 38%, rgba(var(--voice-accent), 0.085), transparent 47%),
linear-gradient(180deg, rgba(17, 27, 41, 0.8), rgba(7, 10, 18, 0.35));
}
@media (prefers-reduced-motion: reduce) {
:root.dark body {
background: #070a12;
}
.app-titlebar-icon img,
:root.dark .composer-box:focus-within {
transition: none !important;
}
}

View 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)

View File

@ -0,0 +1,43 @@
{
"id": "./",
"name": "Hermes Chat",
"short_name": "Hermes",
"description": "Private Hermes Agent chat on Atlas",
"start_url": "./?source=pwa",
"scope": "./",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"background_color": "#070A12",
"theme_color": "#0D1420",
"orientation": "any",
"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/hermes-agent-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
],
"icons": [
{
"src": "static/hermes-agent-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "static/hermes-agent-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}

View 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())

View File

@ -0,0 +1,49 @@
# Hermes WebUI release lane
Hermes WebUI has a release lane separate from `hermes-agent-image`. The lane
builds `dockerfiles/Dockerfile.hermes-webui` from one exact reviewed `main`
commit, publishes a unique immutable Harbor tag, independently verifies the
Harbor digest, and renders a review-only Flux patch. It never writes Git and it
never reconciles or restarts a workload.
## Release sequence
1. Merge and review all WebUI source, patch, theme, and PWA asset changes.
2. Wait for Flux to complete both Harbor immutability Jobs and refresh Jenkins
JCasC from reviewed `main`.
3. Open the manual Jenkins job `hermes-webui-image` and set:
- `PUBLISH_IMAGE=true`
- `EXPECTED_SOURCE_REVISION` to the full 40-character `main` commit
- `CONFIRM_PUBLISH=PUBLISH HERMES WEBUI`
4. Retain the fingerprinted seven-file artifact set. In particular, compare
`hermes-webui-image.json` with Harbor and review
`hermes-webui-image-update.patch`.
5. Apply that patch on a fresh branch and open a separate review. The patch is
constrained to the `webui` container in:
- `StatefulSet/hermes-chat-tenant`
- `Deployment/hermes`
6. Merge the digest-only review to let Flux roll out desired state. Do not use a
manual `kubectl set image`, restart, or reconcile as a release substitute.
The release fails closed when the requested revision is not the checked-out
`origin/main`, the unique Harbor tag already exists, the exact WebUI immutable
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
`dockerfiles/hermes-webui-assets/hermes-agent.ico`. Provenance, the canonical
SHA-256, and derivation details for the required 192px/512px PNGs are recorded
beside the asset in `SOURCE.md`; the image build never reads an icon from a
running coordinator.

View File

@ -0,0 +1,79 @@
# services/harbor/hermes-webui-immutability-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: harbor-hermes-webui-immutability-ensure-1
namespace: harbor
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-run-as-user: "65532"
vault.hashicorp.com/agent-run-as-group: "65532"
vault.hashicorp.com/role: harbor-policy-bootstrap
vault.hashicorp.com/agent-inject-secret-harbor-admin-password: kv/data/atlas/harbor/harbor-core
vault.hashicorp.com/agent-inject-template-harbor-admin-password: |
{{- with secret "kv/data/atlas/harbor/harbor-core" -}}
{{ .Data.data.harbor_admin_password }}
{{- end -}}
spec:
serviceAccountName: harbor-policy-bootstrap
enableServiceLinks: false
restartPolicy: Never
nodeSelector:
hardware: rpi5
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values: [titan-04, titan-14, titan-18, titan-19, titan-24]
securityContext:
fsGroup: 65532
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: ensure
image: docker.io/library/python@sha256:efcdfa6a6b2fd2afb9c7dfa9a5b288a6f68338b5cfdebe6b637d986067d85757
imagePullPolicy: IfNotPresent
command: [python3, /scripts/harbor_hermes_webui_immutability_ensure.py]
env:
- name: HARBOR_API_ORIGIN
value: https://registry.bstein.dev/api/v2.0
- name: HARBOR_ADMIN_PASSWORD_FILE
value: /vault/secrets/harbor-admin-password
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsGroup: 65532
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: scripts
mountPath: /scripts
readOnly: true
- name: tmp
mountPath: /tmp
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 128Mi}
volumes:
- name: scripts
configMap:
name: harbor-hermes-webui-immutability-script
defaultMode: 0555
- name: tmp
emptyDir: {}

View File

@ -14,6 +14,7 @@ resources:
- vault-sync-deployment.yaml
- policy-bootstrap-serviceaccount.yaml
- hermes-agent-immutability-job.yaml
- hermes-webui-immutability-job.yaml
- bootstrap-jobs/cassandra-registry-ensure-job.yaml
- image.yaml
configMapGenerator:
@ -23,3 +24,6 @@ configMapGenerator:
- name: harbor-hermes-agent-immutability-script
files:
- harbor_hermes_agent_immutability_ensure.py=scripts/harbor_hermes_agent_immutability_ensure.py
- name: harbor-hermes-webui-immutability-script
files:
- harbor_hermes_webui_immutability_ensure.py=scripts/harbor_hermes_webui_immutability_ensure.py

View File

@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Create and verify the narrowly scoped Hermes WebUI immutable-tag rule."""
from __future__ import annotations
import base64
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
PROJECT = "bstein"
REPOSITORY_PATTERN = "hermes-webui"
TAG_PATTERN = "git-*-build-*"
EXPECTED_ORIGIN = "https://registry.bstein.dev/api/v2.0"
MAX_RESPONSE = 1_048_576
TRANSIENT_STATUSES = {429, 502, 503, 504}
EXPECTED_RULE = {
"disabled": False,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": TAG_PATTERN,
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": REPOSITORY_PATTERN,
}
]
},
}
class NoRedirect(urllib.request.HTTPRedirectHandler):
"""Prevent Basic credentials from following an unexpected redirect."""
def redirect_request(self, _request, _file, _code, _message, _headers, _url):
return None
class HarborUnavailable(RuntimeError):
"""Harbor is not ready yet, rather than returning a policy decision."""
def normalized_rule(rule: dict[str, Any]) -> dict[str, Any]:
"""Return only the immutable contract fields Harbor must preserve."""
return {
"disabled": bool(rule.get("disabled", False)),
"action": rule.get("action"),
"template": rule.get("template"),
"tag_selectors": [
{
"kind": selector.get("kind"),
"decoration": selector.get("decoration"),
"pattern": selector.get("pattern"),
}
for selector in rule.get("tag_selectors") or []
if isinstance(selector, dict)
],
"scope_selectors": {
"repository": [
{
"kind": selector.get("kind"),
"decoration": selector.get("decoration"),
"pattern": selector.get("pattern"),
}
for selector in (rule.get("scope_selectors") or {}).get(
"repository", []
)
if isinstance(selector, dict)
]
},
}
def targets_webui_builds(rule: dict[str, Any]) -> bool:
"""Detect a rule that claims this exact repository and tag selector."""
normalized = normalized_rule(rule)
return (
normalized["tag_selectors"] == EXPECTED_RULE["tag_selectors"]
and normalized["scope_selectors"] == EXPECTED_RULE["scope_selectors"]
)
class HarborClient:
"""Bounded same-origin client for Harbor's immutable-tag API."""
def __init__(self, origin: str, username: str, password: str) -> None:
normalized_origin = origin.rstrip("/")
if normalized_origin != EXPECTED_ORIGIN:
raise ValueError("Harbor API origin is not the pinned production API")
self.origin = normalized_origin
token = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
self.headers = {"Authorization": f"Basic {token}"}
self.opener = urllib.request.build_opener(NoRedirect())
def request(
self, method: str, path: str, payload: dict[str, Any] | None = None
) -> tuple[int, bytes, dict[str, str]]:
"""Issue one request, returning even non-2xx responses for strict checks."""
data = None
headers = dict(self.headers)
if payload is not None:
data = json.dumps(payload, separators=(",", ":")).encode()
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
f"{self.origin}{path}", data=data, headers=headers, method=method
)
try:
response = self.opener.open(request, timeout=20)
except urllib.error.HTTPError as exc:
response = exc
except (urllib.error.URLError, TimeoutError) as exc:
raise HarborUnavailable("Harbor policy API is unavailable") from exc
with response:
body = response.read(MAX_RESPONSE + 1)
if len(body) > MAX_RESPONSE:
raise RuntimeError("Harbor response exceeded the size limit")
return int(response.status), body, dict(response.headers)
def list_rules(client: HarborClient) -> list[dict[str, Any]]:
"""Read and validate the complete small rule set for the project."""
path = f"/projects/{PROJECT}/immutabletagrules?page=1&page_size=100"
status, body, headers = client.request("GET", path)
if status in TRANSIENT_STATUSES:
raise HarborUnavailable(f"Harbor immutable rule list returned HTTP {status}")
if status != 200:
raise RuntimeError(f"Harbor immutable rule list returned HTTP {status}")
try:
values = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("Harbor returned invalid immutable rule JSON") from exc
if not isinstance(values, list) or not all(
isinstance(item, dict) for item in values
):
raise RuntimeError("Harbor immutable rule list has an invalid shape")
raw_total = next(
(value for key, value in headers.items() if key.lower() == "x-total-count"),
None,
)
if raw_total is None or not str(raw_total).isdecimal():
raise RuntimeError("Harbor immutable rule list omitted a valid total count")
if int(raw_total) != len(values):
raise RuntimeError("Harbor immutable rule list was truncated")
return values
def ensure_rule(client: HarborClient) -> int:
"""Create once, or validate the one exact enabled WebUI rule."""
rules = list_rules(client)
matches = [rule for rule in rules if targets_webui_builds(rule)]
if len(matches) > 1:
raise RuntimeError("multiple Hermes WebUI immutable rules exist")
if matches:
if normalized_rule(matches[0]) != EXPECTED_RULE:
raise RuntimeError("Hermes WebUI immutable rule is not enabled and exact")
rule_id = matches[0].get("id")
if not isinstance(rule_id, int) or rule_id < 1:
raise RuntimeError("Harbor immutable rule omitted a valid ID")
return rule_id
path = f"/projects/{PROJECT}/immutabletagrules"
status, _body, headers = client.request("POST", path, EXPECTED_RULE)
if status in TRANSIENT_STATUSES:
raise HarborUnavailable(f"Harbor immutable rule create returned HTTP {status}")
if status != 201:
raise RuntimeError(f"Harbor immutable rule create returned HTTP {status}")
location = headers.get("Location") or headers.get("location") or ""
api_path = urllib.parse.urlsplit(client.origin).path.rstrip("/")
expected_prefix = f"{api_path}{path}/"
if not location.startswith(expected_prefix):
raise RuntimeError("Harbor immutable rule create omitted the exact Location")
suffix = location[len(expected_prefix) :]
if not suffix.isdecimal() or int(suffix) < 1:
raise RuntimeError("Harbor immutable rule Location has an invalid ID")
for attempt in range(1, 6):
matches = [rule for rule in list_rules(client) if targets_webui_builds(rule)]
if len(matches) == 1 and normalized_rule(matches[0]) == EXPECTED_RULE:
rule_id = matches[0].get("id")
if rule_id == int(suffix):
return rule_id
if attempt < 5:
time.sleep(attempt)
raise RuntimeError("created Harbor immutable rule did not verify exactly")
def main() -> int:
"""Load the runtime-only admin credential and enforce tracked policy."""
origin = os.environ.get("HARBOR_API_ORIGIN", "")
password_file = Path(os.environ.get("HARBOR_ADMIN_PASSWORD_FILE", ""))
password = password_file.read_text(encoding="utf-8").strip()
if not password:
raise RuntimeError("Harbor admin password is empty")
client = HarborClient(origin, "admin", password)
for attempt in range(1, 13):
try:
rule_id = ensure_rule(client)
break
except HarborUnavailable:
if attempt == 12:
raise
time.sleep(min(attempt * 2, 15))
print(f"Hermes WebUI immutable build-tag rule is active (id={rule_id})")
return 0
if __name__ == "__main__": # pragma: no cover - exercised through main()
raise SystemExit(main())

View File

@ -671,6 +671,24 @@ data:
}
}
}
pipelineJob('hermes-webui-image') {
disabled(false)
description('Human-gated, daemonless Kaniko build for the reviewed atlas/titan-iac main revision. Publishes an immutable Hermes WebUI image and archives a narrow two-workload Flux digest patch; it never mutates Git or deploys.')
definition {
cpsScm {
scm {
git {
remote {
url('https://scm.bstein.dev/atlas/titan-iac.git')
credentials('gitea-pat')
}
branches('*/main')
}
}
scriptPath('ci/Jenkinsfile.hermes-webui-image')
}
}
}
multibranchPipelineJob('titan-iac-quality-gate') {
branchSources {
branchSource {

View File

@ -1,9 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<title>Hermes</title>
<link rel="icon" type="image/svg+xml" href="static/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32.png">
<link rel="shortcut icon" href="static/favicon.ico">
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<meta name="apple-mobile-web-app-title" content="Hermes">
<link rel="apple-touch-icon" sizes="512x512" href="static/apple-touch-icon.png">
<meta name="theme-color" content="#FAF7F0" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#141425" media="(prefers-color-scheme: dark)">
<meta name="theme-color" id="hermes-theme-color" content="#0D0D1A">
<script>var c=t==='dark'?'#141425':'#FAF7F0';</script>
<link rel="stylesheet" href="static/style.css?v=__WEBUI_VERSION__">
</head>
<body>
<header class="app-titlebar" role="banner">
<div class="app-titlebar-inner">
<span class="app-titlebar-icon" aria-hidden="true">
<svg><path d="fixture"/></svg>
</span>
<span class="app-titlebar-title" id="appTitlebarTitle">Hermes</span>
</div>
</header>
<select id="settingsTtsEngine"><option value="browser">Browser speech synthesis</option><option value="edge">Edge TTS (server)</option></select>
<div class="settings-field"><label for="settingsTtsVoice" data-i18n="settings_label_tts_voice">Voice</label>
<select id="settingsTtsVoice" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">

View 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"
}
]
}

View File

@ -0,0 +1,7 @@
const VQ = '?v=__WEBUI_VERSION__';
const SHELL_ASSETS = [
'./static/style.css' + VQ,
'./static/favicon.svg',
'./static/favicon-32.png',
'./manifest.json',
];

View File

@ -0,0 +1,320 @@
"""Canonical icon, PWA, and fail-closed Hermes WebUI identity contracts."""
from __future__ import annotations
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import struct
import subprocess
import sys
from urllib.parse import urlsplit
import pytest
ROOT = Path(__file__).resolve().parents[2]
DOCKERFILES = ROOT / "dockerfiles"
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"
EXPECTED_HASHES = {
"hermes-agent.ico": "aefe65e6574e6f46d3382588f46c508e1b3f3b3c9ce3dec6d335403a5374add9",
"hermes-agent-192.png": "0e4102cc715372058dd6ab55e9cde2567e46fee5ab6557b564cdd212ccd2616f",
"hermes-agent-512.png": "6661e5ca0ecc690af213b9f85f961541e6d3d0e36946ede9d3c2c97dfbd3c23d",
}
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"
shutil.copytree(FIXTURE, target)
shutil.copytree(AGENT_FIXTURE, agent_target)
env = os.environ.copy()
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
env["HERMES_AGENT_PATCH_ROOT"] = str(agent_target)
env["HERMES_WEBUI_MANIFEST_SOURCE"] = str(MANIFEST)
for patcher in (ATLAS_PATCHER, BRAND_PATCHER, MANIFEST_PATCHER):
subprocess.run(
[sys.executable, str(patcher)],
cwd=ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
return target
def _png_size(path: Path) -> tuple[int, int]:
payload = path.read_bytes()
assert payload.startswith(b"\x89PNG\r\n\x1a\n")
assert payload[12:16] == b"IHDR"
return struct.unpack(">II", payload[16:24])
def test_canonical_icon_provenance_format_and_pwa_derivatives() -> None:
"""The supplied persona is tracked exactly and only resized for PWA use."""
for name, expected in EXPECTED_HASHES.items():
payload = (ASSETS / name).read_bytes()
assert hashlib.sha256(payload).hexdigest() == expected
ico = (ASSETS / "hermes-agent.ico").read_bytes()
reserved, image_type, count = struct.unpack_from("<HHH", ico)
assert (reserved, image_type, count) == (0, 1, 3)
sizes = set()
for index in range(count):
offset = 6 + index * 16
width, height, _colors, _reserved, planes, depth, length, start = (
struct.unpack_from("<BBBBHHII", ico, offset)
)
sizes.add((width or 256, height or 256))
assert (planes, depth) == (1, 32)
assert ico[start : start + 8] == b"\x89PNG\r\n\x1a\n"
assert start + length <= len(ico)
assert sizes == {(16, 16), (32, 32), (48, 48)}
assert _png_size(ASSETS / "hermes-agent-192.png") == (192, 192)
assert _png_size(ASSETS / "hermes-agent-512.png") == (512, 512)
provenance = (ASSETS / "SOURCE.md").read_text(encoding="utf-8")
assert "/opt/hermes/web/public/favicon.ico" in provenance
assert EXPECTED_HASHES["hermes-agent.ico"] in provenance
assert "LANCZOS" in provenance
def test_manifest_is_installable_scoped_and_uses_only_canonical_persona() -> None:
"""The app has both mandatory icon sizes and no remote or secret-bearing data."""
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
assert manifest["id"] == "./"
assert manifest["name"] == "Hermes Chat"
assert manifest["short_name"] == "Hermes"
assert manifest["start_url"].startswith("./")
assert manifest["scope"] == "./"
assert manifest["display"] == "standalone"
assert manifest["background_color"] == "#070A12"
assert manifest["theme_color"] == "#0D1420"
icons = manifest["icons"]
assert {(icon["sizes"], icon["type"]) for icon in icons} == {
("192x192", "image/png"),
("512x512", "image/png"),
}
assert {icon["src"] for icon in icons} == {
"static/hermes-agent-192.png",
"static/hermes-agent-512.png",
}
assert all(icon["purpose"] == "any" for icon in icons)
serialized = json.dumps(manifest).lower()
assert "http:" not in serialized and "https:" not in serialized
assert "secret" not in serialized and "token" not in serialized
def test_production_patchers_apply_title_icons_theme_and_cache_contract(
tmp_path: Path,
) -> None:
"""Exercise the shipped patchers against pinned upstream source fragments."""
target = _patched_fixture(tmp_path)
index = (target / "static/index.html").read_text(encoding="utf-8")
worker = (target / "static/sw.js").read_text(encoding="utf-8")
assert index.count("<title>Hermes Chat</title>") == 1
assert index.count('id="hermesBrandStyles"') == 1
assert 'href="static/hermes-agent.ico"' in index
assert 'sizes="192x192" href="static/hermes-agent-192.png"' in index
assert 'sizes="512x512" href="static/hermes-agent-512.png"' in index
assert '<meta name="apple-mobile-web-app-title" content="Hermes Chat">' in index
assert '<meta name="theme-color" content="#0D1420"' in index
assert 'id="appTitlebarTitle">Hermes Chat</span>' in index
assert '<img src="static/hermes-agent-192.png" alt="">' in index
assert "favicon.svg" not in index
assert "favicon-32.png" not in index
assert worker.count("'./static/hermes-brand.css' + VQ") == 1
for name in EXPECTED_HASHES:
assert worker.count(f"'./static/{name}'") == 1
assert "favicon.svg" not in worker
assert "favicon-32.png" not in worker
def test_brand_patch_rejects_upstream_drift_before_partial_success(
tmp_path: Path,
) -> None:
"""A changed pinned title/favicon context cannot silently ship partial branding."""
target = _patched_fixture(tmp_path)
index = target / "static/index.html"
index.write_text(
index.read_text(encoding="utf-8").replace(
"<title>Hermes Chat</title>", "<title>Upstream changed</title>", 1
),
encoding="utf-8",
)
env = os.environ.copy()
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
result = subprocess.run(
[sys.executable, str(BRAND_PATCHER)],
cwd=ROOT,
env=env,
check=False,
capture_output=True,
text=True,
)
assert result.returncode != 0
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")
dark = css.split(":root.dark {", 1)[1]
assert "color-scheme: dark" in dark
assert "--bg: #070a12" in dark
assert "--text: #e8f1f4" in dark
assert "--accent: #48cfcc" in dark
assert "--voice-accent: 72, 207, 204" in css
assert "--voice-accent-secondary: 76, 164, 205" in css
assert "@media (prefers-reduced-motion: reduce)" in css
reduced = css.split("@media (prefers-reduced-motion: reduce)", 1)[1]
assert "transition: none !important" in reduced
assert "animation:" not in css
def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
"""The immutable image, not a runtime coordinator path, owns PWA assets."""
dockerfile = (DOCKERFILES / "Dockerfile.hermes-webui").read_text(encoding="utf-8")
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-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
assert digest in dockerfile

View File

@ -0,0 +1,479 @@
"""Independent build, Harbor evidence, and Flux handoff for Hermes WebUI."""
from __future__ import annotations
import importlib.util
import io
import json
from pathlib import Path
import sys
import pytest
import yaml
ROOT = Path(__file__).resolve().parents[2]
PIPELINE = ROOT / "ci/Jenkinsfile.hermes-webui-image"
RELEASE = ROOT / "ci/scripts/hermes_webui_release.py"
DOCKERFILE = ROOT / "dockerfiles/Dockerfile.hermes-webui"
CHAT = ROOT / "services/hermes/chat-statefulset.yaml"
DASHBOARD = ROOT / "services/hermes/deployment.yaml"
POLICY = ROOT / "services/harbor/scripts/harbor_hermes_webui_immutability_ensure.py"
sys.path.insert(0, str(RELEASE.parent))
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 _pod_spec() -> dict:
source = PIPELINE.read_text(encoding="utf-8")
pod_yaml = source.split('yaml """', 1)[1].split('"""', 1)[0]
return yaml.safe_load(pod_yaml)["spec"]
def _release_fixture(tmp_path: Path):
module = _load(RELEASE, f"hermes_webui_release_{tmp_path.name}")
digest = "sha256:" + "7" * 64
revision = "8" * 40
build = "23"
destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-{build}"
digest_file = tmp_path / "hermes-webui.digest"
image_file = tmp_path / "hermes-webui.image"
digest_file.write_text(digest + "\n", encoding="utf-8")
image_file.write_text(f"{destination}@{digest}\n", encoding="utf-8")
output = tmp_path / "release"
kwargs = {
"digest_file": digest_file,
"image_file": image_file,
"source_revision": revision,
"build_number": build,
"destination": destination,
"chat_manifest": CHAT,
"dashboard_manifest": DASHBOARD,
"output_dir": output,
}
module.write_release_artifacts(
digest=digest,
source_revision=revision,
build_number=build,
destination=destination,
chat_manifest=CHAT,
dashboard_manifest=DASHBOARD,
output_dir=output,
)
return module, digest, kwargs
def test_webui_job_is_independent_manual_and_main_only() -> None:
"""WebUI has its own job and never widens the existing agent-only lane."""
config = yaml.safe_load(
(ROOT / "services/jenkins/configmap-jcasc.yaml").read_text(encoding="utf-8")
)
jobs = config["data"]["jobs.yaml"]
assert jobs.count("pipelineJob('hermes-agent-image')") == 1
assert jobs.count("pipelineJob('hermes-webui-image')") == 1
block = jobs.split("pipelineJob('hermes-webui-image')", 1)[1].split(
"multibranchPipelineJob(", 1
)[0]
assert "branches('*/main')" in block
assert "scriptPath('ci/Jenkinsfile.hermes-webui-image')" in block
assert "pipelineTriggers" not in block
assert "HERMES_AGENT_IMAGE_BUILD_TOKEN" not in block
def test_pipeline_builds_exact_reviewed_main_and_never_deploys() -> None:
"""Publish is explicit, immutable, evidence-producing, and Git/Flux review only."""
source = PIPELINE.read_text(encoding="utf-8")
assert 'test "${CONFIRM_PUBLISH}" = "PUBLISH HERMES WEBUI"' in source
assert 'test "${actual_revision}" = "$(git rev-parse origin/main)"' in source
assert "dockerfiles/Dockerfile.hermes-webui" in source
assert "ci/scripts/hermes_webui_release.py" in source
assert "registry.bstein.dev/bstein/hermes-webui" in source
assert "Dockerfile.hermes-agent" not in source
assert "hermes_image_release.py" not in source
assert "HERMES_KANIKO_HEREDOC_COMPAT" not in source
assert "--digest-file=" in source
assert "--image-name-tag-with-digest-file=" in source
assert "org.opencontainers.image.revision=${EXPECTED_SOURCE_REVISION}" in source
assert "assert-absent" in source and "verify-evidence" in source
assert "test_hermes_webui_brand.py" in source
assert "test_hermes_webui_release.py" in source
for forbidden in ("kubectl ", "flux reconcile", "git push", "git commit"):
assert forbidden not in source
spec = _pod_spec()
assert spec["serviceAccountName"] == "hermes-image-builder"
assert spec["automountServiceAccountToken"] is False
assert spec["enableServiceLinks"] is False
containers = {item["name"]: item for item in spec["containers"]}
assert "kaniko" in containers
assert "docker.sock" not in source and "hostPath" not in source
for container in containers.values():
assert container["securityContext"]["allowPrivilegeEscalation"] is False
assert container["securityContext"]["capabilities"]["drop"] == ["ALL"]
def test_webui_dockerfile_is_kaniko_safe_and_uses_reviewed_repo_source() -> None:
"""The dedicated build consumes tracked patches/assets without RUN heredocs."""
source = DOCKERFILE.read_text(encoding="utf-8")
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:
"""One digest patch spans the two same-policy WebUI consumers and nothing else."""
module, digest, kwargs = _release_fixture(tmp_path)
output = kwargs["output_dir"]
patch = (output / "hermes-webui-image-update.patch").read_text(encoding="utf-8")
assert patch.count(f"+ image: {module.DEFAULT_IMAGE}@{digest}") == 2
assert "services/hermes/chat-statefulset.yaml" in patch
assert "services/hermes/deployment.yaml" in patch
assert "hermes-agent@sha256" not in "\n".join(
line for line in patch.splitlines() if line.startswith("+")
)
assert CHAT.read_text(encoding="utf-8") != (
output / "hermes-chat-statefulset.yaml"
).read_text(encoding="utf-8")
assert DASHBOARD.read_text(encoding="utf-8") != (
output / "hermes-dashboard-deployment.yaml"
).read_text(encoding="utf-8")
metadata = json.loads((output / "hermes-webui-image.json").read_text())
assert metadata["digest"] == digest
assert metadata["flux_image"] == f"{module.DEFAULT_IMAGE}@{digest}"
assert metadata["flux_targets"] == [
"apps/StatefulSet/hermes/hermes-chat-tenant",
"apps/Deployment/hermes/hermes",
]
module.validate_release_artifacts(**kwargs)
@pytest.mark.parametrize(
("source", "kind", "name", "match"),
[
(
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wrong\n",
"Deployment",
"hermes",
"identity changed",
),
(
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: hermes\n",
"Deployment",
"hermes",
"found 0",
),
(
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: hermes\n"
"spec:\n image: registry.bstein.dev/bstein/hermes-webui:latest\n",
"Deployment",
"hermes",
"found 0",
),
],
)
def test_renderer_fails_closed_on_flux_target_drift(
source: str, kind: str, name: str, match: str
) -> None:
module = _load(RELEASE, f"webui_renderer_{abs(hash(source))}")
with pytest.raises(ValueError, match=match):
module.render_workload(source, "sha256:" + "a" * 64, kind=kind, name=name)
def test_evidence_revalidation_rejects_extra_or_changed_files(tmp_path: Path) -> None:
"""Archived output is an exact deterministic set, not a best-effort bundle."""
module, _digest, kwargs = _release_fixture(tmp_path)
module.validate_release_artifacts(**kwargs)
extra = kwargs["output_dir"] / "unexpected"
extra.write_text("surprise\n", encoding="utf-8")
with pytest.raises(ValueError, match="exactly four"):
module.validate_release_artifacts(**kwargs)
extra.unlink()
metadata = kwargs["output_dir"] / "hermes-webui-image.json"
metadata.write_text("{}\n", encoding="utf-8")
with pytest.raises(ValueError, match="incomplete or mismatched"):
module.validate_release_artifacts(**kwargs)
class _Response(io.BytesIO):
status = 200
def __init__(self, body: bytes, headers: dict[str, str] | None = None):
super().__init__(body)
self.headers = headers or {}
def __enter__(self):
return self
def __exit__(self, *_args):
self.close()
def test_release_verifies_exact_webui_harbor_artifact_and_policy() -> None:
"""Independent evidence resolves the tag and exact WebUI immutability rule."""
module = _load(RELEASE, "webui_registry_contract")
revision = "a" * 40
destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-9"
digest = "sha256:" + "b" * 64
seen = []
def artifact_open(request, timeout):
seen.append((request, timeout))
return _Response(
json.dumps(
{
"digest": digest,
"tags": [{"name": f"git-{revision}-build-9", "immutable": True}],
"extra_attrs": {
"config": {
"Labels": {
"org.opencontainers.image.revision": revision,
}
}
},
}
).encode()
)
module.verify_registry_digest(
destination,
digest,
revision,
username="robot",
password="private",
opener=artifact_open,
)
request = seen[0][0]
assert "/repositories/hermes-webui/artifacts/" in request.full_url
assert request.full_url.startswith("https://registry.bstein.dev/api/v2.0/")
assert request.get_header("Authorization").startswith("Basic ")
assert seen[0][1] == 20
def policy_open(request, timeout):
assert request.full_url.endswith(
"/projects/bstein/immutabletagrules?page=1&page_size=100"
)
return _Response(
json.dumps(
[
{
"disabled": False,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "git-*-build-*",
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "hermes-webui",
}
]
},
}
]
).encode(),
{"X-Total-Count": "1"},
)
module.verify_immutable_policy(
username="robot", password="private", opener=policy_open
)
@pytest.mark.parametrize("labels", [None, {}, {"org.opencontainers.image.revision": "c" * 40}])
def test_release_rejects_missing_or_wrong_harbor_source_revision(labels) -> None:
"""A tag derived from Git cannot substitute for the persisted OCI label."""
module = _load(RELEASE, f"webui_registry_revision_{labels!r}")
revision = "a" * 40
destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-9"
digest = "sha256:" + "b" * 64
def artifact_open(_request, _timeout):
config = {} if labels is None else {"Labels": labels}
return _Response(
json.dumps(
{
"digest": digest,
"tags": [{"name": f"git-{revision}-build-9", "immutable": True}],
"extra_attrs": {"config": config},
}
).encode()
)
with pytest.raises(RuntimeError, match="OCI (image labels|source-revision label)"):
module.verify_registry_digest(
destination,
digest,
revision,
username="robot",
password="private",
opener=artifact_open,
)
def test_flux_tracks_webui_policy_before_jenkins() -> None:
"""The immutable Harbor rule is reviewed desired state, not a pipeline wish."""
harbor = yaml.safe_load(
(
ROOT / "clusters/atlas/flux-system/applications/harbor/kustomization.yaml"
).read_text(encoding="utf-8")
)
checks = harbor["spec"]["healthChecks"]
assert {
"apiVersion": "batch/v1",
"kind": "Job",
"name": "harbor-hermes-webui-immutability-ensure-1",
"namespace": "harbor",
} in checks
jenkins = yaml.safe_load(
(
ROOT / "clusters/atlas/flux-system/applications/jenkins/kustomization.yaml"
).read_text(encoding="utf-8")
)
assert "harbor" in {item["name"] for item in jenkins["spec"]["dependsOn"]}
policy = _load(POLICY, "webui_policy_contract")
assert policy.REPOSITORY_PATTERN == "hermes-webui"
assert policy.TAG_PATTERN == "git-*-build-*"
assert policy.EXPECTED_RULE["disabled"] is False
assert "robot" not in POLICY.read_text(encoding="utf-8").lower()
job = yaml.safe_load(
(ROOT / "services/harbor/hermes-webui-immutability-job.yaml").read_text(
encoding="utf-8"
)
)
template = job["spec"]["template"]
annotations = template["metadata"]["annotations"]
assert annotations["vault.hashicorp.com/role"] == "harbor-policy-bootstrap"
assert (
annotations["vault.hashicorp.com/agent-inject-secret-harbor-admin-password"]
== "kv/data/atlas/harbor/harbor-core"
)
pod = template["spec"]
assert pod["serviceAccountName"] == "harbor-policy-bootstrap"
assert pod["enableServiceLinks"] is False
container = pod["containers"][0]
assert "@sha256:" in container["image"]
assert container["securityContext"]["readOnlyRootFilesystem"] is True
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"
def __init__(self, responses):
self.responses = list(responses)
self.calls = []
def request(self, method, path, payload=None):
self.calls.append((method, path, payload))
return self.responses.pop(0)
def test_webui_policy_is_idempotent_and_create_is_reread() -> None:
"""The desired Harbor rule validates in place or verifies its exact new ID."""
policy = _load(POLICY, "webui_policy_idempotency")
existing = {"id": 17, **policy.EXPECTED_RULE}
client = _FakePolicyClient(
[(200, json.dumps([existing]).encode(), {"X-Total-Count": "1"})]
)
assert policy.ensure_rule(client) == 17
assert [call[0] for call in client.calls] == ["GET"]
created = {"id": 23, **policy.EXPECTED_RULE}
client = _FakePolicyClient(
[
(200, b"[]", {"X-Total-Count": "0"}),
(
201,
b"",
{"Location": ("/api/v2.0/projects/bstein/immutabletagrules/23")},
),
(200, json.dumps([created]).encode(), {"X-Total-Count": "1"}),
]
)
assert policy.ensure_rule(client) == 23
assert client.calls[1] == (
"POST",
"/projects/bstein/immutabletagrules",
policy.EXPECTED_RULE,
)
def test_webui_policy_rejects_disabled_duplicate_or_truncated_rules() -> None:
"""Ambiguous or incomplete Harbor evidence can never unblock publication."""
policy = _load(POLICY, "webui_policy_rejections")
disabled = {"id": 17, **policy.EXPECTED_RULE, "disabled": True}
with pytest.raises(RuntimeError, match="not enabled and exact"):
policy.ensure_rule(
_FakePolicyClient(
[
(
200,
json.dumps([disabled]).encode(),
{"X-Total-Count": "1"},
)
]
)
)
duplicate = [
{"id": 17, **policy.EXPECTED_RULE},
{"id": 18, **policy.EXPECTED_RULE},
]
with pytest.raises(RuntimeError, match="multiple Hermes WebUI"):
policy.ensure_rule(
_FakePolicyClient(
[
(
200,
json.dumps(duplicate).encode(),
{"X-Total-Count": "2"},
)
]
)
)
with pytest.raises(RuntimeError, match="truncated"):
policy.list_rules(_FakePolicyClient([(200, b"[]", {"X-Total-Count": "1"})]))
def test_pipeline_archives_exact_seven_files() -> None:
"""Publication cannot pass with missing digest, workload, or metadata evidence."""
source = PIPELINE.read_text(encoding="utf-8")
post = source.split(" post {", 1)[1]
archive = post.split("artifacts: '", 1)[1].split("'", 1)[0].split(",")
assert len(archive) == len(set(archive)) == 7
assert all("*" not in path for path in archive)
assert "find build -type f" in post
assert "allowEmptyArchive: false" in post
assert "hermes-chat-statefulset.yaml" in post
assert "hermes-dashboard-deployment.yaml" in post