JuliusBrussee/caveman · error
proposalrun: canonicalize detail
Error message
proposalrun: canonicalize detail: %w
What it means
CanonicalDetail wraps any failure from the recursive canonicalization of a decoded JSON value (canonicalizeJSONValue). That recursion only fails when canonicalizing a json.Number via normalizeJSONNumber, so this wrapper always carries an inner error like 'number exceeds the supported JSONB numeric size', 'invalid JSON number', or a PostgreSQL digit/exponent limit error. The library rejects such numbers at the hash boundary because a value that PostgreSQL jsonb (numeric-backed) could not persist would make write-time and read-back hashes diverge, silently breaking the audit hash chain.
Solutions
- Inspect the wrapped inner error (errors.Unwrap / %w chain) to see which specific rule fired: size, grammar, or a PostgreSQL numeric bound.
- Find the offending number in the detail blob and round or clamp it before hashing (e.g. limit exponent magnitude, trim fraction digits to <=16383, cap integer digits to <=131072).
- Validate the detail payload with a bounded-number check before passing it to RowHash/CanonicalDetail so malformed or oversized literals never reach the hash boundary.
- If the number is hostile/untrusted input, reject the whole record at the API boundary instead of attempting to canonicalize it.
Example fix
// before
canon, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts) // detail has a 200000-digit number
// after
if err := boundDetailNumbers(detail); err != nil {
return fmt.Errorf("reject detail: %w", err)
}
canon, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts) Defensive patterns
Strategy: validation
Validate before calling
func detailNumbersWithinBounds(detail json.RawMessage) error {
var v any
dec := json.NewDecoder(bytes.NewReader(detail))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {
return err
}
return jsonWalkNumbers(v, func(n json.Number) error {
lit := n.String()
if len(lit) == 0 || len(lit) > 147456 {
return fmt.Errorf("number too large: %d chars", len(lit))
}
if i := strings.IndexAny(lit[1:], "eE"); i >= 0 {
if exp := lit[i+2:]; len(exp) > 6 {
return fmt.Errorf("exponent too large")
}
}
return nil
})
} Try / catch
hash, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts)
if err != nil {
var inner error = errors.Unwrap(err)
log.Printf("detail hash failed (inner=%v), rejecting record seq=%d", inner, seq)
return fmt.Errorf("reject proposal run: %w", err)
} Prevention
- Round or clamp telemetry numbers (exponent and digit counts) before embedding them in detail blobs.
- Serialize arbitrary-precision values as JSON strings, never as JSON numbers.
- Validate detail payloads at the ingestion API boundary, before they reach the hash layer.
- Add a unit test with adversarial numbers (huge exponents, thousands of digits) to catch regressions early.
When it happens
Trigger: Calling CanonicalDetail(detail) (or RowHash with a non-empty detail) where the detail JSON contains a number that normalizeJSONNumber rejects: an empty or >147456-character numeric lexeme, a bare '-' with no digits, multiple 'e'/'E' markers, non-digit mantissa characters, a fraction scale above 16383 digits, an integer part above 131072 digits, or an exponent magnitude above 147455.
Common situations: Instrumentation or telemetry code embedding host-supplied numbers (e.g. computed big-number strings, hostile payloads from external APIs) into tool-call detail blobs; hand-built detail JSON with malformed number literals that a lenient producer allowed; upstream version changes that now emit numbers with extreme exponents or thousands of digits.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- number exceeds the supported JSONB numeric size
- proposalrun: marshal canonical detail
- envelope: encode scope
- envelope: marshal metadata
- envelope: parse metadata
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/db4af63218ff3f57.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/proposalrun/proposalrun.go:75
if len(detail) == 0 {
return []byte("{}"), nil
}
var v any
dec := json.NewDecoder(bytes.NewReader(detail))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {
return nil, fmt.Errorf("proposalrun: detail is not valid JSON: %w", err)
}
var trailing any
if err := dec.Decode(&trailing); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("proposalrun: detail contains trailing JSON")
}
return nil, fmt.Errorf("proposalrun: detail contains trailing data: %w", err)
}
v, err := canonicalizeJSONValue(v)
if err != nil {
return nil, fmt.Errorf("proposalrun: canonicalize detail: %w", err)
}
out, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("proposalrun: marshal canonical detail: %w", err)
}
return out, nil
}
// PostgreSQL jsonb stores JSON numbers using its numeric type. These are the
// documented numeric bounds (131072 digits before and 16383 after the decimal
// point); rejecting at the canonicalization boundary keeps write and read-back
// hashing deterministic and avoids unbounded strings from hostile exponents.
// maxJSONBNumberExponent is an additional allocation guard. PostgreSQL can
// accept some zero literals with a larger positive exponent after normalizing
// their zero weight; this package rejects those spellings conservatively so a
// hostile exponent never drives unbounded intermediate arithmetic.
const (
maxJSONBNumericIntegerDigits = 131072View on GitHub (pinned to 3ee70a1026)