2026-08-23 13:41:58 -03:00
|
|
|
"""Contracts for automatic deployment of validated Hermes image releases."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
2026-08-24 04:12:03 -03:00
|
|
|
import re
|
2026-08-23 13:41:58 -03:00
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
SERVICE = ROOT / "services/hermes"
|
|
|
|
|
APPLICATIONS = ROOT / "clusters/atlas/flux-system/applications"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_image_policies_observe_only_validated_release_tags() -> None:
|
|
|
|
|
"""Candidates remain invisible until Jenkins publishes the release suffix."""
|
|
|
|
|
documents = list(
|
|
|
|
|
yaml.safe_load_all((SERVICE / "image.yaml").read_text(encoding="utf-8"))
|
|
|
|
|
)
|
|
|
|
|
repositories = {
|
|
|
|
|
item["metadata"]["name"]: item
|
|
|
|
|
for item in documents
|
|
|
|
|
if item["kind"] == "ImageRepository"
|
|
|
|
|
}
|
|
|
|
|
policies = {
|
|
|
|
|
item["metadata"]["name"]: item
|
|
|
|
|
for item in documents
|
|
|
|
|
if item["kind"] == "ImagePolicy"
|
|
|
|
|
}
|
2026-08-23 15:54:56 -03:00
|
|
|
assert set(repositories) == {
|
|
|
|
|
"hermes-agent-release",
|
2026-08-24 02:46:36 -03:00
|
|
|
"hermes-chat-router-release",
|
2026-08-23 15:54:56 -03:00
|
|
|
"hermes-webui-release",
|
|
|
|
|
"hermes-stt-release",
|
|
|
|
|
"hermes-tts-release",
|
|
|
|
|
}
|
2026-08-23 13:41:58 -03:00
|
|
|
assert set(policies) == set(repositories)
|
|
|
|
|
for name, policy in policies.items():
|
|
|
|
|
assert policy["metadata"]["namespace"] == "hermes"
|
|
|
|
|
assert policy["spec"]["imageRepositoryRef"]["name"] == name
|
|
|
|
|
assert policy["spec"]["filterTags"] == {
|
|
|
|
|
"pattern": ("^git-[0-9a-f]{40}-build-" "(?P<build>[1-9][0-9]*)-release$"),
|
|
|
|
|
"extract": "$build",
|
|
|
|
|
}
|
|
|
|
|
assert policy["spec"]["policy"] == {"numerical": {"order": "asc"}}
|
|
|
|
|
assert policy["spec"]["digestReflectionPolicy"] == "Always"
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 15:54:56 -03:00
|
|
|
def test_flux_updates_only_the_reviewed_hermes_image_digests() -> None:
|
2026-08-23 13:41:58 -03:00
|
|
|
"""Flux persists selected digests to Git and rolls all matching workloads."""
|
|
|
|
|
service_kustomization = (SERVICE / "kustomization.yaml").read_text(encoding="utf-8")
|
|
|
|
|
applications_kustomization = (APPLICATIONS / "kustomization.yaml").read_text(
|
|
|
|
|
encoding="utf-8"
|
|
|
|
|
)
|
|
|
|
|
automation = yaml.safe_load(
|
|
|
|
|
(APPLICATIONS / "hermes/image-automation.yaml").read_text(encoding="utf-8")
|
|
|
|
|
)
|
|
|
|
|
agent = (SERVICE / "kustomization.yaml").read_text(encoding="utf-8")
|
|
|
|
|
chat = (SERVICE / "chat-statefulset.yaml").read_text(encoding="utf-8")
|
|
|
|
|
dashboard = (SERVICE / "deployment.yaml").read_text(encoding="utf-8")
|
2026-08-23 15:54:56 -03:00
|
|
|
voice = (SERVICE / "voice-deployment.yaml").read_text(encoding="utf-8")
|
2026-08-24 02:46:36 -03:00
|
|
|
router = (SERVICE / "chat-router.yaml").read_text(encoding="utf-8")
|
2026-08-23 13:41:58 -03:00
|
|
|
|
|
|
|
|
assert " - image.yaml" in service_kustomization
|
|
|
|
|
assert " - hermes/image-automation.yaml" in applications_kustomization
|
|
|
|
|
assert automation["spec"]["git"]["checkout"]["ref"]["branch"] == "main"
|
|
|
|
|
assert automation["spec"]["git"]["push"]["branch"] == "main"
|
|
|
|
|
assert automation["spec"]["update"] == {
|
|
|
|
|
"strategy": "Setters",
|
|
|
|
|
"path": "services/hermes",
|
|
|
|
|
}
|
|
|
|
|
assert agent.count('"$imagepolicy": "hermes:hermes-agent-release:digest"') == 1
|
2026-08-23 14:22:26 -03:00
|
|
|
webui_marker = '"$imagepolicy": "hermes:hermes-webui-release"'
|
2026-08-24 04:17:02 -03:00
|
|
|
chat_object = yaml.safe_load(chat)
|
hermes(chat): stage the HUX-12 evidence producer sidecar
Activation-layer staging, fail-closed until enablement: a per-tenant
hux-evidence-producer sidecar on the exact reviewed WebUI image runs
hux_producer.run_once on a 60s loop, inert until the Vault-staged
evidence key (tolerant init, tmpfs, 0400, staged only for the hux
service and producer containers - never hermes or webui), the policy
ConfigMap, and the scope ConfigMap exist. Adds least-privilege
read-only RBAC (pods+statefulset in hermes, the single named Flux
Kustomization), tenant egress to the Kubernetes API ClusterIP and the
traefik edge, the policy allowlist, hux_producer packaging in the WebUI
image, and a third expected WebUI consumer in the Flux release
renderer. Delivery and image-automation gates enforce the boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:39:45 -03:00
|
|
|
containers = chat_object["spec"]["template"]["spec"]["containers"]
|
|
|
|
|
hux = next((item for item in containers if item["name"] == "hux"), None)
|
|
|
|
|
producer = next(
|
|
|
|
|
(item for item in containers if item["name"] == "hux-evidence-producer"),
|
2026-08-24 04:17:02 -03:00
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
# Before activation the chat StatefulSet carries exactly one WebUI
|
hermes(chat): stage the HUX-12 evidence producer sidecar
Activation-layer staging, fail-closed until enablement: a per-tenant
hux-evidence-producer sidecar on the exact reviewed WebUI image runs
hux_producer.run_once on a 60s loop, inert until the Vault-staged
evidence key (tolerant init, tmpfs, 0400, staged only for the hux
service and producer containers - never hermes or webui), the policy
ConfigMap, and the scope ConfigMap exist. Adds least-privilege
read-only RBAC (pods+statefulset in hermes, the single named Flux
Kustomization), tenant egress to the Kubernetes API ClusterIP and the
traefik edge, the policy allowlist, hux_producer packaging in the WebUI
image, and a third expected WebUI consumer in the Flux release
renderer. Delivery and image-automation gates enforce the boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:39:45 -03:00
|
|
|
# consumer and no HUX setters; after activation the HUX sidecar (and the
|
|
|
|
|
# HUX-12 evidence producer) consume the very same reviewed image line,
|
|
|
|
|
# with tag and digest setters binding the sidecar's build metadata.
|
|
|
|
|
assert producer is None or hux is not None
|
|
|
|
|
consumers = 1 + (1 if hux else 0) + (1 if producer else 0)
|
|
|
|
|
assert chat.count(webui_marker) == consumers
|
2026-08-23 13:41:58 -03:00
|
|
|
assert dashboard.count(webui_marker) == 1
|
2026-08-24 04:12:03 -03:00
|
|
|
assert chat.count(
|
|
|
|
|
'"$imagepolicy": "hermes:hermes-webui-release:tag"'
|
2026-08-24 04:17:02 -03:00
|
|
|
) == (1 if hux else 0)
|
2026-08-24 04:12:03 -03:00
|
|
|
assert chat.count(
|
|
|
|
|
'"$imagepolicy": "hermes:hermes-webui-release:digest"'
|
2026-08-24 04:17:02 -03:00
|
|
|
) == (1 if hux else 0)
|
|
|
|
|
if hux:
|
|
|
|
|
hux_env = {
|
|
|
|
|
item["name"]: item["value"] for item in hux["env"] if "value" in item
|
|
|
|
|
}
|
|
|
|
|
release = re.fullmatch(
|
|
|
|
|
r"git-([0-9a-f]{40})-build-[1-9][0-9]*-release",
|
|
|
|
|
hux_env["HUX_IMAGE_TAG"],
|
|
|
|
|
)
|
|
|
|
|
assert release is not None
|
|
|
|
|
assert hux["image"].split(":git-", 1)[1].split("@", 1)[0] == hux_env[
|
|
|
|
|
"HUX_IMAGE_TAG"
|
|
|
|
|
].removeprefix("git-")
|
|
|
|
|
assert hux["image"].endswith("@" + hux_env["HUX_IMAGE_DIGEST"])
|
hermes(chat): stage the HUX-12 evidence producer sidecar
Activation-layer staging, fail-closed until enablement: a per-tenant
hux-evidence-producer sidecar on the exact reviewed WebUI image runs
hux_producer.run_once on a 60s loop, inert until the Vault-staged
evidence key (tolerant init, tmpfs, 0400, staged only for the hux
service and producer containers - never hermes or webui), the policy
ConfigMap, and the scope ConfigMap exist. Adds least-privilege
read-only RBAC (pods+statefulset in hermes, the single named Flux
Kustomization), tenant egress to the Kubernetes API ClusterIP and the
traefik edge, the policy allowlist, hux_producer packaging in the WebUI
image, and a third expected WebUI consumer in the Flux release
renderer. Delivery and image-automation gates enforce the boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:39:45 -03:00
|
|
|
if producer is not None:
|
|
|
|
|
assert producer["image"] == hux["image"]
|
2026-08-23 14:22:26 -03:00
|
|
|
# A digest-only setter replaces the complete YAML scalar with ``sha256:...``.
|
|
|
|
|
# Whole-image setters must retain the registry and repository in pod specs.
|
|
|
|
|
for workload in (chat, dashboard):
|
|
|
|
|
marked_line = next(
|
|
|
|
|
line for line in workload.splitlines() if webui_marker in line
|
|
|
|
|
)
|
|
|
|
|
assert "registry.bstein.dev/bstein/hermes-webui:" in marked_line
|
|
|
|
|
assert "@sha256:" in marked_line
|
2026-08-23 15:54:56 -03:00
|
|
|
for component in ("stt", "tts"):
|
|
|
|
|
marker = f'"$imagepolicy": "hermes:hermes-{component}-release"'
|
|
|
|
|
assert voice.count(marker) == 1
|
|
|
|
|
marked_line = next(line for line in voice.splitlines() if marker in line)
|
2026-08-23 16:55:20 -03:00
|
|
|
assert f"registry.bstein.dev/bstein/hermes-jetson-{component}" in marked_line
|
|
|
|
|
assert "@sha256:" in marked_line
|
2026-08-24 02:46:36 -03:00
|
|
|
router_marker = '"$imagepolicy": "hermes:hermes-chat-router-release"'
|
|
|
|
|
assert router.count(router_marker) == 1
|
|
|
|
|
marked_line = next(line for line in router.splitlines() if router_marker in line)
|
|
|
|
|
assert "registry.bstein.dev/bstein/hermes-chat-router" in marked_line
|
|
|
|
|
assert "@sha256:" in marked_line
|