The bug report was about a type. A field called ttl_seconds had accepted "3600" — a string — and more quietly, 3600.0, a float that the code then used as 3600, so one attestation was validated with a one-second lifetime and no error at all. The fix was the obvious one and it was correct: make the field type-correct. Reject anything that is not an integer.
It did not stop the crash.
An integer is not a small integer
The new guard accepts a value if it is an integer — and 10**30 is an integer. So is 1e30: float.is_integer() is True. Both walk straight through the type check. Then the program dies anyway, one line later, in the arithmetic the type check never looked at:
self.expires_at = self.issued_at + timedelta(seconds=self.ttl_seconds)
Two different operations, two different overflows, one shape each:
ttl_seconds = 3600 -> 2026-10-10T01:00:00 # fine
ttl_seconds = 86400 -> 2026-10-11T00:00:00 # fine
ttl_seconds = 10**10 -> 2343-08-30T17:46:40 # fine, ~317 years
ttl_seconds = 999999999999 -> OverflowError: date value out of range
ttl_seconds = 10**30 -> OverflowError: Python int too large to convert to C int
ttl_seconds = 1e30 -> OverflowError: Python int too large to convert to C int
timedelta(seconds=...) has its own range before you even get to the sum, and the datetime sum has a second: a year in 1..9999. The two fail with different messages, on different magnitudes, and a value in the middle hits the second while a larger one hits the first.
The type check could not have caught either one, and this is the part worth keeping: the bound is on the computation, not on the number. The same value is valid or invalid depending on a value the field does not contain.
86400 from 2026-10-10 -> 2026-10-11T00:00:00 # fine
86400 from 9999-12-31 -> OverflowError: date value out of range
There is no constant ceiling you can write on ttl_seconds. Whatever number you pick, it is fine from today's clock and out of range from a clock near the top of the range — because the failure is in the sum, and the sum has two inputs. "Check that the value is an integer" and "check that the value is representable" are two different questions about one field, and closing the first says nothing about the second.
The crash that wore a disguise
The overflow escaped uncaught, so the process exited with code 1. That was also the exit code a stale attestation used. So a malformed document was indistinguishable, to every caller, from an expired one — through the validator, the chain checker and the MCP checker alike. The cost is not the traceback. It is that the traceback arrived wearing someone else's identity, and the caller acted on it: "your attestation is expired" instead of "your input is broken".
The fix: translate the overflow where it happens
One helper, applied wherever the field meets a clock:
def _deadline(issued_at: datetime, seconds: int, field: str = "ttl_seconds") -> datetime:
try:
return issued_at + timedelta(seconds=seconds)
except (OverflowError, OSError, ValueError) as exc:
raise ValueError(
f"{field} {seconds} is out of range for a deadline from "
f"issued_at {issued_at.isoformat()}: {exc}"
) from exc
ValueError, not OverflowError, because ValueError is the exception every caller in this package already maps to "bad input" (exit 2). No caller needs a new except clause; the malformed input joins the family it belonged to all along.
Two details made it hold up:
-
Every site, not one. Construction computes the deadline unconditionally, so an unusable value fails where callers already handle it; validation calls the same helper so a re-pointed field still returns a verdict; and the
--max-ttlceiling turns out to be a third place that adds seconds to the same clock and is reachable with the same shape (--max-ttl 99999999999999999). Missing any one of them leaves a path where the old crash survives. -
A ceiling the arithmetic cannot represent is not a ceiling. No representable deadline exceeds it, so the comparison is vacuous rather than fatal — and
validatekeeps its contract of returning a result instead of raising. Raising there would have reintroduced, inside the fix, the exact exit-1traceback the fix exists to remove.
And a control arm: a representable large value (10**10, about 317 years out) must still pass. Without it, the fix is indistinguishable from simply lowering a ceiling — the tests would be green for the wrong reason.
What to take from it
- Checking a value's type is not bounding it. The bound belongs on the computation the value feeds, applied at every place the value is combined.
- A failure that shares an exit code with a different failure is two bugs, not one. The overflow wasn't only a crash; it was a malformed input impersonating an expired one.
- If you don't have an arm proving a large-but-valid value still passes, you haven't bounded the input — you've moved a ceiling and called it a bound.
The evidence for this one is a small open-source package: yunaremaia/agent-capability-attestation, issue #53 → PR #55 (base 95f532a8; the suite went 404 → 435 passed, with 7 mutation arms killed; merged 2026-10-07). The same shape came up in the sibling issues — a check added for one dimension of a field, leaving the other dimension open — which is the reason it is worth writing down.
Top comments (0)