"""Adversarial contracts for the Atlas-only draft pull-request client.""" from __future__ import annotations import importlib.util import io import json import sys import urllib.error from pathlib import Path import pytest import yaml ROOT = Path(__file__).parents[2] CLIENT_PATH = ROOT / "services/hermes/scripts/gitea_api.py" def _load(): spec = importlib.util.spec_from_file_location("safe_gitea_api", CLIENT_PATH) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def _draft_payload(**updates): payload = { "base": "main", "body": "Review evidence", "head": "hermes/review-fix", "title": "WIP: Repair review findings", } payload.update(updates) return payload class Response: def __init__(self, body: object): self.body = body if isinstance(body, bytes) else json.dumps(body).encode() def __enter__(self): return self def __exit__(self, *_args): return False def read(self): return self.body @pytest.mark.parametrize( ("base_url", "path"), [ ("https://evil.example", "/api/v1/repos/atlas/cassandra"), ("http://scm.bstein.dev", "/api/v1/repos/atlas/cassandra"), ("https://scm.bstein.dev:443", "/api/v1/repos/atlas/cassandra"), ("https://scm.bstein.dev", "https://evil.example/api/v1/repos/atlas/cassandra"), ("https://scm.bstein.dev", "/api/v1/repos/evil/cassandra"), ("https://scm.bstein.dev", "/api/v1/repos/%61tlas/cassandra"), ("https://scm.bstein.dev", "/api/v1/repos/atlas/../admin"), ], ) def test_host_owner_and_path_escape_attempts_are_rejected(base_url: str, path: str): client = _load() with pytest.raises(client.PolicyError): client.build_request("GET", path, base_url=base_url, token="secret") @pytest.mark.parametrize( ("method", "path", "data"), [ ("DELETE", "/api/v1/repos/atlas/cassandra/pulls/4", None), ("POST", "/api/v1/repos/atlas/cassandra/pulls/4/merge", {}), ( "POST", "/api/v1/repos/atlas/cassandra/pulls/4/reviews", {"event": "APPROVED"}, ), ("PATCH", "/api/v1/repos/atlas/cassandra/pulls/4", {"state": "closed"}), ("PATCH", "/api/v1/repos/atlas/cassandra/pulls/4", {"draft": False}), ("PUT", "/api/v1/repos/atlas/cassandra/branches/main", {}), ], ) def test_merge_approve_close_delete_and_other_mutations_are_rejected( method: str, path: str, data: object ): client = _load() with pytest.raises(client.PolicyError): client.build_request( method, path, base_url=client.CANONICAL_BASE_URL, token="secret", data=data ) def test_create_forces_draft_title_and_same_repository_branch_names(): client = _load() assert ( client.authorize_request( "POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload() ) == "create-draft" ) with pytest.raises(client.PolicyError, match="draft-title prefix"): client.authorize_request( "POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload(title="Not a draft"), ) with pytest.raises(client.PolicyError): client.authorize_request( "POST", "/api/v1/repos/atlas/cassandra/pulls", _draft_payload(head="someone:branch"), ) def test_update_checks_server_draft_state_before_patching(): client = _load() calls = [] responses = iter( [ Response({"number": 7, "draft": True}), Response({"number": 7, "draft": True}), ] ) def opener(request, timeout): calls.append((request.method, request.full_url, request.data, timeout)) return next(responses) result = client.update_draft( "cassandra", 7, {"title": "Narrowed repair"}, token="runtime", opener=opener ) assert json.loads(result) == {"number": 7, "draft": True} assert [call[0] for call in calls] == ["GET", "PATCH"] assert json.loads(calls[1][2]) == {"title": "WIP: Narrowed repair"} def test_update_rejects_non_draft_without_sending_patch(): client = _load() calls = [] def opener(request, timeout): calls.append((request.method, timeout)) return Response({"number": 7, "draft": False}) with pytest.raises(client.PolicyError, match="human review now owns"): client.update_draft( "cassandra", 7, {"body": "new"}, token="runtime", opener=opener ) assert calls == [("GET", 30)] def test_runtime_token_is_only_an_authorization_header(): client = _load() request = client.build_request( "POST", "/api/v1/repos/atlas/cassandra/pulls", base_url=client.CANONICAL_BASE_URL, token="do-not-leak", data=_draft_payload(), ) assert "do-not-leak" not in request.full_url assert b"do-not-leak" not in request.data assert request.get_header("Authorization") == "token do-not-leak" def test_create_uses_live_gitea_schema_and_verifies_draft_response(): client = _load() calls = [] def opener(request, timeout): calls.append((json.loads(request.data), timeout)) return Response({"number": 3, "draft": True}) result = client.create_draft( "cassandra", base="main", head="hermes/fix", title="Focused fix", body="Evidence", token="runtime", opener=opener, ) assert json.loads(result)["draft"] is True assert calls[0][0]["title"] == "WIP: Focused fix" assert "draft" not in calls[0][0] def test_create_fails_closed_when_server_does_not_confirm_draft(): client = _load() with pytest.raises(client.PolicyError, match="did not confirm draft"): client.create_draft( "cassandra", base="main", head="hermes/fix", title="Focused fix", body="Evidence", token="runtime", opener=lambda *_a, **_k: Response({"number": 3, "draft": False}), ) def test_output_redaction_covers_exact_token_and_authorization_header(): client = _load() raw = b'{"message":"do-not-leak","debug":"Authorization: token do-not-leak"}' redacted = client.redact_bytes(raw, "do-not-leak") assert b"do-not-leak" not in redacted assert redacted.count(b"[REDACTED]") >= 1 def test_dry_run_does_not_read_token_or_use_network( tmp_path: Path, monkeypatch, capsys ): client = _load() body = tmp_path / "body.md" body.write_text("Evidence only\n", encoding="utf-8") monkeypatch.setattr(client, "read_token", lambda: pytest.fail("read token")) monkeypatch.setattr( client.urllib.request, "urlopen", lambda *_a, **_k: pytest.fail("network") ) assert ( client.main( [ "--dry-run", "create-draft", "cassandra", "--base", "main", "--head", "hermes/fix", "--title", "Repair", "--body-file", str(body), ] ) == 0 ) output = json.loads(capsys.readouterr().out) assert output["operation"] == "create-draft" assert output["owner"] == "atlas" assert "Evidence only" not in json.dumps(output) def test_http_error_path_redacts_token(monkeypatch, capsys): client = _load() monkeypatch.setattr(client, "read_token", lambda: "do-not-leak") def fail(*_args, **_kwargs): raise urllib.error.HTTPError( "https://scm.bstein.dev/api/v1/repos/atlas/cassandra", 403, "forbidden", {}, io.BytesIO(b"Authorization: token do-not-leak"), ) monkeypatch.setattr(client, "read", lambda *_a, **_k: fail()) assert client.main(["read", "/api/v1/repos/atlas/cassandra"]) == 1 captured = capsys.readouterr() assert "do-not-leak" not in captured.err assert "HTTP 403" in captured.err def test_flux_manifest_projects_runtime_vault_token_and_skill_only(): client_source = CLIENT_PATH.read_text(encoding="utf-8") assert "/runtime-access/gitea-token" in client_source assert "GITEA_TOKEN" not in client_source deployment = yaml.safe_load( (ROOT / "services/hermes/agent-deployment.yaml").read_text(encoding="utf-8") ) template = deployment["spec"]["template"] annotations = template["metadata"]["annotations"] assert annotations["vault.hashicorp.com/agent-inject-secret-gitea-token"] == ( "kv/data/atlas/hermes/developer-gitea" ) runtime = next( volume for volume in template["spec"]["volumes"] if volume["name"] == "runtime-access" ) assert runtime["emptyDir"]["medium"] == "Memory" expected = {"hermes", "terminal", "cli-lane-runner"} mounted = { container["name"] for container in template["spec"]["containers"] if any( mount["name"] == "atlas-pr-skill" and mount["mountPath"] == "/opt/data/workspace/skills/manage-atlas-pull-requests" and mount.get("readOnly") is True for mount in container.get("volumeMounts", []) ) } assert mounted == expected kustomization = yaml.safe_load( (ROOT / "services/hermes/kustomization.yaml").read_text(encoding="utf-8") ) generator = next( item for item in kustomization["configMapGenerator"] if item["name"] == "hermes-atlas-pr-skill" ) assert generator["files"] == [ "SKILL.md=skills/manage-atlas-pull-requests/SKILL.md", "openai.yaml=skills/manage-atlas-pull-requests/agents/openai.yaml", ]