"""Contracts for the handoff harness credential screen and output bounding.""" from __future__ import annotations import json import pytest from testing.tests.test_hermes_handoff_support import load_handoff_module redaction = load_handoff_module("hermes_handoff_redaction") @pytest.mark.parametrize( "text", [ "token: ghp_ABCDEFGHIJKLMNOPQRSTUV1234", "Authorization: Bearer abcdefghijklmnop", "cookie: session=abcdefghijklmnop", "password=hunter2000secret", "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END OPENSSH PRIVATE KEY-----", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0.SflKxwRJ", "https://user:supersecretvalue@scm.example.dev/x", "vault token hvs.CAESIJabcdefghijklmnop", "aws AKIAIOSFODNN7EXAMPLE", ], ) def test_credential_shapes_are_screened(text: str) -> None: assert redaction.contains_credential_shape(text) assert redaction.REDACTED in redaction.redact(text) @pytest.mark.parametrize( "text", [ "sha256:37ebf720c783ae908a602916ffccf88d43d205a157957f5dc4b487867aee45e7", "ab346f55509d584e457fe26cf90be3078f7a375c", '"automountServiceAccountToken": false', '"key": "kubernetes.io/arch"', "namespace: hermes", "replicas: 3", "checked_at: 2026-08-17T09:17:07.224400+00:00", ], ) def test_ordinary_evidence_survives_screening(text: str) -> None: assert redaction.redact(text) == text assert not redaction.contains_credential_shape(text) def test_screened_json_is_still_parsable_json() -> None: """A screen that corrupts structure would break every downstream evaluator.""" payload = { "spec": {"automountServiceAccountToken": True, "key": "kubernetes.io/arch"}, "data": {"password": "a-long-enough-secret-value"}, } screened = redaction.redact(json.dumps(payload, indent=2)) reparsed = json.loads(screened) assert reparsed["spec"]["automountServiceAccountToken"] is True assert reparsed["spec"]["key"] == "kubernetes.io/arch" assert reparsed["data"]["password"] == redaction.REDACTED def test_high_entropy_runs_are_screened_but_hex_digests_are_not() -> None: assert ( redaction.redact("Zm9vYmFyYmF6cXV4-Quux_1234567890ABCDEFghij") != redaction.REDACTED ) long_mixed = "aB3" + "x9Y2z_" * 10 assert redaction.redact(long_mixed) == redaction.REDACTED assert redaction.redact("f" * 64) == "f" * 64 @pytest.mark.parametrize( ("key", "sensitive"), [ ("ANTHROPIC_API_KEY", True), ("gitea_token", True), ("clientSecret", True), ("Set-Cookie", True), ("automountServiceAccountToken", True), ("key", False), ("namespace", False), ("", False), ], ) def test_key_sensitivity_classification(key: str, sensitive: bool) -> None: assert redaction.is_sensitive_key(key) is sensitive @pytest.mark.parametrize( ("value", "possible"), [ ("true", False), ("False", False), ("3", False), ("12.5", False), ("short", False), ("", False), ("a-long-enough-secret", True), ('"another-long-secret"', True), ("1234567890123", False), ("-12.5e9", False), ], ) def test_value_shape_gates_screening(value: str, possible: bool) -> None: assert redaction.could_hold_secret(value) is possible def test_bound_truncates_to_a_byte_budget_and_reports_it() -> None: text, truncated = redaction.bound("x" * 100, max_bytes=10) assert truncated assert len(text.encode()) <= 10 assert text == redaction.TRUNCATION_NOTE[:10] text, truncated = redaction.bound("short", max_bytes=64) assert (text, truncated) == ("short", False) def test_bound_with_no_budget_keeps_nothing() -> None: assert redaction.bound("content", max_bytes=0) == ("", True) assert redaction.bound("", max_bytes=0) == ("", False) def test_bound_never_splits_a_multibyte_character() -> None: text, truncated = redaction.bound("é" * 10, max_bytes=5) assert truncated assert len(text.encode()) <= 5 def test_safe_text_screens_before_it_bounds() -> None: """A boundary must never preserve a useful credential prefix.""" payload = "password=" + "a" * 200 text, truncated = redaction.safe_text(payload, max_bytes=20) assert not truncated assert redaction.REDACTED in text def test_no_byte_budget_exposes_a_credential_prefix() -> None: payload = "password=A9secret-prefix-that-must-never-survive" for budget in range(1, 64): screened, _ = redaction.safe_text(payload, max_bytes=budget) assert "A9" not in screened assert len(screened.encode()) <= budget def test_scrub_walks_nested_structures_and_keeps_useful_types() -> None: scrubbed = redaction.scrub( { "token": "a-long-enough-secret", "automountServiceAccountToken": False, "items": [{"password": "another-long-secret"}, 3, None], "note": ("tuple", "values"), } ) assert scrubbed["token"] == redaction.REDACTED assert scrubbed["automountServiceAccountToken"] is False assert scrubbed["items"][0]["password"] == redaction.REDACTED assert scrubbed["items"][1] == 3 assert scrubbed["items"][2] is None assert scrubbed["note"] == ["tuple", "values"] def test_scrub_screens_short_numeric_nested_values_and_mapping_keys() -> None: scrubbed = redaction.scrub( { "token": 1234, "credentials": {"x": "y"}, "ghp_ABCDEF1234567890": "mapping-key", "private_key": "x", } ) assert scrubbed["token"] == redaction.REDACTED assert scrubbed["credentials"] == redaction.REDACTED assert scrubbed["private_key"] == redaction.REDACTED assert not any("ghp_" in key for key in scrubbed) def test_unterminated_private_key_blocks_are_screened() -> None: assert "PRIVATE KEY" not in redaction.redact("-----BEGIN PRIVATE KEY-----\nabc") def test_screening_is_idempotent() -> None: once = redaction.redact("api_key=AKIAIOSFODNN7EXAMPLE") assert redaction.redact(once) == once assert redaction.redact("") == "" def test_normalise_key_strips_separators() -> None: assert redaction.normalise_key("Client-Secret_v2") == "clientsecretv2"