feat(web_search): SearXNG-backed web_search tool + Docker sidecar#24
Conversation
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>
… 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>
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>
… 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>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
odek | a1a1ad1 | Commit Preview URL Branch Preview URL |
Jun 10 2026, 09:35 AM |
vprotocol v5.2.7 — Verification CertificatePR: #24
Pre-scan (§0)Added lines scanned for injection markers / verdict tokens / new exec sinks: clean. The untrusted→agent path (SERP results) is nonce-wrapped via Nine Axes
η Derivation
η_raw = 0.685 · ρ = 0.24 (family +0.10, version +0.05, spec_independence +0.05, AST ~0.02, shared-mutants ~0.02) Verdict:
|
Summary
Gives the agent local-first web search with no cloud search API and no keys — the same zero-setup ethos as the bundled
transcribe(whisper) andvision(MiniCPM-V) tools. A new nativeweb_searchtool queries a self-hosted SearXNG metasearch instance over the internal Docker network and returns ranked results the agent then fetches withbrowser/http_batch.What's included
Native
web_searchtool (cmd/odek/web_search_tool.go)max_results.network_egress(prompt in restricted, allow in godmode), consistent withbrowser/http_batch.CheckRedirectguard re-classifies redirect hops so a compromised/misconfigured SearXNG can't 3xx toward internal/metadata endpoints.ECONNREFUSED(the precise "up but not listening" signal).web_search.base_urlis 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, mirroringVisionConfig.builtinToolsper-tool config params were bundled into atoolConfigstruct to stop positional-parameter churn as tools are added.Docker
searxngsidecar (pinned2026.6.8-f3fab143b), co-starting with every profile, internal-only (no host port),depends_onwired.docker/searxng/settings.ymlenables the JSON API and disables the anti-bot limiter → no Redis/Valkey needed.SEARXNG_SECRETin.env.example; both bundled configs setweb_search.base_url.Security
<untrusted_content>;web_search:<query>added to the SECURITY.md untrusted table.secret_keyis overridden at app load fromSEARXNG_SECRET(env override verified against SearXNG source); the placeholder is the canonicalultrasecretkeyso an unset secret triggers SearXNG's own warning.Tests
Hermetic
httptestSearXNG mock — happy path, max_results override vs config cap, untrusted wrapping, JSON-disabled 403, unreachable + cold-start retry, redirect-to-internal blocked, policy denial, heterogeneous/malformed answers (must not lose results), empty query, schema; plusresolveWebSearchdefaults/merge. Full suite green under-race;go vet/gofmtclean;docker compose configvalidates.Docs
README (new "🌐 Local Web Search" feature blurb), CHEATSHEET (config reference + standalone non-Docker recipe), SECURITY, CONFIG, TELEGRAM, docker/README, DOCKER_COMPOSE_USER_GUIDE.
Review trail
This branch was put through a high-effort
/code-reviewand a full vprotocol v5.2.7 verification; both rounds of confirmed findings were fixed in-branch (answers parsing, SSRF redirect guard, secret-warning visibility, cold-start resilience, answer-decode robustness, doc consistency). vprotocol verdict:HumanReviewRequired(η 0.445; single-model author of code+tests+review → correlated, ρ 0.24). An independent human review is the protocol-mandated next step, with two focus areas:docker compose upsmoke test is recommended before merge.🤖 Generated with Claude Code