28. One Reason Bound for Every Contract Parser¶
Date: 2026-10-14
Status¶
Accepted. Amends ADR-0020 point 3 and ADR-0021 decision 1.
Context¶
pkg/aegis bounds the reason of a rejection since pull request #71: a reason built from decoder or I/O error text is cut to the exported aegis.MaxReasonLen (256 bytes) and ends with a space and (truncated). ADR-0020 does not record that bound, and the review of #71 asked for a decision record before the constant becomes part of the public surface.
The two parsers of pkg/kernel had no bound. ParseRequirement and ParseArtifactManifest returned the text of encoding/json unchanged, so a document with a 5000-byte unknown field name produced an error of more than 5000 bytes. LoadRequirement and LoadArtifactManifest repeated the caller's path. The correlation prefix of ADR-0021 (<stream>@<version> for the manifest, the correlation id for the requirement) echoed the raw value even when that value had failed its own check, so a 4000-byte stream reached the log line in full.
Decision¶
- One bound.
pkg/reasonholds the bound for every contract parser:reason.MaxLen= 256 bytes andreason.Bound, which keeps a text of at mostMaxLenbytes unchanged and otherwise cuts it toMaxLenbytes ending withreason.TruncatedMark, which is a space followed by(truncated). The cut never splits a UTF-8 character. A new parser callsreason.Boundinstead of carrying its own copy (HISS-19). aegis.MaxReasonLenstays exported and equalsreason.MaxLen. It is the bound ADR-0020 point 3 applies toError.Reason; the cause inError.Erris unchanged.- Every error an exported function of
pkg/kernelreturns is bounded. The text is cut byreason.Bound; the full cause stays in the chain, soerrors.Isanderrors.AsreachErrEmptyRequirement,ErrTooLarge,fs.ErrNotExistand the verification sentinels as before. - A correlation value that failed its check is not echoed. In the prefix of a
pkg/kernelerror, an empty correlation id, stream or version reads(missing)and one that fails its own check reads(invalid), the placeholders ADR-0020 already uses forpkg/aegis. A manifest with neither stream nor version keeps the field-only form. - Tests hold the bound.
pkg/reason/reason_test.gochecks the boundary atMaxLenandMaxLen + 1and a cut inside a multi-byte character.pkg/kernel/reason_test.gochecks long unknown fields, long paths, invalid correlation values and the kept sentinels.FuzzParseRequirementandFuzzParseArtifactManifestfail on any error text longer thanreason.MaxLen.
Consequences¶
- Positive: no rejection text of either contract grows with the input, and no value that failed validation reaches a log line.
- Positive: one implementation of the cut; the aegis and kernel bounds cannot drift apart.
- Negative: an error longer than 256 bytes loses its tail in the text. The cause in the chain keeps the full detail for a caller that needs it.
- Neutral: callers that matched on the raw correlation prefix of an invalid value see the placeholder instead; no caller in this repository does.