Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/agentrust_trace/adapters/agt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import rfc8785
from agentrust_trace.models import (
JCS_SAFE_INTEGER,
Appraisal,
BuildProvenance,
ModelInfo,
Expand Down Expand Up @@ -57,7 +58,17 @@ class AGTSessionResult:

iat: int = field(default_factory=lambda: int(time.time()))
"""Issuance timestamp. Defaults to now."""

def __post_init__(self) -> None:
if (
not isinstance(self.iat, int)
or isinstance(self.iat, bool)
or self.iat < 0
or self.iat > JCS_SAFE_INTEGER
):
raise ValueError(
f"iat must be a non-negative integer Unix timestamp within the JCS "
f"safe-integer range, got {self.iat!r}"
)

class TraceAGTAdapter:
"""Build Level 0 TRACE Trust Records from AGT govern() session output.
Expand Down
12 changes: 12 additions & 0 deletions src/agentrust_trace/adapters/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import rfc8785

from agentrust_trace.models import (
JCS_SAFE_INTEGER,
Appraisal,
BuildProvenance,
ModelInfo,
Expand Down Expand Up @@ -227,9 +228,20 @@ def __post_init__(self) -> None:
if not _DIGEST_RE.match(self.image_digest):
raise ValueError(
f"image_digest {self.image_digest!r} must be a sha256: or sha384: digest."
)
if (
not isinstance(self.iat, int)
or isinstance(self.iat, bool)
or self.iat < 0
or self.iat > JCS_SAFE_INTEGER
):
raise ValueError(
f"iat must be a non-negative integer Unix timestamp within the JCS "
f"safe-integer range, got {self.iat!r}"
)



class TraceSandboxAdapter:
"""Build Trust Records from a sandboxed agent runtime's session output.

Expand Down
24 changes: 24 additions & 0 deletions tests/test_agt_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,27 @@ def test_an_unappraised_record_still_signs_and_verifies() -> None:
signed = sign_record(record, key)
assert verify_record(signed, public_key_or_jwk=key_to_jwk(key)) is not None
assert signed["appraisal"]["status"] == "none"


# ---------------------------------------------------------------------------
# iat reaches the record untouched (same failure mode as #320)
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("iat", ["1800000000", True, False, -5, 1.5, 2**60])
def test_iat_must_be_a_bounded_non_negative_integer(iat) -> None:
"""Every other field here reaches the record through a models.py pydantic
constructor, which coerces or refuses a bad type before it is dumped. `iat`
used to reach build_trust_record's top-level "iat" key untouched, with
AGTSessionResult performing no validation of any field at all: a numeric
string or a bool survived to the signed wire form, and TrustRecord.model_validate()
reported it valid (pydantic's lax mode coerces on the way in) while the wire
bytes stayed the original, schema-invalid type. Same failure mode as
provenance.build_record's pre-#320 issued_at coercion."""
with pytest.raises(ValueError, match="iat must be a non-negative integer"):
_make_session(iat=iat)


def test_valid_iat_round_trips_as_an_int_on_the_wire() -> None:
record = _make_adapter().build_trust_record(_make_session(iat=1800000000))
assert record["iat"] == 1800000000
assert isinstance(record["iat"], int)
19 changes: 19 additions & 0 deletions tests/test_sandbox_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,25 @@ def test_a_bad_sandbox_id_fails_at_the_adapter_not_at_model_validate() -> None:
_make_session(sandbox_id="build-7f2a")


@pytest.mark.parametrize("iat", ["1800000000", True, False, -5, 1.5, 2**60])
def test_iat_must_be_a_bounded_non_negative_integer(iat) -> None:
"""Every other field here reaches the record through a models.py pydantic
constructor, which coerces or refuses a bad type before it is dumped. `iat`
used to reach build_trust_record's top-level "iat" key untouched, so a
numeric string or a bool survived to the signed wire form: model_validate()
reported it valid (pydantic's lax mode coerces on the way in) while the wire
bytes stayed the original, schema-invalid type. Same failure mode as
provenance.build_record's pre-#320 issued_at coercion."""
with pytest.raises(ValueError, match="iat must be a non-negative integer"):
_make_session(iat=iat)


def test_valid_iat_round_trips_as_an_int_on_the_wire() -> None:
record = _make_adapter().build_trust_record(_make_session(iat=1800000000))
assert record["iat"] == 1800000000
assert isinstance(record["iat"], int)


# ---------------------------------------------------------------------------
# 6. Transcript hashing uses JCS
# ---------------------------------------------------------------------------
Expand Down
Loading