From 4bcb04ad886c0c7b66e6822c08fed4a1f03bece1 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Fri, 22 Aug 2025 15:22:14 -0700 Subject: [PATCH] tools: avoid matching braces that are part of tool content (#12039) --- tools/tools.go | 33 +++++++++- tools/tools_test.go | 155 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 4 deletions(-) diff --git a/tools/tools.go b/tools/tools.go index f9ca15530b..80fc6e0d0c 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -224,22 +224,45 @@ func findArguments(buffer []byte) (map[string]any, int) { return nil, 0 } + start := -1 var braces int - var start int = -1 + var inString, escaped bool + + for i := range buffer { + c := buffer[i] + + if escaped { + escaped = false + continue + } + + if c == '\\' { + escaped = true + continue + } + + if c == '"' { + inString = !inString + continue + } + + if inString { + continue + } - for i, c := range buffer { if c == '{' { if braces == 0 { start = i } braces++ - } else if c == '}' && braces > 0 { + } else if c == '}' { braces-- if braces == 0 && start != -1 { object := buffer[start : i+1] var data map[string]any if err := json.Unmarshal(object, &data); err != nil { + // not a valid object, keep looking start = -1 continue } @@ -282,6 +305,10 @@ func findArguments(buffer []byte) (map[string]any, int) { return data, i } + + if braces < 0 { + braces = 0 + } } } diff --git a/tools/tools_test.go b/tools/tools_test.go index 7f00be2052..36b552c964 100644 --- a/tools/tools_test.go +++ b/tools/tools_test.go @@ -1,6 +1,7 @@ package tools import ( + "strings" "testing" "text/template" @@ -1140,11 +1141,163 @@ func TestFindArguments(t *testing.T) { }, { name: "deepseek", - buffer: []byte(`", "arguments": {"location": "Tokyo"}}`), + buffer: []byte(`"arguments": {"location": "Tokyo"}}`), want: map[string]any{ "location": "Tokyo", }, }, + { + name: "string with braces", + buffer: []byte(`{"name": "process_code", "arguments": {"code": "if (x > 0) { return true; }"}}`), + want: map[string]any{ + "code": "if (x > 0) { return true; }", + }, + }, + { + name: "string with nested json", + buffer: []byte(`{"name": "send_data", "arguments": {"payload": "{\"nested\": {\"key\": \"value\"}}"}}`), + want: map[string]any{ + "payload": `{"nested": {"key": "value"}}`, + }, + }, + { + name: "string with escaped quotes and braces", + buffer: []byte(`{"name": "analyze", "arguments": {"text": "The JSON is: {\"key\": \"val{ue}\"}"}}`), + want: map[string]any{ + "text": `The JSON is: {"key": "val{ue}"}`, + }, + }, + { + name: "multiple objects with string containing braces", + buffer: []byte(`{"name": "test", "arguments": {"query": "find } in text"}} {"name": "other"}`), + want: map[string]any{ + "query": "find } in text", + }, + }, + { + name: "unmatched closing brace in string", + buffer: []byte(`{"name": "search", "arguments": {"pattern": "regex: }"}}`), + want: map[string]any{ + "pattern": "regex: }", + }, + }, + { + name: "complex nested with mixed braces", + buffer: []byte(`{"name": "analyze", "arguments": {"data": "{\"items\": [{\"value\": \"}\"}, {\"code\": \"if (x) { return y; }\"}]}"}}`), + want: map[string]any{ + "data": `{"items": [{"value": "}"}, {"code": "if (x) { return y; }"}]}`, + }, + }, + { + name: "string with newline and braces", + buffer: []byte(`{"name": "format", "arguments": {"template": "{\n \"key\": \"value\"\n}"}}`), + want: map[string]any{ + "template": "{\n \"key\": \"value\"\n}", + }, + }, + { + name: "string with unicode escape", + buffer: []byte(`{"name": "test", "arguments": {"text": "Unicode: \u007B and \u007D"}}`), + want: map[string]any{ + "text": "Unicode: { and }", + }, + }, + { + name: "array arguments", + buffer: []byte(`{"name": "batch", "arguments": ["item1", "item2", "{\"nested\": true}"]}`), + want: nil, // This should return nil because arguments is not a map + }, + { + name: "escaped backslash before quote", + buffer: []byte(`{"name": "path", "arguments": {"dir": "C:\\Program Files\\{App}\\"}}`), + want: map[string]any{ + "dir": `C:\Program Files\{App}\`, + }, + }, + { + name: "single quotes not treated as string delimiters", + buffer: []byte(`{"name": "query", "arguments": {"sql": "SELECT * FROM users WHERE name = '{admin}'"}}`), + want: map[string]any{ + "sql": "SELECT * FROM users WHERE name = '{admin}'", + }, + }, + { + name: "incomplete json at buffer end", + buffer: []byte(`{"name": "test", "arguments": {"data": "some {"`), + want: nil, + }, + { + name: "multiple escaped quotes", + buffer: []byte(`{"name": "echo", "arguments": {"msg": "He said \"Hello {World}\" loudly"}}`), + want: map[string]any{ + "msg": `He said "Hello {World}" loudly`, + }, + }, + { + name: "json with comments style string", + buffer: []byte(`{"name": "code", "arguments": {"snippet": "// This is a comment with { and }"}}`), + want: map[string]any{ + "snippet": "// This is a comment with { and }", + }, + }, + { + name: "consecutive escaped backslashes", + buffer: []byte(`{"name": "test", "arguments": {"path": "C:\\\\{folder}\\\\"}}`), + want: map[string]any{ + "path": `C:\\{folder}\\`, + }, + }, + { + name: "empty string with braces after", + buffer: []byte(`{"name": "test", "arguments": {"a": "", "b": "{value}"}}`), + want: map[string]any{ + "a": "", + "b": "{value}", + }, + }, + { + name: "unicode in key names", + buffer: []byte(`{"name": "test", "arguments": {"key{": "value", "key}": "value2"}}`), + want: map[string]any{ + "key{": "value", + "key}": "value2", + }, + }, + { + name: "very long string with braces", + buffer: []byte(`{"name": "test", "arguments": {"data": "` + strings.Repeat("a{b}c", 100) + `"}}`), + want: map[string]any{ + "data": strings.Repeat("a{b}c", 100), + }, + }, + { + name: "tab characters and braces", + buffer: []byte(`{"name": "test", "arguments": {"code": "\tif (true) {\n\t\treturn;\n\t}"}}`), + want: map[string]any{ + "code": "\tif (true) {\n\t\treturn;\n\t}", + }, + }, + { + name: "null byte in string", + buffer: []byte(`{"name": "test", "arguments": {"data": "before\u0000{after}"}}`), + want: map[string]any{ + "data": "before\x00{after}", + }, + }, + { + name: "escaped quote at end of string", + buffer: []byte(`{"name": "test", "arguments": {"data": "text with quote at end\\\""}}`), + want: map[string]any{ + "data": `text with quote at end\"`, + }, + }, + { + name: "mixed array and object in arguments", + buffer: []byte(`{"name": "test", "arguments": {"items": ["{", "}", {"key": "value"}]}}`), + want: map[string]any{ + "items": []any{"{", "}", map[string]any{"key": "value"}}, + }, + }, } for _, tt := range tests {