diff --git a/cmd/launch/openclaw.go b/cmd/launch/openclaw.go
index d5980a532..b86034797 100644
--- a/cmd/launch/openclaw.go
+++ b/cmd/launch/openclaw.go
@@ -14,8 +14,6 @@ import (
"strings"
"time"
- "golang.org/x/mod/semver"
-
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/cmd/internal/fileutil"
"github.com/ollama/ollama/envconfig"
@@ -98,9 +96,7 @@ func (c *Openclaw) Run(model string, args []string) error {
patchDeviceScopes()
}
- if ensureWebSearchPlugin() {
- registerWebSearchPlugin()
- }
+ configureOllamaWebSearch()
// When extra args are passed through, run exactly what the user asked for
// after setup and skip the built-in gateway+TUI convenience flow.
@@ -738,89 +734,13 @@ func clearSessionModelOverride(primary string) {
_ = os.WriteFile(path, out, 0o600)
}
-const (
- webSearchNpmPackage = "@ollama/openclaw-web-search"
- webSearchMinVersion = "0.2.1"
-)
-
-// ensureWebSearchPlugin installs the openclaw-web-search extension into the
-// user-level extensions directory (~/.openclaw/extensions/) if it isn't already
-// present, or re-installs if the installed version is older than webSearchMinVersion.
-// Returns true if the extension is available.
-func ensureWebSearchPlugin() bool {
- home, err := os.UserHomeDir()
- if err != nil {
- return false
- }
-
- pluginDir := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
- if webSearchPluginUpToDate(pluginDir) {
- return true
- }
-
- npmBin, err := exec.LookPath("npm")
- if err != nil {
- return false
- }
-
- if err := os.MkdirAll(pluginDir, 0o755); err != nil {
- return false
- }
-
- // Download the tarball via `npm pack`, extract it flat into the plugin dir.
- pack := exec.Command(npmBin, "pack", webSearchNpmPackage, "--pack-destination", pluginDir)
- out, err := pack.Output()
- if err != nil {
- fmt.Fprintf(os.Stderr, "%s Warning: could not download web search plugin: %v%s\n", ansiYellow, err, ansiReset)
- return false
- }
-
- tgzName := strings.TrimSpace(string(out))
- tgzPath := filepath.Join(pluginDir, tgzName)
- defer os.Remove(tgzPath)
-
- tar := exec.Command("tar", "xzf", tgzPath, "--strip-components=1", "-C", pluginDir)
- if err := tar.Run(); err != nil {
- fmt.Fprintf(os.Stderr, "%s Warning: could not extract web search plugin: %v%s\n", ansiYellow, err, ansiReset)
- return false
- }
-
- fmt.Fprintf(os.Stderr, "%s ✓ Installed Ollama web search %s\n", ansiGreen, ansiReset)
- return true
-}
-
-// webSearchPluginUpToDate returns true if the plugin is installed and its
-// package.json version is >= webSearchMinVersion.
-func webSearchPluginUpToDate(pluginDir string) bool {
- data, err := os.ReadFile(filepath.Join(pluginDir, "package.json"))
- if err != nil {
- return false
- }
- var pkg struct {
- Version string `json:"version"`
- }
- if json.Unmarshal(data, &pkg) != nil || pkg.Version == "" {
- return false
- }
- return !versionLessThan(pkg.Version, webSearchMinVersion)
-}
-
-// versionLessThan compares two semver version strings (major.minor.patch).
-// Inputs may omit the "v" prefix; it is added automatically for semver.Compare.
-func versionLessThan(a, b string) bool {
- if !strings.HasPrefix(a, "v") {
- a = "v" + a
- }
- if !strings.HasPrefix(b, "v") {
- b = "v" + b
- }
- return semver.Compare(a, b) < 0
-}
-
-// registerWebSearchPlugin adds plugins.entries.openclaw-web-search to the OpenClaw
-// config so the gateway activates it on next start. Best-effort; silently returns
-// on any error.
-func registerWebSearchPlugin() {
+// configureOllamaWebSearch keeps launch-managed OpenClaw installs on the
+// bundled Ollama web_search provider. Older launch builds installed an
+// external openclaw-web-search plugin that added custom ollama_web_search and
+// ollama_web_fetch tools. Current OpenClaw versions ship Ollama web_search as
+// the bundled "ollama" plugin instead, so we migrate stale config and ensure
+// fresh installs select the bundled provider.
+func configureOllamaWebSearch() {
home, err := os.UserHomeDir()
if err != nil {
return
@@ -835,6 +755,8 @@ func registerWebSearchPlugin() {
return
}
+ stalePluginConfigured := false
+
plugins, _ := config["plugins"].(map[string]any)
if plugins == nil {
plugins = make(map[string]any)
@@ -843,68 +765,100 @@ func registerWebSearchPlugin() {
if entries == nil {
entries = make(map[string]any)
}
- entries["openclaw-web-search"] = map[string]any{"enabled": true}
- plugins["entries"] = entries
-
- // Pin trust so the gateway doesn't warn about untracked plugins.
- allow, _ := plugins["allow"].([]any)
- hasAllow := false
- for _, v := range allow {
- if s, ok := v.(string); ok && s == "openclaw-web-search" {
- hasAllow = true
- break
- }
- }
- if !hasAllow {
- allow = append(allow, "openclaw-web-search")
- }
- plugins["allow"] = allow
-
- // Record install provenance so the loader can verify the plugin origin.
- installs, _ := plugins["installs"].(map[string]any)
- if installs == nil {
- installs = make(map[string]any)
- }
- pluginDir := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
- installs["openclaw-web-search"] = map[string]any{
- "source": "npm",
- "spec": webSearchNpmPackage,
- "installPath": pluginDir,
- }
- plugins["installs"] = installs
-
- config["plugins"] = plugins
-
- // Add plugin tools to tools.alsoAllow so they survive the coding profile's
- // policy pipeline (which has an explicit allow list of core tools only).
tools, _ := config["tools"].(map[string]any)
if tools == nil {
tools = make(map[string]any)
}
-
- alsoAllow, _ := tools["alsoAllow"].([]any)
- needed := []string{"ollama_web_search", "ollama_web_fetch"}
- have := make(map[string]bool, len(alsoAllow))
- for _, v := range alsoAllow {
- if s, ok := v.(string); ok {
- have[s] = true
- }
- }
- for _, name := range needed {
- if !have[name] {
- alsoAllow = append(alsoAllow, name)
- }
- }
- tools["alsoAllow"] = alsoAllow
-
- // Disable built-in web search/fetch since our plugin replaces them.
web, _ := tools["web"].(map[string]any)
if web == nil {
web = make(map[string]any)
}
- web["search"] = map[string]any{"enabled": false}
- web["fetch"] = map[string]any{"enabled": false}
+ search, _ := web["search"].(map[string]any)
+ if search == nil {
+ search = make(map[string]any)
+ }
+ fetch, _ := web["fetch"].(map[string]any)
+ if fetch == nil {
+ fetch = make(map[string]any)
+ }
+
+ alsoAllow, _ := tools["alsoAllow"].([]any)
+ var filteredAlsoAllow []any
+ for _, v := range alsoAllow {
+ s, ok := v.(string)
+ if !ok {
+ filteredAlsoAllow = append(filteredAlsoAllow, v)
+ continue
+ }
+ if s == "ollama_web_search" || s == "ollama_web_fetch" {
+ stalePluginConfigured = true
+ continue
+ }
+ filteredAlsoAllow = append(filteredAlsoAllow, v)
+ }
+ if len(filteredAlsoAllow) > 0 {
+ tools["alsoAllow"] = filteredAlsoAllow
+ } else {
+ delete(tools, "alsoAllow")
+ }
+
+ if _, ok := entries["openclaw-web-search"]; ok {
+ delete(entries, "openclaw-web-search")
+ stalePluginConfigured = true
+ }
+ ollamaEntry, _ := entries["ollama"].(map[string]any)
+ if ollamaEntry == nil {
+ ollamaEntry = make(map[string]any)
+ }
+ ollamaEntry["enabled"] = true
+ entries["ollama"] = ollamaEntry
+ plugins["entries"] = entries
+
+ if allow, ok := plugins["allow"].([]any); ok {
+ var nextAllow []any
+ hasOllama := false
+ for _, v := range allow {
+ s, ok := v.(string)
+ if ok && s == "openclaw-web-search" {
+ stalePluginConfigured = true
+ continue
+ }
+ if ok && s == "ollama" {
+ hasOllama = true
+ }
+ nextAllow = append(nextAllow, v)
+ }
+ if !hasOllama {
+ nextAllow = append(nextAllow, "ollama")
+ }
+ plugins["allow"] = nextAllow
+ }
+
+ if installs, ok := plugins["installs"].(map[string]any); ok {
+ if _, exists := installs["openclaw-web-search"]; exists {
+ delete(installs, "openclaw-web-search")
+ stalePluginConfigured = true
+ }
+ if len(installs) > 0 {
+ plugins["installs"] = installs
+ } else {
+ delete(plugins, "installs")
+ }
+ }
+
+ if stalePluginConfigured || search["provider"] == nil {
+ search["provider"] = "ollama"
+ }
+ if stalePluginConfigured {
+ fetch["enabled"] = true
+ }
+ search["enabled"] = true
+ web["search"] = search
+ if len(fetch) > 0 {
+ web["fetch"] = fetch
+ }
tools["web"] = web
+ config["plugins"] = plugins
config["tools"] = tools
out, err := json.MarshalIndent(config, "", " ")
diff --git a/cmd/launch/openclaw_test.go b/cmd/launch/openclaw_test.go
index 434496bae..b25c2e618 100644
--- a/cmd/launch/openclaw_test.go
+++ b/cmd/launch/openclaw_test.go
@@ -2242,95 +2242,7 @@ func TestIntegrationOnboarded(t *testing.T) {
})
}
-func TestVersionLessThan(t *testing.T) {
- tests := []struct {
- a, b string
- want bool
- }{
- {"0.1.7", "0.2.1", true},
- {"0.2.0", "0.2.1", true},
- {"0.2.1", "0.2.1", false},
- {"0.2.2", "0.2.1", false},
- {"1.0.0", "0.2.1", false},
- {"0.2.1", "1.0.0", true},
- {"v0.1.7", "0.2.1", true},
- {"0.2.1", "v0.2.1", false},
- }
- for _, tt := range tests {
- t.Run(tt.a+"_vs_"+tt.b, func(t *testing.T) {
- if got := versionLessThan(tt.a, tt.b); got != tt.want {
- t.Errorf("versionLessThan(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
- }
- })
- }
-}
-
-func TestWebSearchPluginUpToDate(t *testing.T) {
- t.Run("missing directory", func(t *testing.T) {
- if webSearchPluginUpToDate(filepath.Join(t.TempDir(), "nonexistent")) {
- t.Error("expected false for missing directory")
- }
- })
-
- t.Run("missing package.json", func(t *testing.T) {
- dir := t.TempDir()
- if webSearchPluginUpToDate(dir) {
- t.Error("expected false for missing package.json")
- }
- })
-
- t.Run("old version", func(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"0.1.7"}`), 0o644); err != nil {
- t.Fatal(err)
- }
- if webSearchPluginUpToDate(dir) {
- t.Error("expected false for old version 0.1.7")
- }
- })
-
- t.Run("exact minimum version", func(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"0.2.1"}`), 0o644); err != nil {
- t.Fatal(err)
- }
- if !webSearchPluginUpToDate(dir) {
- t.Error("expected true for exact minimum version 0.2.1")
- }
- })
-
- t.Run("newer version", func(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":"1.0.0"}`), 0o644); err != nil {
- t.Fatal(err)
- }
- if !webSearchPluginUpToDate(dir) {
- t.Error("expected true for newer version 1.0.0")
- }
- })
-
- t.Run("invalid json", func(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`not json`), 0o644); err != nil {
- t.Fatal(err)
- }
- if webSearchPluginUpToDate(dir) {
- t.Error("expected false for invalid json")
- }
- })
-
- t.Run("empty version", func(t *testing.T) {
- dir := t.TempDir()
- if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"version":""}`), 0o644); err != nil {
- t.Fatal(err)
- }
- if webSearchPluginUpToDate(dir) {
- t.Error("expected false for empty version")
- }
- })
-}
-
-func TestRegisterWebSearchPlugin(t *testing.T) {
+func TestConfigureOllamaWebSearch(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
@@ -2345,7 +2257,7 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
t.Fatal(err)
}
- registerWebSearchPlugin()
+ configureOllamaWebSearch()
data, err := os.ReadFile(configPath)
if err != nil {
@@ -2361,40 +2273,30 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
t.Fatal("plugins section missing")
}
- // Check entries
entries, _ := plugins["entries"].(map[string]any)
- entry, _ := entries["openclaw-web-search"].(map[string]any)
+ entry, _ := entries["ollama"].(map[string]any)
if enabled, _ := entry["enabled"].(bool); !enabled {
- t.Error("expected entries.openclaw-web-search.enabled = true")
+ t.Error("expected entries.ollama.enabled = true")
+ }
+ if _, ok := entries["openclaw-web-search"]; ok {
+ t.Error("expected stale openclaw-web-search entry to be absent")
}
- // Check allow list
- allow, _ := plugins["allow"].([]any)
- found := false
- for _, v := range allow {
- if s, ok := v.(string); ok && s == "openclaw-web-search" {
- found = true
- }
+ if _, ok := plugins["allow"]; ok {
+ t.Error("did not expect plugins.allow to be created when no allowlist exists")
}
- if !found {
- t.Error("expected plugins.allow to contain openclaw-web-search")
+ if _, ok := plugins["installs"]; ok {
+ t.Error("did not expect plugins.installs to be created")
}
- // Check install provenance
- installs, _ := plugins["installs"].(map[string]any)
- record, _ := installs["openclaw-web-search"].(map[string]any)
- if record == nil {
- t.Fatal("expected plugins.installs.openclaw-web-search")
+ tools, _ := config["tools"].(map[string]any)
+ web, _ := tools["web"].(map[string]any)
+ search, _ := web["search"].(map[string]any)
+ if got, _ := search["provider"].(string); got != "ollama" {
+ t.Errorf("search provider = %q, want %q", got, "ollama")
}
- if source, _ := record["source"].(string); source != "npm" {
- t.Errorf("install source = %q, want %q", source, "npm")
- }
- if spec, _ := record["spec"].(string); spec != webSearchNpmPackage {
- t.Errorf("install spec = %q, want %q", spec, webSearchNpmPackage)
- }
- expectedPath := filepath.Join(home, ".openclaw", "extensions", "openclaw-web-search")
- if installPath, _ := record["installPath"].(string); installPath != expectedPath {
- t.Errorf("installPath = %q, want %q", installPath, expectedPath)
+ if enabled, _ := search["enabled"].(bool); !enabled {
+ t.Error("expected tools.web.search.enabled = true")
}
})
@@ -2403,8 +2305,8 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
t.Fatal(err)
}
- registerWebSearchPlugin()
- registerWebSearchPlugin()
+ configureOllamaWebSearch()
+ configureOllamaWebSearch()
data, err := os.ReadFile(configPath)
if err != nil {
@@ -2416,30 +2318,39 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
}
plugins, _ := config["plugins"].(map[string]any)
- allow, _ := plugins["allow"].([]any)
- count := 0
- for _, v := range allow {
- if s, ok := v.(string); ok && s == "openclaw-web-search" {
- count++
- }
+ entries, _ := plugins["entries"].(map[string]any)
+ if len(entries) != 1 {
+ t.Fatalf("expected only bundled ollama entry, got %v", entries)
}
- if count != 1 {
- t.Errorf("expected exactly 1 openclaw-web-search in allow, got %d", count)
+ if _, ok := entries["ollama"]; !ok {
+ t.Fatalf("expected entries.ollama to exist, got %v", entries)
}
})
- t.Run("preserves existing config", func(t *testing.T) {
+ t.Run("migrates stale plugin config and preserves unrelated settings", func(t *testing.T) {
initial := map[string]any{
"plugins": map[string]any{
- "allow": []any{"some-other-plugin"},
+ "allow": []any{"some-other-plugin", "openclaw-web-search"},
"entries": map[string]any{
- "some-other-plugin": map[string]any{"enabled": true},
+ "some-other-plugin": map[string]any{"enabled": true},
+ "openclaw-web-search": map[string]any{"enabled": true},
},
"installs": map[string]any{
"some-other-plugin": map[string]any{
"source": "npm",
"installPath": "/some/path",
},
+ "openclaw-web-search": map[string]any{
+ "source": "npm",
+ "installPath": "/old/path",
+ },
+ },
+ },
+ "tools": map[string]any{
+ "alsoAllow": []any{"ollama_web_search", "ollama_web_fetch", "browser"},
+ "web": map[string]any{
+ "search": map[string]any{"enabled": false},
+ "fetch": map[string]any{"enabled": false},
},
},
"customField": "preserved",
@@ -2449,7 +2360,7 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
t.Fatal(err)
}
- registerWebSearchPlugin()
+ configureOllamaWebSearch()
out, err := os.ReadFile(configPath)
if err != nil {
@@ -2469,28 +2380,61 @@ func TestRegisterWebSearchPlugin(t *testing.T) {
if entries["some-other-plugin"] == nil {
t.Error("existing plugin entry was lost")
}
+ if entries["openclaw-web-search"] != nil {
+ t.Error("stale openclaw-web-search entry should be removed")
+ }
+ if ollamaEntry, _ := entries["ollama"].(map[string]any); ollamaEntry == nil {
+ t.Fatal("expected bundled ollama entry to be enabled")
+ }
installs, _ := plugins["installs"].(map[string]any)
if installs["some-other-plugin"] == nil {
t.Error("existing install record was lost")
}
+ if installs["openclaw-web-search"] != nil {
+ t.Error("stale openclaw-web-search install record should be removed")
+ }
allow, _ := plugins["allow"].([]any)
- hasOther, hasWebSearch := false, false
+ hasOther, hasStalePlugin, hasOllama := false, false, false
for _, v := range allow {
s, _ := v.(string)
if s == "some-other-plugin" {
hasOther = true
}
if s == "openclaw-web-search" {
- hasWebSearch = true
+ hasStalePlugin = true
+ }
+ if s == "ollama" {
+ hasOllama = true
}
}
if !hasOther {
t.Error("existing allow entry was lost")
}
- if !hasWebSearch {
- t.Error("openclaw-web-search not added to allow")
+ if hasStalePlugin {
+ t.Error("stale openclaw-web-search allow entry should be removed")
+ }
+ if !hasOllama {
+ t.Error("expected plugins.allow to contain bundled ollama plugin")
+ }
+
+ tools, _ := config["tools"].(map[string]any)
+ alsoAllow, _ := tools["alsoAllow"].([]any)
+ if len(alsoAllow) != 1 || alsoAllow[0] != "browser" {
+ t.Errorf("expected stale custom web tools to be removed, got %v", alsoAllow)
+ }
+ web, _ := tools["web"].(map[string]any)
+ search, _ := web["search"].(map[string]any)
+ fetch, _ := web["fetch"].(map[string]any)
+ if got, _ := search["provider"].(string); got != "ollama" {
+ t.Errorf("search provider = %q, want %q", got, "ollama")
+ }
+ if enabled, _ := search["enabled"].(bool); !enabled {
+ t.Error("expected migrated tools.web.search.enabled = true")
+ }
+ if enabled, _ := fetch["enabled"].(bool); !enabled {
+ t.Error("expected migrated tools.web.fetch.enabled = true")
}
})
}
diff --git a/docs/integrations/openclaw.mdx b/docs/integrations/openclaw.mdx
index 10df4a1c1..ff5f91e26 100644
--- a/docs/integrations/openclaw.mdx
+++ b/docs/integrations/openclaw.mdx
@@ -15,7 +15,7 @@ Ollama handles everything automatically:
1. **Install** — If OpenClaw isn't installed, Ollama prompts to install it via npm
2. **Security** — On the first launch, a security notice explains the risks of tool access
3. **Model** — Pick a model from the selector (local or cloud)
-4. **Onboarding** — Ollama configures the provider, installs the gateway daemon, sets your model as the primary, and installs the web search and fetch plugin
+4. **Onboarding** — Ollama configures the provider, installs the gateway daemon, sets your model as the primary, and enables OpenClaw's bundled Ollama web search
5. **Gateway** — Starts in the background and opens the OpenClaw TUI
OpenClaw requires a larger context window. It is recommended to use a context window of at least 64k tokens if using local models. See [Context length](/context-length) for more information.
@@ -24,19 +24,19 @@ Ollama handles everything automatically:
## Web search and fetch
-OpenClaw ships with a web search and fetch plugin that gives local or cloud models the ability to search the web and extract readable page content.
+OpenClaw ships with a bundled Ollama `web_search` provider that lets local or cloud-backed Ollama setups search the web through the configured Ollama host.
```bash
ollama launch openclaw
```
-Web search and fetch is enabled automatically when launching OpenClaw through Ollama. To install the plugin directly:
+Ollama web search is enabled automatically when launching OpenClaw through Ollama. To configure it manually:
```bash
-openclaw plugins install @ollama/openclaw-web-search
+openclaw configure --section web
```
-Web search for local models requires `ollama signin`.
+Ollama web search for local models requires `ollama signin`.
## Configure without launching
@@ -93,4 +93,3 @@ Link WhatsApp, Telegram, Slack, Discord, or iMessage to chat with your local mod
```bash
openclaw gateway stop
```
-