{"record":{"id":"db4af63218ff3f57","repo":"JuliusBrussee/caveman","slug":"proposalrun-canonicalize-detail-w","errorCode":null,"errorMessage":"proposalrun: canonicalize detail: %w","messagePattern":"proposalrun: canonicalize detail: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":75,"sourceCode":"\tif len(detail) == 0 {\n\t\treturn []byte(\"{}\"), nil\n\t}\n\tvar v any\n\tdec := json.NewDecoder(bytes.NewReader(detail))\n\tdec.UseNumber()\n\tif err := dec.Decode(&v); err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: detail is not valid JSON: %w\", err)\n\t}\n\tvar trailing any\n\tif err := dec.Decode(&trailing); err != io.EOF {\n\t\tif err == nil {\n\t\t\treturn nil, fmt.Errorf(\"proposalrun: detail contains trailing JSON\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"proposalrun: detail contains trailing data: %w\", err)\n\t}\n\tv, err := canonicalizeJSONValue(v)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: canonicalize detail: %w\", err)\n\t}\n\tout, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: marshal canonical detail: %w\", err)\n\t}\n\treturn out, nil\n}\n\n// PostgreSQL jsonb stores JSON numbers using its numeric type. These are the\n// documented numeric bounds (131072 digits before and 16383 after the decimal\n// point); rejecting at the canonicalization boundary keeps write and read-back\n// hashing deterministic and avoids unbounded strings from hostile exponents.\n// maxJSONBNumberExponent is an additional allocation guard. PostgreSQL can\n// accept some zero literals with a larger positive exponent after normalizing\n// their zero weight; this package rejects those spellings conservatively so a\n// hostile exponent never drives unbounded intermediate arithmetic.\nconst (\n\tmaxJSONBNumericIntegerDigits  = 131072","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/proposalrun/proposalrun.go#L57-L93","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ncanon, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts) // detail has a 200000-digit number\n\n// after\nif err := boundDetailNumbers(detail); err != nil {\n    return fmt.Errorf(\"reject detail: %w\", err)\n}\ncanon, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts)","handlingStrategy":"validation","validationCode":"func detailNumbersWithinBounds(detail json.RawMessage) error {\n    var v any\n    dec := json.NewDecoder(bytes.NewReader(detail))\n    dec.UseNumber()\n    if err := dec.Decode(&v); err != nil {\n        return err\n    }\n    return jsonWalkNumbers(v, func(n json.Number) error {\n        lit := n.String()\n        if len(lit) == 0 || len(lit) > 147456 {\n            return fmt.Errorf(\"number too large: %d chars\", len(lit))\n        }\n        if i := strings.IndexAny(lit[1:], \"eE\"); i >= 0 {\n            if exp := lit[i+2:]; len(exp) > 6 {\n                return fmt.Errorf(\"exponent too large\")\n            }\n        }\n        return nil\n    })\n}","typeGuard":null,"tryCatchPattern":"hash, err := proposalrun.RowHash(prev, seq, action, detail, cost, ts)\nif err != nil {\n    var inner error = errors.Unwrap(err)\n    log.Printf(\"detail hash failed (inner=%v), rejecting record seq=%d\", inner, seq)\n    return fmt.Errorf(\"reject proposal run: %w\", err)\n}","preventionTips":["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."],"tags":["go","json","hash-chain","numeric-limits","audit"],"backgroundTag":"value-out-of-range","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}