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

  1. Inspect the wrapped inner error (errors.Unwrap / %w chain) to see which specific rule fired: size, grammar, or a PostgreSQL numeric bound.
  2. 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).
  3. 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.
  4. 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

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


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  = 131072

View on GitHub (pinned to 3ee70a1026)