From 68d3ca8e7b4f16175dfeb1e9aa756eeba6ed3874 Mon Sep 17 00:00:00 2001 From: "Altru.dev" Date: Fri, 4 Sep 2026 08:41:49 -0700 Subject: [PATCH 1/3] fix(sandbox): validate required textual fields --- src/agentrust_trace/adapters/sandbox.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/agentrust_trace/adapters/sandbox.py b/src/agentrust_trace/adapters/sandbox.py index cbf805fc..c9ee22b3 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." @@ -309,7 +324,7 @@ def transcript_hash(decisions: list[dict[str, Any]]) -> str: def software_measurement(image_digest: str, bundle_hash: str) -> str: """The measurement for an unattested sandbox. - ``sha256(image_digest + "\\n" + bundle_hash)`` over UTF-8, both operands in their + ``sha256(image_digest + "\n" + bundle_hash)`` over UTF-8, both operands in their ``sha256:``-prefixed form. Deliberately simple so another implementation can reproduce it without a JSON library. From a0e81932d217b102d30bb93f586776c39568c1e8 Mon Sep 17 00:00:00 2001 From: "Altru.dev" Date: Fri, 4 Sep 2026 08:42:06 -0700 Subject: [PATCH 2/3] test(sandbox): cover required string primitive boundaries --- tests/test_sandbox_required_string_fields.py | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_sandbox_required_string_fields.py 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://") From 5b4da407937897fcb3c40cc0545a3d82efafd7bb Mon Sep 17 00:00:00 2001 From: "Altru.dev" Date: Fri, 4 Sep 2026 08:43:19 -0700 Subject: [PATCH 3/3] chore(sandbox): preserve measurement documentation --- src/agentrust_trace/adapters/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agentrust_trace/adapters/sandbox.py b/src/agentrust_trace/adapters/sandbox.py index c9ee22b3..6210e9df 100644 --- a/src/agentrust_trace/adapters/sandbox.py +++ b/src/agentrust_trace/adapters/sandbox.py @@ -324,7 +324,7 @@ def transcript_hash(decisions: list[dict[str, Any]]) -> str: def software_measurement(image_digest: str, bundle_hash: str) -> str: """The measurement for an unattested sandbox. - ``sha256(image_digest + "\n" + bundle_hash)`` over UTF-8, both operands in their + ``sha256(image_digest + "\\n" + bundle_hash)`` over UTF-8, both operands in their ``sha256:``-prefixed form. Deliberately simple so another implementation can reproduce it without a JSON library.