diff --git a/src/agentrust_trace/adapters/sandbox.py b/src/agentrust_trace/adapters/sandbox.py index cbf805fc..6210e9df 100644 --- a/src/agentrust_trace/adapters/sandbox.py +++ b/src/agentrust_trace/adapters/sandbox.py @@ -96,6 +96,10 @@ class SandboxAttestation: nonce: str | None = None def __post_init__(self) -> None: + if not isinstance(self.platform, str): + raise ValueError( + f"SandboxAttestation.platform must be a string, got {type(self.platform).__name__}" + ) if self.platform == "software-only": raise ValueError( "SandboxAttestation.platform must not be 'software-only'. Omit the " @@ -108,6 +112,11 @@ def __post_init__(self) -> None: f"SandboxAttestation.platform {self.platform!r} is not an accepted " f"platform. Accepted: {', '.join(sorted(_PLATFORMS))}." ) + if not isinstance(self.measurement, str): + raise ValueError( + "SandboxAttestation.measurement must be a string, got " + f"{type(self.measurement).__name__}" + ) if not _DIGEST_RE.match(self.measurement): raise ValueError( f"SandboxAttestation.measurement {self.measurement!r} is not a sha256: or " @@ -151,12 +160,18 @@ class SandboxSessionResult: """Issuance timestamp. Defaults to now.""" def __post_init__(self) -> None: + if not isinstance(self.sandbox_id, str): + raise ValueError(f"sandbox_id must be a string, got {type(self.sandbox_id).__name__}") if not _SUBJECT_RE.match(self.sandbox_id): raise ValueError( f"sandbox_id {self.sandbox_id!r} must be a SPIFFE URI " "('spiffe:///') or a DID ('did::'). " "It becomes the record subject, which is what a verifier keys on." ) + if not isinstance(self.image_digest, str): + raise ValueError( + f"image_digest must be a string, got {type(self.image_digest).__name__}" + ) if not _DIGEST_RE.match(self.image_digest): raise ValueError( f"image_digest {self.image_digest!r} must be a sha256: or sha384: digest." diff --git a/tests/test_sandbox_required_string_fields.py b/tests/test_sandbox_required_string_fields.py new file mode 100644 index 00000000..ea7d10c6 --- /dev/null +++ b/tests/test_sandbox_required_string_fields.py @@ -0,0 +1,59 @@ +"""Regression tests for sandbox adapter required-string primitive boundaries.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from agentrust_trace.adapters.sandbox import SandboxAttestation, SandboxSessionResult + +DIGEST = "sha256:" + "a" * 64 +BAD_VALUES: tuple[Any, ...] = (True, 1, [1], {"x": 1}) + + +@pytest.mark.parametrize("platform", BAD_VALUES, ids=repr) +def test_attestation_platform_refuses_non_string_values(platform: Any) -> None: + with pytest.raises(ValueError, match="SandboxAttestation.platform"): + SandboxAttestation(platform=platform, measurement=DIGEST) + + +@pytest.mark.parametrize("measurement", BAD_VALUES, ids=repr) +def test_attestation_measurement_refuses_non_string_values(measurement: Any) -> None: + with pytest.raises(ValueError, match="SandboxAttestation.measurement"): + SandboxAttestation(platform="tpm2", measurement=measurement) + + +@pytest.mark.parametrize("sandbox_id", BAD_VALUES, ids=repr) +def test_session_sandbox_id_refuses_non_string_values(sandbox_id: Any) -> None: + with pytest.raises(ValueError, match="sandbox_id"): + SandboxSessionResult( + sandbox_id=sandbox_id, + image_digest=DIGEST, + policy_bundle_bytes=b"policy", + decisions=[], + ) + + +@pytest.mark.parametrize("image_digest", BAD_VALUES, ids=repr) +def test_session_image_digest_refuses_non_string_values(image_digest: Any) -> None: + with pytest.raises(ValueError, match="image_digest"): + SandboxSessionResult( + sandbox_id="spiffe://runtime.example.org/sandbox/test", + image_digest=image_digest, + policy_bundle_bytes=b"policy", + decisions=[], + ) + + +def test_textual_controls_still_construct() -> None: + attestation = SandboxAttestation(platform="tpm2", measurement=DIGEST) + session = SandboxSessionResult( + sandbox_id="spiffe://runtime.example.org/sandbox/test", + image_digest=DIGEST, + policy_bundle_bytes=b"policy", + decisions=[], + attestation=attestation, + ) + assert attestation.platform == "tpm2" + assert session.sandbox_id.startswith("spiffe://")