"""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-tag-with-digest-file=" in source assert "--destination=" in source assert "assert-absent" in source assert "git-${actual_revision}-build-${BUILD_NUMBER}" 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": "https://ci.bstein.dev/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": "https://ci.bstein.dev/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") seeder = ( REPO_ROOT / "services/vault/scripts/vault_hermes_jenkins_build_token_ensure.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 "vault_hermes_jenkins_build_token_ensure.sh" in vault assert "sys/tools/random/32 format=hex" in seeder assert "kv put -cas=0" in seeder assert "kv/atlas/hermes/developer-jenkins" in seeder 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 build_number = "17" 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, build_number=build_number, destination=f"{module.DEFAULT_IMAGE}:git-{revision}-build-{build_number}", 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", "build_number", "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 + "-build-1", ), ( "sha256:" + "1" * 64, "short", "registry.bstein.dev/bstein/hermes-agent:git-short-build-1", ), ( "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, build_number="1", 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, build_number="1", destination=f"{module.DEFAULT_IMAGE}:git-{revision}-build-1", 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") destination = f"{module.DEFAULT_IMAGE}:git-{revision}-build-9" image_file = tmp_path / "image" image_file.write_text(f"{destination}@{digest}\n", encoding="utf-8") output = tmp_path / "out" monkeypatch.setenv("HARBOR_USER", "robot") monkeypatch.setenv("HARBOR_PASSWORD", "secret") monkeypatch.setattr( module, "verify_registry_digest", lambda *_args, **_kwargs: None ) monkeypatch.setattr( "sys.argv", [ "hermes_image_release.py", "render", "--digest-file", str(digest_file), "--image-file", str(image_file), "--source-revision", revision, "--build-number", "9", "--destination", destination, "--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 )