From 709cc288eded77b578979c887d8bf28af0fa9a73 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 00:17:58 +0530 Subject: [PATCH 1/2] Preserve large-integer precision when decoding tool arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applySchema decoded incoming JSON arguments into an interface{} via json.Unmarshal, which turns every JSON number into a float64. Integer arguments beyond 2^53 (int64/uint64) were therefore silently truncated before being re-marshalled and handed to the typed handler — e.g. a tool called with {"n":9007199254740993} received 9007199254740992. Decode with json.Decoder.UseNumber() so numbers are preserved as json.Number and round-trip exactly. Only the input path (Format.Unmarshal) was affected; the output/Normalize path already operates on the typed value. Adds a black-box test asserting a 2^53+1 argument round-trips exactly. --- agent/format/jsonformat/encoding.go | 7 ++++++- agent/format/jsonformat/encoding_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/agent/format/jsonformat/encoding.go b/agent/format/jsonformat/encoding.go index f601bbd3..8a3f9d5d 100644 --- a/agent/format/jsonformat/encoding.go +++ b/agent/format/jsonformat/encoding.go @@ -3,6 +3,7 @@ package jsonformat import ( + "bytes" "encoding/json" "fmt" @@ -69,7 +70,11 @@ func (f *Format) Normalize(v any) error { func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawMessage, error) { var v any if len(data) > 0 { - if err := json.Unmarshal(data, &v); err != nil { + // Decode with UseNumber so integers beyond 2^53 are not silently + // truncated by being decoded into float64 and re-marshalled. + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + if err := dec.Decode(&v); err != nil { return nil, fmt.Errorf("unmarshaling arguments: %w", err) } } diff --git a/agent/format/jsonformat/encoding_test.go b/agent/format/jsonformat/encoding_test.go index 3371a4d7..675868e3 100644 --- a/agent/format/jsonformat/encoding_test.go +++ b/agent/format/jsonformat/encoding_test.go @@ -133,3 +133,19 @@ func TestNormalizeEmptyStruct(t *testing.T) { t.Fatalf("Normalize: %v", err) } } + +// Integer arguments beyond 2^53 must survive Unmarshal: decoding through an +// interface{} as float64 would silently truncate them. +func TestFormat_Unmarshal_PreservesLargeIntegerPrecision(t *testing.T) { + type output struct { + N int64 `json:"n"` + } + format := requireFormat(t, jsonformat.MustFor[output]()) + var out output + if err := format.Unmarshal([]byte(`{"n":9007199254740993}`), &out); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if out.N != 9007199254740993 { + t.Errorf("N = %d, want 9007199254740993 (large-integer precision lost)", out.N) + } +} From e8eb82b6fdc08fc1abeec4a07b3afd50f77177af Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 10:00:18 +0530 Subject: [PATCH 2/2] Reject trailing data after the JSON value in applySchema Address review feedback: switching to json.Decoder for UseNumber made applySchema tolerate trailing data after the first JSON value, unlike the json.Unmarshal it replaced. Restore strict single-value parsing by requiring io.EOF after the decoded value, and add a regression test. (The related suggestion to also decode with UseNumber inside validate() is not applied: json.Number is a string type that the jsonschema validator rejects as a non-integer, so it cannot be used there.) --- agent/format/jsonformat/encoding.go | 6 ++++++ agent/format/jsonformat/encoding_test.go | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/agent/format/jsonformat/encoding.go b/agent/format/jsonformat/encoding.go index 8a3f9d5d..5ba45a3f 100644 --- a/agent/format/jsonformat/encoding.go +++ b/agent/format/jsonformat/encoding.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "github.com/google/jsonschema-go/jsonschema" ) @@ -77,6 +78,11 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawM if err := dec.Decode(&v); err != nil { return nil, fmt.Errorf("unmarshaling arguments: %w", err) } + // Unlike json.Unmarshal, json.Decoder tolerates trailing data after the + // first value; reject it so validation is not looser than before. + if _, err := dec.Token(); err != io.EOF { + return nil, fmt.Errorf("unmarshaling arguments: unexpected trailing data after JSON value") + } } if err := resolved.ApplyDefaults(&v); err != nil { return nil, fmt.Errorf("applying schema defaults: %w", err) diff --git a/agent/format/jsonformat/encoding_test.go b/agent/format/jsonformat/encoding_test.go index 675868e3..191dadab 100644 --- a/agent/format/jsonformat/encoding_test.go +++ b/agent/format/jsonformat/encoding_test.go @@ -149,3 +149,14 @@ func TestFormat_Unmarshal_PreservesLargeIntegerPrecision(t *testing.T) { t.Errorf("N = %d, want 9007199254740993 (large-integer precision lost)", out.N) } } + +func TestFormat_Unmarshal_RejectsTrailingData(t *testing.T) { + type output struct { + N int `json:"n"` + } + format := requireFormat(t, jsonformat.MustFor[output]()) + var out output + if err := format.Unmarshal([]byte(`{"n":1} {"n":2}`), &out); err == nil { + t.Fatal("expected an error for trailing data after the JSON value, got nil") + } +}