Bundle
dsh-web-search-searxng
SearXNG-backed web search provider for the DeepSeek Harness web capability seam (ctx.web) — self-hosted metasearch, no API key, no model turn per search
- Source
- chinng-inta
- stars
- 4 stars
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# dsh-web-search-searxng
A [SearXNG](https://docs.searxng.org/)-backed `WebSearchProvider` for the
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web capability seam (`ctx.web`).
SearXNG is a self-hosted metasearch engine. One search here is a plain retrieval call against the
instance's `/search?format=json` endpoint, so unlike the shipped DeepSeek provider it needs **no API
key** and costs **no model turn** — the shipped provider issues a full Messages request with the
native `web_search` server tool, paying latency and generated tokens for every search.
This is an **implementation** package: it registers a provider into `ctx.web` and does **not**
register a model-facing tool. `@deepseek-ai/dsh-tool-web` owns `web_search`, its schema, its prompt
guidance, and the result card. Installing this package makes that existing tool work against your
own instance.
## Install
### From npm
```bash
dsh plugin --profile web add dsh-web-search-searxng
```
### From this repository
```bash
dsh plugin --profile web add github:chinng-inta/dsh-web-search-searxng
```
A git dependency ships no `lib/`, so the package builds itself through its `prepare` script — and
pnpm blocks install scripts until you allow them. The first run therefore fails and prints the exact
key to allow. Add it to your profile's `pnpm-workspace.yaml` and re-run:
```yaml
allowBuilds:
"dsh-web-search-searxng@https://codeload.github.com/chinng-inta/dsh-web-search-searxng/tar.gz/<commit-sha>": true
```
The key pins a commit, so it changes whenever you install a newer revision. Building from source also
needs the dev toolchain fetched (~20 s versus ~2 s for the registry). Prefer npm unless you are
tracking unreleased changes.
### Either way
The package declares `dsh.bundle`, so a single command also activates it: the bundle patch
inserts the provider row and selects it on the `web` row. Point it at your instance and restart:
```bash
export SEARXNG_URL=http://searxng.internal:8888
```
Verify the composition before booting:
```bash
dsh --profile web --dump-config | grep -A3 'id: web'
```
### Your instance must serve JSON
SearXNG does not enable the JSON API by default. In the instance's `settings.yml`:
```yaml
search:
formats:
- html
- json
```
Without it the endpoint answers with the HTML result page, and this provider fails with a message
naming the fix rather than a parse error.
A public instance is a poor backend: most refuse programmatic access (HTTP 403 from a bot filter)
or rate-limit aggressively. Run your own.
## Configuration
The plugin owns the `web-search-searxng` settings namespace, so its section resolves in the harness's
own layering:
```
schema defaults → the plugin row's `config` (composition base) → the user layer in the settings document
```
`baseURL` additionally falls back to `$SEARXNG_URL` when no layer sets it. Nothing else is needed to
get started: export the variable and the provider is configured.
The section is projected **per search**, so an edit to the settings document reaches the next search
without a restart — and clearing `baseURL` falls back to the environment again rather than stranding
the provider on a value it can no longer see. Registration itself never moves, so provider selection
does not flicker when configuration changes.
A deployment without a settings provider mounted keeps working: the source falls back to the
composition entry, exactly as composed.
All keys are optional.
| Key | Default | Meaning |
|---|---|---|
| `baseURL` | `$SEARXNG_URL` | Instance root; `/search` is appended. Missing or non-http(s) makes the provider report unavailable rather than fail every search. |
| `categories` | instance default | `categories=` filter, e.g. `['news']`. |
| `engines` | instance default | `engines=` filter, e.g. `['duckduckgo', 'brave']`. |
| `language` | instance default | `language=` filter, e.g. `ja`, `en-US`. |
| `timeRange` | unset | `time_range=` filter: `day` / `week` / `month` / `year`. |
| `safesearch` | instance default | `safesearch=`: `0` off, `1` moderate, `2` strict. |
| `timeoutMs` | `10000` | Resource backstop for one search. |
| `maxSnippetChars` | `500` | Per-source snippet cap. |
| `headers` | none | Extra request headers, e.g. for an instance behind an authenticating proxy. |
As the plugin row's composition base:
```yaml
- id: web-search-searxng
name: 'dsh-web-search-searxng'
config:
baseURL: http://searxng.internal:8888
language: ja
categories:
- general
- news
```
…or as the user layer in the harness settings document (`$DSH_HOME/settings.yaml` by default),
which wins over the row above and is what a configuration surface writes:
```yaml
web-search-searxng:
language: ja
maxSnippetChars: 300
```
Every search-shaping knob is a **deployment setting, not a model argument**. The seam's
`WebSearchRequest` is deliberately just `query` + `maxResults`; provider-neutral controls (recency,
domain filters, search depth) are named deferred work upstream. Keeping them in config is what makes
this provider substitutable for the shipped ones.
`timeoutMs` is a resource backstop, not the model-facing tool-call budget —
`@deepseek-ai/dsh-tool-call-timeout-policy` owns that via `tool-web`'s `searchTimeoutMs`. Leave this
below the tool budget so a slow instance surfaces as a provider failure rather than a tool timeout.
## Provider selection
The bundle patch sets `web.searchProvider: searxng`. This is required, not opinionated.
The seam auto-selects only when exactly **one** registered provider is usable, and
`@deepseek-ai/dsh-web-search-deepseek` reports usable whenever a credential *resolver* exists — which
its own `apply()` always supplies — so it answers `available() === true` on a stock composition even
with no key configured. Registering a second provider without naming one would make every search fail
with `WEB_PROVIDER_AMBIGUOUS`.
Bundle layers apply before your profile's `cordis.patch.yml`, the home patch, and any `--patch`
overlay, so you can always override the choice. But note that a patch replaces the targeted row's
**whole** `config`: if you patch the `web` row yourself for anything else, restate
`searchProvider: searxng` there too.
## Mapping
| SearXNG | Seam |
|---|---|
| `results[].url` | `sources[].url` (required; results without one are dropped) |
| `results[].title` | `sources[].title` |
| `results[].content` | `sources[].snippet`, capped at `maxSnippetChars` |
| `results[].publishedDate` | `sources[].publishedAt` |
| `answers[]` | `content`, newline-joined; omitted when empty |
Sources are deduplicated by URL, because a metasearch merges engines that routinely return the same
page. Blank strings are treated as absent rather than emitted as empty fields — the seam's optional
fields exist so an adapter never has to invent them.
`truncated` is always `false` from this provider: the seam owns `maxResults` enforcement, and
reporting our own truncation would misattribute whose bound cut the list.
## Errors
Failures are `WebError`s the tool layer turns into a readable tool result.
| Situation | Code |
|---|---|
| Caller cancelled | `WEB_ABORTED` |
| `timeoutMs` elapsed | `WEB_PROVIDER_ERROR` |
| Non-2xx from the instance (403 carries a bot-filter hint) | `WEB_PROVIDER_ERROR` |
| Response was not JSON (usually `formats` misconfiguration) | `WEB_PROVIDER_ERROR` |
| Unparseable body | `WEB_PROVIDER_ERROR` |
`available()` is a cheap synchronous check — a parseable `http(s)` base URL — as the seam requires;
it never touches the network.
Redirects are refused (`redirect: 'error'`). A self-hosted instance has no reason to redirect a
search, and following one would send the query to a host the deployment never configured.
## Known limitations
- **`maxResults` is not pushed down.** SearXNG exposes no result-count parameter, so the instance
returns its full first page and the seam truncates. This bounds tokens, not the instance's work.
- **`publishedDate` is usually absent.** General web engines rarely date results; news engines
usually do. Filter with `categories: ['news']` if you need dates.
- **Infoboxes are not surfaced.** They are structured entity cards rather than an answer to the
query, so flattening them into `content` would present them as one.
- **No per-engine failure reporting.** SearXNG reports `unresponsive_engines[]` on partial failures;
the seam's result shape has nowhere to put it, so a degraded search looks like a thin one.
## Compatibility
| This package | DeepSeek Harness |
|---|---|
| `0.3.3`+ | `0.1.0-rc.8` |
| `0.1.x` – `0.3.2` | `0.1.0-rc.6` |
**`0.3.3` is not backward compatible, and neither is anything before it forward compatible.**
Installing the wrong pair does not merely lose this card — it fails the whole Web UI's plugin
load, because both breakages surface as an unanswerable client `require` or an invalid slot
registration ([#1](https://github.com/chinng-inta/dsh-web-search-searxng/issues/1)):
- rc.8 dropped `@deepseek-ai/dsh-client-web-react` from the loader's seed table. Up to `0.3.2`
this package imported `bindSnapshotSelector` from it. `0.3.3` hands its store to the renderer
through the slot inject face's `hooks` compartment instead, which is where rc.8 synthesizes
selector hooks.
- rc.8 turned `settings.plugin.item` from a `list` slot (`id` + `order`) into a `keyed` one
(`key` = the settings namespace the card edits). `0.3.3` registers the keyed way, which rc.6
rejects.
`0.3.0` shipped the settings card without its stylesheet — it works, but renders with browser
defaults. Use `0.3.1` or later.
The harness is a developer preview with breaking changes between release candidates, and its
packages publish the active line under the **`next`** dist-tag (`latest` still points at the older
`0.0.1-rc.1`). Pin your harness version.
## License
MIT
Install
dsh plugin --profile web add github:chinng-inta/dsh-web-search-searxng
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-web-search-searxng from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.