Problem
On current main at a247244fb0bb97aea4e60d655f36d445cca18cd5, provenance.build_record() converts an explicitly supplied issued_at with int(...) before passing it to the shared structural validator:
stamped_at = int(issued_at if issued_at is not None else time.time())
_check_structure(
...,
issued_at=stamped_at,
)
But _check_structure() explicitly intends to enforce the original JSON-integer boundary:
# bool is an int subclass, and True would otherwise pass as a timestamp.
if not isinstance(issued_at, int) or isinstance(issued_at, bool) or issued_at < 0:
raise ProvenanceError(...)
Because validation happens after coercion, that guard cannot distinguish several malformed caller values from legitimate integers.
Measured directly from the conversion used by the function:
supplied issued_at |
value handed to _check_structure |
True |
1 |
False |
0 |
1.9 |
1 |
"123" |
123 |
-0.5 |
0 |
All five therefore satisfy the validator's integer/non-negative test after conversion. The last case is particularly diagnostic: a negative non-integer is turned into an accepted non-negative timestamp.
Why this is distinct from #142 / #146
#142 found that consumer-side verification failed to enforce structural rules that build_record() was assumed to enforce, including the issued_at rule. #146 moved those rules into the shared _check_structure() helper.
This is a different producer-side ordering defect: build_record() transforms the value before the shared rule sees it. The shared validator itself is strict; the caller path defeats it by normalizing first.
Expected invariant
For an explicitly supplied issued_at:
- a non-negative JSON/Python integer is accepted unchanged;
bool, float, string, null-equivalent explicit values, containers, and negative values are refused with ProvenanceError;
- only the omitted/default path may derive an integer timestamp from
time.time().
The emitted record should never contain a timestamp different in primitive/value semantics from the one the caller explicitly supplied.
Bounded fix direction
Validate the explicit issued_at before conversion, or avoid converting explicit values entirely. Keep conversion only for the internally generated default, e.g. conceptually:
stamped_at = int(time.time()) if issued_at is None else issued_at
_check_structure(..., issued_at=stamped_at)
Regression coverage should pin the coercible malformed values above plus an unchanged valid-integer control and the default-timestamp path.
No provenance wire-format, signature, freshness, revocation, catalog, or verifier semantics need to change.
Duplicate / ownership review
I searched the TRACE issue and PR history for provenance.build_record, issued_at, boolean/float/string coercion, and found no existing issue or PR covering this exact pre-validation coercion. #142/#146 are adjacent but address consumer/producer structural-rule symmetry, not this ordering bug.
AI-assistance disclosure: ChatGPT assisted with current-main source review, duplicate search, conversion reproduction, and issue drafting. altrudev reviewed the bounded claim and remains responsible for the contribution.
Problem
On current
mainata247244fb0bb97aea4e60d655f36d445cca18cd5,provenance.build_record()converts an explicitly suppliedissued_atwithint(...)before passing it to the shared structural validator:But
_check_structure()explicitly intends to enforce the original JSON-integer boundary:Because validation happens after coercion, that guard cannot distinguish several malformed caller values from legitimate integers.
Measured directly from the conversion used by the function:
issued_at_check_structureTrue1False01.91"123"123-0.50All five therefore satisfy the validator's integer/non-negative test after conversion. The last case is particularly diagnostic: a negative non-integer is turned into an accepted non-negative timestamp.
Why this is distinct from #142 / #146
#142 found that consumer-side verification failed to enforce structural rules that
build_record()was assumed to enforce, including theissued_atrule. #146 moved those rules into the shared_check_structure()helper.This is a different producer-side ordering defect:
build_record()transforms the value before the shared rule sees it. The shared validator itself is strict; the caller path defeats it by normalizing first.Expected invariant
For an explicitly supplied
issued_at:bool, float, string, null-equivalent explicit values, containers, and negative values are refused withProvenanceError;time.time().The emitted record should never contain a timestamp different in primitive/value semantics from the one the caller explicitly supplied.
Bounded fix direction
Validate the explicit
issued_atbefore conversion, or avoid converting explicit values entirely. Keep conversion only for the internally generated default, e.g. conceptually:Regression coverage should pin the coercible malformed values above plus an unchanged valid-integer control and the default-timestamp path.
No provenance wire-format, signature, freshness, revocation, catalog, or verifier semantics need to change.
Duplicate / ownership review
I searched the TRACE issue and PR history for
provenance.build_record,issued_at, boolean/float/string coercion, and found no existing issue or PR covering this exact pre-validation coercion. #142/#146 are adjacent but address consumer/producer structural-rule symmetry, not this ordering bug.AI-assistance disclosure: ChatGPT assisted with current-main source review, duplicate search, conversion reproduction, and issue drafting.
altrudevreviewed the bounded claim and remains responsible for the contribution.