Commit 3cf96f6
feat(web_search): SearXNG-backed web_search tool + Docker sidecar (#24)
* feat(web_search): add SearXNG-backed web_search tool + compose sidecar
Gives the agent local-first web search with no cloud API or keys, matching the
transcribe/vision zero-setup pattern.
Tool (cmd/odek/web_search_tool.go):
- Native Go tool querying a self-hosted SearXNG JSON API; returns ranked
results (title/url/snippet/engine) + direct answers, capped by max_results.
- Output wrapped as untrusted content (SERP snippets can carry injection).
- Gated as network_egress (prompt in restricted, allow in godmode), consistent
with browser/http_batch. The backend URL is fixed config, not agent-supplied,
so the tool has no SSRF surface (only a query string is accepted).
- Registered only when web_search.base_url is set, so plain installs without a
SearXNG instance don't see a dead tool.
Config (internal/config):
- WebSearchConfig{BaseURL, Categories, Language, MaxResults, Timeout} threaded
end-to-end (FileConfig, ResolvedConfig, resolveWebSearch, overlayFile).
Wiring (cmd/odek):
- builtinTools' growing positional config params (Transcription, Vision) are
bundled into a toolConfig struct to stop per-tool signature churn; all ~10
call sites updated. web_search is threaded into run/serve/repl/telegram/
schedule/subagent/mcp.
Docker:
- New `searxng` compose sidecar (pinned image), co-starting with every profile,
internal-only (no host port), with depends_on wired on each odek service.
- docker/searxng/settings.yml enables the JSON API and disables the anti-bot
limiter, so no Redis/Valkey is needed. SEARXNG_SECRET added to .env.example.
- Both bundled configs set web_search.base_url=http://searxng:8080.
Tests: hermetic httptest SearXNG mock covering happy path, max_results override
vs config cap, untrusted wrapping, JSON-disabled 403, unreachable backend,
policy denial, empty query, schema; resolveWebSearch defaults/merge. Full suite
green under -race.
Docs: README, SECURITY, CHEATSHEET, CONFIG, TELEGRAM, docker/README,
DOCKER_COMPOSE_USER_GUIDE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web_search): address code-review findings (answers, SSRF, secret, cold-start)
Code review of the SearXNG integration surfaced four actionable issues; fixes:
#2 (confirmed) — answers parsing emitted raw answer-object JSON. SearXNG
`answers` are objects ({"answer":"...","url":...,"template":...}), not bare
strings, so strings.Trim(rawJSON,'"') left the {...} blob intact. Decode the
`answer` field out of each object instead (also drops the fragile Trim). The
test mock used a string array, masking this — corrected to objects.
#3 (defense-in-depth) — the http.Client had no CheckRedirect. A compromised
or misconfigured SearXNG could 3xx the client toward an internal/metadata
endpoint (SSRF). Install the same per-hop re-classification guard browser and
http_batch use, capped at 10 hops. New TestWebSearch_RedirectToInternalBlocked.
#1 (hardening) — the mounted settings.yml hardcoded secret_key
"change-me-in-dot-env". Deeper tracing showed SEARXNG_SECRET *does* override
the file at app load (searx/settings_defaults.py environ_name), so the env
wiring worked — but the placeholder defeated SearXNG's built-in "secret_key
is not changed" warning, which only fires for the canonical "ultrasecretkey".
Switch the placeholder + the compose env fallback to "ultrasecretkey" so an
unset secret is loudly flagged rather than silently weak. Comments/.env.example
corrected to describe the real (app-load) override mechanism.
#4 (reliability) — `depends_on: [searxng]` only waits for container start, so
the first web_search after `compose up` could race SearXNG readiness. Rather
than a Docker healthcheck (whose probe tooling I can't verify in the upstream
image — a broken probe would deadlock odek startup), make the tool resilient:
retry only on ECONNREFUSED (the precise "up but not yet listening" signal),
2 extra attempts with a 1s backoff. Timeouts / genuine-down fail fast.
#5 (overlay whole-pointer replace) intentionally deferred — it is consistent
with the existing Skills/Memory/Transcription/Vision merge behavior; fixing
only web_search would be inconsistent.
Full suite green under -race; vet/gofmt clean; compose config validates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(web_search): surface in README + add standalone SearXNG recipe
Close two documentation gaps from the feature review:
- README discoverability: web_search was only mentioned in the prompt-injection
paragraph and absent from the feature list. Add a "🌐 Local Web Search"
strategic-feature blurb linking to the CHEATSHEET config reference.
- Non-Docker path: the CHEATSHEET only said "run SearXNG yourself" with no
recipe. Add a concrete `docker run` snippet (reusing the repo's ready-made
docker/searxng/settings.yml), the matching odek config, and the two settings
that matter (search.formats json + limiter off) for bring-your-own configs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web_search): vprotocol auto-repair — tolerant answer decode + doc consistency
vprotocol v5.2.7 verification of the feat/searxng-web-search branch. Three
confirmed findings repaired; others refuted (redirect hop-count matches the
httpBatch pattern; :ro mount is non-fatal per the SearXNG entrypoint; all
settings.yml keys verified valid).
F1 (robustness) — the typed `answers []struct{Answer string}` decode coupled
the critical `results` parse to the answers shape: a non-string "answer" value
(or any foreign answer type) would fail the whole json.Unmarshal and drop ALL
results. Restore the original immunity by keeping answers as []json.RawMessage
and decoding each element tolerantly — a non-conforming answer is skipped, never
fatal. New TestWebSearch_HeterogeneousAnswersDoNotLoseResults locks it in.
F2 (consistency) — the CHEATSHEET standalone recipe used searxng/searxng:latest
while compose pins 2026.6.8-f3fab143b. Pin the recipe to the same tag.
F3 (docs) — the standalone recipe's `$PWD/docker/searxng/...` volume path
assumed the repo root without saying so. State "run from the repo root".
Full suite green under -race; vet/gofmt clean; compose validates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 903e453 commit 3cf96f6
26 files changed
Lines changed: 852 additions & 21 deletions
File tree
- cmd/odek
- docker
- searxng
- docs
- internal/config
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
77 | 80 | | |
78 | 81 | | |
79 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
13 | 12 | | |
14 | 13 | | |
15 | 14 | | |
| |||
244 | 243 | | |
245 | 244 | | |
246 | 245 | | |
247 | | - | |
| 246 | + | |
248 | 247 | | |
249 | 248 | | |
250 | 249 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
779 | 779 | | |
780 | 780 | | |
781 | 781 | | |
782 | | - | |
| 782 | + | |
783 | 783 | | |
784 | 784 | | |
785 | 785 | | |
| |||
1054 | 1054 | | |
1055 | 1055 | | |
1056 | 1056 | | |
1057 | | - | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
1058 | 1068 | | |
1059 | 1069 | | |
1060 | 1070 | | |
| |||
1088 | 1098 | | |
1089 | 1099 | | |
1090 | 1100 | | |
1091 | | - | |
1092 | | - | |
| 1101 | + | |
| 1102 | + | |
1093 | 1103 | | |
1094 | 1104 | | |
1095 | 1105 | | |
| |||
1098 | 1108 | | |
1099 | 1109 | | |
1100 | 1110 | | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
1101 | 1118 | | |
1102 | 1119 | | |
1103 | 1120 | | |
| |||
1599 | 1616 | | |
1600 | 1617 | | |
1601 | 1618 | | |
1602 | | - | |
| 1619 | + | |
1603 | 1620 | | |
1604 | 1621 | | |
1605 | 1622 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
206 | | - | |
| 206 | + | |
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
76 | | - | |
| 76 | + | |
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
570 | 570 | | |
571 | 571 | | |
572 | 572 | | |
573 | | - | |
| 573 | + | |
574 | 574 | | |
575 | 575 | | |
576 | 576 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
270 | | - | |
| 270 | + | |
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
291 | 291 | | |
292 | 292 | | |
293 | 293 | | |
294 | | - | |
| 294 | + | |
295 | 295 | | |
296 | 296 | | |
297 | 297 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
16 | 15 | | |
17 | 16 | | |
18 | 17 | | |
| |||
320 | 319 | | |
321 | 320 | | |
322 | 321 | | |
323 | | - | |
| 322 | + | |
324 | 323 | | |
325 | 324 | | |
326 | 325 | | |
| |||
338 | 337 | | |
339 | 338 | | |
340 | 339 | | |
341 | | - | |
| 340 | + | |
342 | 341 | | |
343 | 342 | | |
344 | 343 | | |
| |||
432 | 431 | | |
433 | 432 | | |
434 | 433 | | |
435 | | - | |
| 434 | + | |
436 | 435 | | |
437 | 436 | | |
438 | 437 | | |
| |||
0 commit comments