From 9aa53444c73721d424a07f4198093f28b3def347 Mon Sep 17 00:00:00 2001 From: xiexian <3468714225@qq.com> Date: Fri, 2 Oct 2026 23:45:31 +0800 Subject: [PATCH] fix(sdk): declare only callable tool handlers --- .../ext/extension/sidecar/sdk_tools_test.go | 101 ++++++++++++++++++ sdk/go/tools.go | 6 +- sdk/go/tools_test.go | 19 ++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 internal/ext/extension/sidecar/sdk_tools_test.go diff --git a/internal/ext/extension/sidecar/sdk_tools_test.go b/internal/ext/extension/sidecar/sdk_tools_test.go new file mode 100644 index 0000000000..a762cc963a --- /dev/null +++ b/internal/ext/extension/sidecar/sdk_tools_test.go @@ -0,0 +1,101 @@ +package sidecar + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "reasonix/internal/base/testenv" + "reasonix/internal/ext/extension/protocol" + "reasonix/internal/ext/pluginpkg" +) + +func TestSDKNilToolsDoNotExceedManifest(t *testing.T) { + sdk, err := filepath.Abs(filepath.Join("..", "..", "..", "..", "sdk", "go")) + if err != nil { + t.Fatal(err) + } + source := testenv.TempDir(t) + main := filepath.Join(source, "main.go") + if err := os.WriteFile(main, []byte(`package main + +import ( + "context" + "encoding/json" + "os" + + extension "github.com/esengine/DeepSeek-Reasonix/sdk/go" +) + +type handler struct{} + +func (handler) Initialize(context.Context, extension.InitializeParams) (*extension.InitializeResult, error) { + return &extension.InitializeResult{}, nil +} + +func main() { + err := extension.Serve(context.Background(), handler{}, extension.Options{ + Name: "nil-tools", Version: "0.1.0", + Tools: map[string]extension.ToolFunc{ + "lookup": func(_ context.Context, args json.RawMessage) (string, error) { return string(args), nil }, + "disabled": nil, + }, + }) + if err != nil { os.Exit(1) } +} +`), 0o644); err != nil { + t.Fatal(err) + } + binary := filepath.Join(source, "nil-tools.exe") + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + cmd := exec.CommandContext(ctx, "go", "build", "-o", binary, main) + cmd.Dir = sdk + cmd.Env = append(os.Environ(), "GOWORK=off", "GOTOOLCHAIN=local", "GOPROXY=off") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("build SDK tool fixture: %v\n%s", err, out) + } + if err := os.WriteFile(filepath.Join(source, pluginpkg.NativeManifest), []byte(`{ + "apiVersion": "reasonix.io/plugin/v2", + "name": "nil-tools", + "version": "0.1.0", + "contributes": {}, + "runtime": { + "command": "${REASONIX_PLUGIN_ROOT}/nil-tools.exe", + "capabilities": ["tools"], + "tools": [{"name": "lookup", "description": "Echo arguments", "inputSchema": {"type": "object"}}] + } +}`), 0o644); err != nil { + t.Fatal(err) + } + pkg, _, err := pluginpkg.ParseDir(source) + if err != nil { + t.Fatal(err) + } + installed := pluginpkg.InstalledPlugin{Name: "nil-tools", Version: "0.1.0", Enabled: true, Root: source} + client, err := StartClient(t.Context(), ClientOptions{Package: pkg, Installed: installed, Session: testSessionContext()}) + if err != nil { + t.Fatalf("nil callback prevented the SDK's served tool from starting: %v", err) + } + defer func() { + if err := client.Close(); err != nil { + t.Error(err) + } + waitFor(t, "SDK tool fixture process exit", 10*time.Second, client.Exited) + }() + if tools := client.Handshake().Tools; len(tools) != 1 || tools[0] != "lookup" || client.ServesTool("disabled") { + t.Fatalf("served tools = %v, want only lookup", tools) + } + result, err := client.CallTool(t.Context(), "lookup", json.RawMessage(`{"q":"hello"}`), 5*time.Second) + if err != nil || result.IsError || result.Content != `{"q":"hello"}` { + t.Fatalf("served SDK tool = %+v, %v", result, err) + } + _, err = client.CallTool(t.Context(), "disabled", nil, 5*time.Second) + if err == nil || protocolReason(t, err) != protocol.ErrUnknownMethod { + t.Fatalf("nil SDK tool callback = %v, want unknown_method", err) + } +} diff --git a/sdk/go/tools.go b/sdk/go/tools.go index 1d68d4a62d..06ab60892e 100644 --- a/sdk/go/tools.go +++ b/sdk/go/tools.go @@ -19,8 +19,10 @@ func (s *server) declareTools(result *InitializeResult) { if len(result.Tools) > 0 || len(s.opts.Tools) == 0 { return } - for name := range s.opts.Tools { - result.Tools = append(result.Tools, name) + for name, run := range s.opts.Tools { + if run != nil { + result.Tools = append(result.Tools, name) + } } slices.Sort(result.Tools) } diff --git a/sdk/go/tools_test.go b/sdk/go/tools_test.go index ae970c53d3..fbb9430093 100644 --- a/sdk/go/tools_test.go +++ b/sdk/go/tools_test.go @@ -18,6 +18,7 @@ func TestToolsAreDeclaredAndServed(t *testing.T) { "broken": func(context.Context, json.RawMessage) (string, error) { return "", errors.New("the index is not built yet") }, + "disabled": nil, } host, _ := startFakeHost(t, basicHandler(), Options{Tools: tools}) init := host.handshake(t) @@ -38,4 +39,22 @@ func TestToolsAreDeclaredAndServed(t *testing.T) { if resp.Err == nil || resp.Err.Code != CodeMethodNotFound { t.Fatalf("an unserved tool = %+v, want unknown_method", resp.Err) } + resp = host.request(MethodExtensionToolCall, ToolCallParams{Name: "disabled", Arguments: json.RawMessage(`{}`)}) + if resp.Err == nil || resp.Err.Code != CodeMethodNotFound { + t.Fatalf("a nil tool handler = %+v, want unknown_method", resp.Err) + } +} + +func TestNilToolHandlersDoNotDeclareTools(t *testing.T) { + for _, tools := range []map[string]ToolFunc{nil, {}, {"disabled": nil}} { + host, _ := startFakeHost(t, basicHandler(), Options{Tools: tools}) + init := host.handshake(t) + if len(init.Tools) != 0 { + t.Fatalf("nil or empty handlers declared tools: %v", init.Tools) + } + resp := host.request(MethodExtensionToolCall, ToolCallParams{Name: "disabled", Arguments: json.RawMessage(`{}`)}) + if resp.Err == nil || resp.Err.Code != CodeMethodNotFound { + t.Fatalf("a nil tool handler = %+v, want unknown_method", resp.Err) + } + } }