From 3bd506bd1cf9933bf06867ea9681daeb6d6479a8 Mon Sep 17 00:00:00 2001 From: Parth Sareen Date: Tue, 14 Jul 2026 13:35:16 -0700 Subject: [PATCH] agent/tools: surface actionable web auth error (#17169) --- agent/tools/web.go | 11 +++----- agent/tools/web_test.go | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/agent/tools/web.go b/agent/tools/web.go index 1e819489d..a89681adb 100644 --- a/agent/tools/web.go +++ b/agent/tools/web.go @@ -13,17 +13,14 @@ import ( internalcloud "github.com/ollama/ollama/internal/cloud" ) -var ( - ErrWebSearchAuthRequired = errors.New("web search requires authentication") - ErrWebFetchAuthRequired = errors.New("web fetch requires authentication") -) - const ( maxWebFetchContentRunes = 60_000 webSearchTimeout = 15 * time.Second webFetchTimeout = 30 * time.Second ) +var ErrWebAuthRequired = errors.New("Not authenticated. Run `ollama signin` and try again.") + type WebSearch struct{} func (w *WebSearch) Name() string { @@ -76,7 +73,7 @@ func (w *WebSearch) Execute(ctx context.Context, _ agent.ToolContext, args map[s if err != nil { var authErr api.AuthorizationError if errors.As(err, &authErr) { - return agent.ToolResult{}, fmt.Errorf("%w: %s", ErrWebSearchAuthRequired, authErr) + return agent.ToolResult{}, ErrWebAuthRequired } return agent.ToolResult{}, err } @@ -160,7 +157,7 @@ func (w *WebFetch) Execute(ctx context.Context, _ agent.ToolContext, args map[st if err != nil { var authErr api.AuthorizationError if errors.As(err, &authErr) { - return agent.ToolResult{}, fmt.Errorf("%w: %s", ErrWebFetchAuthRequired, authErr) + return agent.ToolResult{}, ErrWebAuthRequired } return agent.ToolResult{}, err } diff --git a/agent/tools/web_test.go b/agent/tools/web_test.go index da155ce08..17c3cd238 100644 --- a/agent/tools/web_test.go +++ b/agent/tools/web_test.go @@ -2,6 +2,7 @@ package tools import ( "encoding/json" + "errors" "net/http" "net/http/httptest" "strings" @@ -20,6 +21,61 @@ func TestWebToolsRequireApproval(t *testing.T) { } } +var webToolCases = []struct { + name string + tool coreagent.Tool + args map[string]any + path string +}{ + {"search", &WebSearch{}, map[string]any{"query": "ollama"}, "/api/experimental/web_search"}, + {"fetch", &WebFetch{}, map[string]any{"url": "https://ollama.com"}, "/api/experimental/web_fetch"}, +} + +// runWebTool executes tool against a stub server that responds to every +// request with status and body, returning the resulting error. +func runWebTool(t *testing.T, tool coreagent.Tool, args map[string]any, path string, status int, body string) error { + t.Helper() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != path { + t.Fatalf("path = %q, want %q", r.URL.Path, path) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write([]byte(body)) + })) + t.Cleanup(ts.Close) + t.Setenv("OLLAMA_HOST", ts.URL) + _, err := tool.Execute(t.Context(), coreagent.ToolContext{}, args) + return err +} + +func TestWebToolsReportAuthenticationError(t *testing.T) { + for _, tt := range webToolCases { + t.Run(tt.name, func(t *testing.T) { + err := runWebTool(t, tt.tool, tt.args, tt.path, http.StatusUnauthorized, + `{"error":"unauthorized","signin_url":"https://ollama.com/signin"}`) + if !errors.Is(err, ErrWebAuthRequired) { + t.Fatalf("error = %v, want %v", err, ErrWebAuthRequired) + } + }) + } +} + +func TestWebToolsPreserveNonAuthenticationErrors(t *testing.T) { + for _, tt := range webToolCases { + t.Run(tt.name, func(t *testing.T) { + err := runWebTool(t, tt.tool, tt.args, tt.path, http.StatusTooManyRequests, + `{"error":"web search quota exceeded"}`) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "web search quota exceeded") { + t.Fatalf("error = %q, want original error message", err) + } + }) + } +} + func TestWebFetchRejectsUnsupportedScheme(t *testing.T) { tests := []struct { name string