"""Safety and artifact contracts for the Hermes agent image release lane.""" from __future__ import annotations import importlib.util import io import json import urllib.parse from pathlib import Path import pytest import yaml REPO_ROOT = Path(__file__).resolve().parents[2] PIPELINE_PATH = REPO_ROOT / "ci/Jenkinsfile.hermes-agent-image" RELEASE_SCRIPT = REPO_ROOT / "ci/scripts/hermes_image_release.py" TRIGGER_SCRIPT = ( REPO_ROOT / "services/hermes/scripts/jenkins_image_build_trigger.py" ) def _load_release_module(): spec = importlib.util.spec_from_file_location("hermes_image_release", RELEASE_SCRIPT) 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 _load_trigger_module(): spec = importlib.util.spec_from_file_location("jenkins_image_build_trigger", TRIGGER_SCRIPT) 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_PATH.read_text(encoding="utf-8") pod_yaml = source.split('yaml """', 1)[1].split('"""', 1)[0] return yaml.safe_load(pod_yaml)["spec"] def test_builder_pod_is_daemonless_and_kernel_unprivileged() -> None: """The builder must not gain host, daemon, service-token, or Linux privileges.""" source = PIPELINE_PATH.read_text(encoding="utf-8") spec = _pod_spec() assert spec["automountServiceAccountToken"] is False assert spec["enableServiceLinks"] is False assert "hostPath" not in source assert "docker.sock" not in source assert "tcp://" not in source assert "buildkitd" not in source.lower() assert "dind" not in source.lower() containers = {item["name"]: item for item in spec["containers"]} kaniko = containers["kaniko"] assert kaniko["image"] == ( "gcr.io/kaniko-project/executor@sha256:" "c3109d5926a997b100c4343944e06c6b30a6804b2f9abe0994d3de6ef92b028e" ) for container in containers.values(): security = container["securityContext"] assert security["allowPrivilegeEscalation"] is False assert security["capabilities"]["drop"] == ["ALL"] assert security["seccompProfile"]["type"] == "RuntimeDefault" assert security.get("privileged", False) is False def test_builder_is_restricted_to_healthy_rpi5_capacity() -> None: """Disposable builds must stay off unhealthy and reserved Atlas nodes.""" spec = _pod_spec() assert spec["nodeSelector"]["hardware"] == "rpi5" expressions = spec["affinity"]["nodeAffinity"][ "requiredDuringSchedulingIgnoredDuringExecution" ]["nodeSelectorTerms"][0]["matchExpressions"] host_rule = next(rule for rule in expressions if rule["key"] == "kubernetes.io/hostname") assert host_rule["operator"] == "NotIn" assert set(host_rule["values"]) >= { "titan-04", "titan-14", "titan-18", "titan-19", "titan-22", "titan-24", } def test_pipeline_requires_reviewed_main_and_runtime_credentials() -> None: """Publishing requires explicit confirmation and a reviewed main commit.""" source = PIPELINE_PATH.read_text(encoding="utf-8") assert 'test "${PUBLISH_IMAGE}" = "true"' in source assert 'test "${CONFIRM_PUBLISH}" = "PUBLISH HERMES AGENT"' in source assert 'git rev-parse origin/main' in source assert "credentialsId: 'harbor-robot'" in source assert "set +x" in source assert "umask 077" in source assert "unset HARBOR_USER HARBOR_PASSWORD auth" in source assert "/busybox/rm -f /kaniko/.docker/config.json" in source assert "--digest-file=" in source assert "--image-name-with-digest-file=" in source assert "--destination=" in source def test_jenkins_job_is_manual_and_reads_pipeline_from_main() -> None: """JCasC must not publish unreviewed branch contents or poll automatically.""" config = yaml.safe_load( (REPO_ROOT / "services/jenkins/configmap-jcasc.yaml").read_text( encoding="utf-8" ) ) jobs = config["data"]["jobs.yaml"] block = jobs.split("pipelineJob('hermes-agent-image')", 1)[1].split( "pipelineJob(", 1 )[0] assert "branches('*/main')" in block assert "scriptPath('ci/Jenkinsfile.hermes-agent-image')" in block assert "authenticationToken(System.getenv('HERMES_AGENT_IMAGE_BUILD_TOKEN'))" in block assert "pipelineTriggers" not in block assert "scmTrigger" not in block def test_agent_trigger_is_limited_to_the_image_job(tmp_path: Path) -> None: """Agent Hermes gets one job token, fixed parameters, and no Jenkins admin API.""" module = _load_trigger_module() revision = "a" * 40 token_path = tmp_path / "token" token_path.write_text("private-job-token\n", encoding="utf-8") captured = {} class Response(io.BytesIO): status = 201 headers = {"Location": "http://jenkins/queue/item/42/"} def __enter__(self): return self def __exit__(self, *_args): self.close() def opener(request, timeout): captured["request"] = request captured["timeout"] = timeout return Response(b"") result = module.trigger_build(revision, token_file=token_path, opener=opener) request = captured["request"] fields = urllib.parse.parse_qs(request.data.decode("utf-8")) assert request.full_url == module.JENKINS_BUILD_URL assert fields == { "CONFIRM_PUBLISH": ["PUBLISH HERMES AGENT"], "EXPECTED_SOURCE_REVISION": [revision], "PUBLISH_IMAGE": ["true"], "job": ["hermes-agent-image"], "token": ["private-job-token"], } assert captured["timeout"] == 20 assert "private-job-token" not in json.dumps(result) assert result["source_revision"] == revision def test_agent_trigger_rejects_unsafe_revision_and_empty_token(tmp_path: Path) -> None: """No user-controlled job, URL, or abbreviated revision reaches Jenkins.""" module = _load_trigger_module() token_path = tmp_path / "token" token_path.write_text("token\n", encoding="utf-8") with pytest.raises(ValueError): module.trigger_build("main", token_file=token_path) token_path.write_text("\n", encoding="utf-8") with pytest.raises(RuntimeError, match="empty"): module.trigger_build("a" * 40, token_file=token_path) def test_agent_trigger_accepts_existing_queue_redirect(tmp_path: Path) -> None: """HTTP 303 means the exact release is already queued, not a trigger failure.""" module = _load_trigger_module() token_path = tmp_path / "token" token_path.write_text("token\n", encoding="utf-8") class Response(io.BytesIO): status = 303 headers = {"Location": "http://jenkins/queue/item/7/"} def __enter__(self): return self def __exit__(self, *_args): self.close() result = module.trigger_build( "e" * 40, token_file=token_path, opener=lambda *_args, **_kwargs: Response() ) assert result["status"] == 303 assert result["queue_path"] == "/queue/item/7/" def test_job_token_is_generated_and_injected_only_at_runtime() -> None: """The fixed-job credential stays in Vault and pod-lifetime memory.""" plugins = (REPO_ROOT / "services/jenkins/configmap-plugins.yaml").read_text( encoding="utf-8" ) vault = ( REPO_ROOT / "services/vault/scripts/vault_k8s_auth_configure.sh" ).read_text(encoding="utf-8") jenkins = (REPO_ROOT / "services/jenkins/deployment.yaml").read_text( encoding="utf-8" ) agent = (REPO_ROOT / "services/hermes/agent-deployment.yaml").read_text( encoding="utf-8" ) stage = ( REPO_ROOT / "services/hermes/scripts/stage_runtime_access.py" ).read_text(encoding="utf-8") assert "build-token-root:365.v717f8685a_09e" in plugins assert "sys/tools/random/32 format=hex" in vault assert "kv/atlas/hermes/developer-jenkins" in vault assert "HERMES_AGENT_IMAGE_BUILD_TOKEN={{ .Data.data.build_token }}" in jenkins assert "agent-inject-secret-jenkins-image-build-token" in agent assert '"jenkins-image-build-token"' in stage def test_release_renderer_preserves_manifest_and_emits_safe_artifacts(tmp_path: Path) -> None: """The release artifact is exact, reviewable, and contains no credentials.""" module = _load_release_module() old_digest = "sha256:" + "1" * 64 new_digest = "sha256:" + "2" * 64 revision = "a" * 40 manifest = tmp_path / "kustomization.yaml" manifest.write_text( "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "images:\n" f" - name: {module.DEFAULT_IMAGE}\n" f" digest: {old_digest}\n", encoding="utf-8", ) output = tmp_path / "out" metadata = module.write_release_artifacts( digest=f"{new_digest}\n", source_revision=revision, destination=f"{module.DEFAULT_IMAGE}:git-{revision}", kustomization=manifest, output_dir=output, ) assert manifest.read_text(encoding="utf-8").endswith(f"{old_digest}\n") assert (output / "hermes-kustomization.yaml").read_text( encoding="utf-8" ).endswith(f"{new_digest}\n") patch = (output / "hermes-image-update.patch").read_text(encoding="utf-8") assert f"- digest: {old_digest}" in patch assert f"+ digest: {new_digest}" in patch assert json.loads( (output / "hermes-agent-image.json").read_text(encoding="utf-8") ) == metadata assert set(metadata) == { "digest", "flux_image", "image", "published_tag", "source_revision", } @pytest.mark.parametrize( ("digest", "revision", "destination"), [ ("latest", "a" * 40, "registry.bstein.dev/bstein/hermes-agent:git-" + "a" * 40), ("sha256:" + "1" * 64, "short", "registry.bstein.dev/bstein/hermes-agent:git-short"), ("sha256:" + "1" * 64, "a" * 40, "registry.bstein.dev/bstein/hermes-agent:latest"), ], ) def test_release_renderer_rejects_unpinned_inputs( tmp_path: Path, digest: str, revision: str, destination: str ) -> None: """Only exact digests and immutable source-derived tags are accepted.""" module = _load_release_module() manifest = tmp_path / "kustomization.yaml" manifest.write_text( "images:\n" f" - name: {module.DEFAULT_IMAGE}\n" " digest: sha256:" + "0" * 64 + "\n", encoding="utf-8", ) with pytest.raises(ValueError): module.write_release_artifacts( digest=digest, source_revision=revision, destination=destination, kustomization=manifest, output_dir=tmp_path / "out", ) def test_release_renderer_fails_closed_on_manifest_drift(tmp_path: Path) -> None: """Missing, duplicate, or already-current image entries require human review.""" module = _load_release_module() digest = "sha256:" + "f" * 64 missing = "images:\n - name: example.invalid/other\n digest: " + digest + "\n" with pytest.raises(ValueError, match="found 0"): module.render_kustomization(missing, digest) duplicate = ( "images:\n" f" - name: {module.DEFAULT_IMAGE}\n digest: {digest}\n" f" - name: {module.DEFAULT_IMAGE}\n digest: {digest}\n" ) with pytest.raises(ValueError, match="found 2"): module.render_kustomization(duplicate, digest) manifest = tmp_path / "kustomization.yaml" manifest.write_text( f"images:\n - name: {module.DEFAULT_IMAGE}\n digest: {digest}\n", encoding="utf-8", ) revision = "b" * 40 with pytest.raises(ValueError, match="already matches"): module.write_release_artifacts( digest=digest, source_revision=revision, destination=f"{module.DEFAULT_IMAGE}:git-{revision}", kustomization=manifest, output_dir=tmp_path / "out", ) def test_release_cli_reads_digest_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The pipeline CLI uses the Kaniko digest file as its sole digest input.""" module = _load_release_module() digest = "sha256:" + "c" * 64 revision = "d" * 40 manifest = tmp_path / "kustomization.yaml" manifest.write_text( f"images:\n - name: {module.DEFAULT_IMAGE}\n digest: sha256:" + "0" * 64 + "\n", encoding="utf-8", ) digest_file = tmp_path / "digest" digest_file.write_text(digest + "\n", encoding="utf-8") output = tmp_path / "out" monkeypatch.setattr( "sys.argv", [ "hermes_image_release.py", "--digest-file", str(digest_file), "--source-revision", revision, "--destination", f"{module.DEFAULT_IMAGE}:git-{revision}", "--kustomization", str(manifest), "--output-dir", str(output), ], ) assert module.main() == 0 assert json.loads( (output / "hermes-agent-image.json").read_text(encoding="utf-8") )["digest"] == digest