searxng: wire paid Kagi engine with deploy-time key rendering

The stock SearXNG image ships a native kagi engine; enabling it needs an
API key that must never enter git, and SearXNG does not expand env vars
in settings.yml (the live secret_key literally was '${SEARXNG_SECRET}'
until now — the app tolerated the placeholder string silently).

- settings.yml: kagi engine enabled (weight 2, kg shortcut, paging off so
  exactly one API credit per aggregated query, timeout 8.0 vs 5.0 scrapers
  so a slow paid query is not a wasted credit); api_key stays ${KAGI_API_KEY}.
- new scripts/searxng-render-settings.py: deploy-time renderer, allowlisted
  to KAGI_API_KEY/SEARXNG_SECRET/SEARXNG_BASE_URL, fails the deploy loudly
  on an unresolvable placeholder, idempotent on redeploys.
- deploy-remote.sh copy_searxng_config runs the renderer against
  /opt/searxng/.env after copying settings.yml into /opt/searxng/config.
- KAGI_API_KEY placeholder added to dankclaw/.env.example (real key staged
  into the host-side /opt/searxng/.env, mode 600, out of band).
- bats: engine wiring, renderer substitution, loud-failure, deployer seam.
- docs: SearXNG.md Kagi section + renderer law; Providers.md carve-out on
  the no-credentials policy (paid engine acceptable when bounded + ordered).

Verified live on dankclaw: direct API probe via the gluetun netns (200,
data.search[] shape), then an aggregated query returned 29/29 results
tagged kagi, zero unresponsive engines.
This commit is contained in:
Frank
2026-09-26 23:25:55 -07:00
parent 4992e5a010
commit 7a8d992fce
7 changed files with 198 additions and 8 deletions
+48
View File
@@ -96,3 +96,51 @@ PYEOF
grep -q "/opt/searxng/config/limiter.toml" "$d" \
|| { echo "deploy-remote.sh does not place limiter.toml at /opt/searxng/config"; return 1; }
}
@test "kagi engine is wired enabled with an env-referenced key" {
python3 - "$REPO_ROOT/shared/searxng/settings.yml" <<'PYEOF'
import sys, yaml
with open(sys.argv[1]) as fh:
data = yaml.safe_load(fh)
engines = {entry["name"]: entry for entry in data.get("engines", [])}
assert "kagi" in engines, "kagi engine block missing from settings.yml"
k = engines["kagi"]
assert k["engine"] == "kagi", k
assert k["disabled"] is False, k
assert k["weight"] == 2, k
assert k["paging"] is False, k
assert k["kagi_categ"] == "search", k
# The tracked file must reference the env placeholder, never a literal key.
assert k["api_key"] == "${KAGI_API_KEY}", k
PYEOF
}
@test "searxng render script substitutes allowlisted vars and nothing else" {
local tmp="$BATS_TEST_TMPDIR/render"
mkdir -p "$tmp"
printf 'SEARXNG_SECRET=abc123\nKAGI_API_KEY=k-xyz\nSEARXNG_BASE_URL=http://x/y\nOTHER=must-not-appear\n' > "$tmp/env"
printf 'server:\n secret_key: "${SEARXNG_SECRET}"\n base_url: "${SEARXNG_BASE_URL}"\nengines:\n - name: kagi\n api_key: "${KAGI_API_KEY}"\n' > "$tmp/settings.yml"
python3 "$REPO_ROOT/scripts/searxng-render-settings.py" "$tmp/env" "$tmp/settings.yml"
grep -qF 'secret_key: "abc123"' "$tmp/settings.yml"
grep -qF 'base_url: "http://x/y"' "$tmp/settings.yml"
grep -qF 'api_key: "k-xyz"' "$tmp/settings.yml"
! grep -qF '${' "$tmp/settings.yml"
! grep -qF 'must-not-appear' "$tmp/settings.yml"
}
@test "searxng render script fails loudly on an unresolvable placeholder" {
local tmp="$BATS_TEST_TMPDIR/unresolved"
mkdir -p "$tmp"
printf 'SEARXNG_SECRET=abc123\n' > "$tmp/env"
printf 'engines:\n - name: kagi\n api_key: "${KAGI_API_KEY}"\n' > "$tmp/settings.yml"
run python3 "$REPO_ROOT/scripts/searxng-render-settings.py" "$tmp/env" "$tmp/settings.yml"
[ "$status" -ne 0 ]
[[ "$output" == *"KAGI_API_KEY"* ]]
}
@test "deployer renders the live searxng settings from the env file" {
grep -q "searxng-render-settings.py" "$REPO_ROOT/scripts/deploy-remote.sh" \
|| { echo "deploy-remote.sh must run searxng-render-settings.py"; return 1; }
grep -q "/opt/searxng/.env" "$REPO_ROOT/scripts/deploy-remote.sh" \
|| { echo "deploy-remote.sh must source /opt/searxng/.env for rendering"; return 1; }
}