Bundle
dsh-web-search-custom
Generic URL + optional API key search provider for the DeepSeek Harness web profile (ctx.web), with an in-settings configuration card. Zero-intrusion bundle plugin; works with any SearXNG-compatible JSON endpoint.
- Source
- runfali
- License
- MIT
- Updated
- Updated 10 hours ago
Readme
# dsh-web-search-custom
[](LICENSE)
[](package.json)
[](https://deepseek.com)
[English](README.md) | [简体中文](README.zh-CN.md)
Point the [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness)
`web` profile at **any JSON search API** — give it a URL and (optionally) an API
key, and it replaces the built-in DeepSeek search. The shipped default already
works against a self-hosted SearXNG:
```text
http://127.0.0.1:8080/search?format=json&q={query}
```
> [!IMPORTANT]
> **Design intent.** The plugin is a thin adapter, nothing more: it owns one
> search provider behind the `ctx.web` capability seam and one settings card.
> It ships no search engine, no index, no cache, and no bundled credentials; it
> talks only to the endpoint you configure. Uninstall it and the stock provider
> is back with zero residue.
---
## Table of Contents
- [Why](#why)
- [Features](#features)
- [How it works](#how-it-works)
- [Requirements](#requirements)
- [Install](#install)
- [Configure in the settings page](#configure-in-the-settings-page)
- [Configuration reference](#configuration-reference)
- [Other configuration channels](#other-configuration-channels)
- [Uninstall](#uninstall)
- [Limits](#limits)
- [Development](#development)
- [License](#license)
## Why
dsh's web profile resolves search through the `ctx.web` seam. Out of the box
that seam is served by a hosted search backend, which is the wrong trade for
anyone who already runs their own search stack (SearXNG, an internal search
gateway, a vendor API) or who wants queries to stay inside their own network.
This plugin registers one additional provider into that seam and lets you select
it. Everything else — the `web_search` tool, result capping, cancellation, error
propagation — keeps working unchanged, because it is the same seam the stock
provider uses.
## Features
- **Standard dsh bundle plugin.** Zero intrusion: no dsh source is modified;
removing the plugin restores the stock search provider.
- **Zero runtime dependencies.** Only dsh-shipped packages are used
(`@deepseek-ai/dsh-settings`, `@deepseek-ai/schemastery`, plus React on the browser
side). Nothing is vendored or bundled.
- **In-settings configuration card** (Settings → Plugins): URL, API key,
timeout and the whole field mapping are editable in the page, with the same
overridden/reset affordances as first-party plugin cards.
- **Works with any JSON API**: GET or POST, `Authorization` bearer (or any header
name/scheme), extra headers, and dotted-path field mapping.
- **SearXNG-compatible by default**: the shipped mapping matches SearXNG's
`results[]` / `url` / `title` / `content` / `publishedDate` shape.
## How it works
```text
web_search tool
│
▼
ctx.web (capability seam) ──selection──▶ provider id "custom"
│ │
│ ▼
│ fetch(your endpoint) ──▶ your JSON API
│ │
└◀──── WebSearchResult { sources[], truncated } ◀──┘
```
The host side registers the namespace `web-search-custom` (so the settings page
can render a card for it) and registers a search provider with the stable id
`custom`. The provider's `search(query, signal)` reads the live settings value on
**every call**, fills the `{query}` / `{apiKey}` placeholders, performs the HTTP
request with the caller's cancellation signal plus its own timeout, and maps the
JSON payload into `sources[]` (deduplicated by URL, non-`http(s)` entries dropped,
HTML stripped, entities decoded).
Provider selection follows the seam's own rules, resolved at call time:
a configured id wins; with no id configured, exactly one usable provider is
required. The plugin's own `cordis.patch.yml` therefore also sets
`web.searchProvider: custom` so the choice is explicit rather than accidental.
## Requirements
| Item | Value |
|---|---|
| DeepSeek Harness | `>=0.1.2-alpha.3 <0.2.0 || >=0.1.5-alpha.1 <0.1.6` (verified on 0.1.2-alpha.4 and 0.1.5-rc.1) |
| Node.js | `^22.19.0 || >=24.0.0` |
| Runtime dependencies | **none** — the three peers come from the dsh host install |
| Network | outbound access from the dsh host to the endpoint you configure |
The `dsh` range is declared under `dsh.engines.dsh` (and mirrored in
`peerDependencies` for the dsh packages the plugin relies on). The disjunction is
load-bearing rather than cosmetic: npm semver only satisfies a prerelease from a
range group that itself contains a prerelease with the same
`[major, minor, patch]` tuple, so a plain `<0.2.0` group does **not** cover
`0.1.5-rc.1`. `tests/entry.test.mjs` pins this with a decision table and a
counter-proof against the old single range.
## Install
```bash
dsh plugin --profile web add dsh-web-search-custom
```
Restart the dsh web instance afterwards. From a local checkout, pass the
directory path instead of the package name.
## Configure in the settings page
Open **Settings → Plugins** in the web UI and expand the
"Custom search (web-search-custom)" card. Everything is editable there:
- the search URL (with `{query}` / `{apiKey}` placeholders),
- the API key (**optional**; when set it is sent as `Authorization: Bearer`),
- request method, POST body template, extra headers (JSON), timeout,
- the result field mapping (`resultsPath` / `urlField` / `titleField` /
`snippetField` / `publishedField`).
Saving writes the `web-search-custom:` section of your dsh settings document and
takes effect immediately — the next search uses the new value, no restart. The
overridden/reset badges behave exactly like first-party plugin cards.
## Configuration reference
| Field | Default | Description |
| --- | --- | --- |
| `url` | local SearXNG | Search endpoint. Supports `{query}` and `{apiKey}` placeholders (URL-encoded automatically). For GET without a `{query}` placeholder, `q=` is appended automatically |
| `apiKey` | empty | Optional. When set, sent as `Authorization: Bearer <key>` by default; if the URL or the POST body already contains `{apiKey}`, no extra header is added |
| `method` | `GET` | `GET` or `POST`; any other value degrades to `GET` |
| `body` | `{"query":"{query}"}` | POST body template, supports `{query}` / `{apiKey}` |
| `headers` | `{}` | Extra request headers as a JSON string |
| `authHeader` | `Authorization` | Header name used for the API key |
| `authScheme` | `Bearer` | Auth scheme; leave empty to send the bare key |
| `timeoutMs` | `30000` | Per-request timeout |
| `resultsPath` | `results` | Dotted path to the result array, e.g. `data.results` |
| `urlField` / `titleField` / `snippetField` / `publishedField` | `url` / `title` / `content` / `publishedDate` | Result field-name mapping (SearXNG's defaults) |
Each field also falls back to a wider list of common aliases at mapping time
(for example a missing `titleField` value still tries `title` then `name`).
## Other configuration channels
### Profile patch layer (settings document alternative)
```yaml
- id: web-search-custom
config:
url: 'http://127.0.0.1:8080/search?format=json&q={query}'
apiKey: ''
method: GET
body: '{"query":"{query}"}'
headers: '{}'
authHeader: Authorization
authScheme: Bearer
timeoutMs: 30000
resultsPath: results
urlField: url
titleField: title
snippetField: content
publishedField: publishedDate
```
> Note: a patch replaces the whole `config` block — write every key when overriding.
### Environment variables
Environment overrides win over the settings page and require no file edit:
| Variable | Description |
| --- | --- |
| `WEB_SEARCH_CUSTOM_URL` | Override the search URL |
| `WEB_SEARCH_CUSTOM_API_KEY` | Override the API key (recommended for secrets) |
| `WEB_SEARCH_CUSTOM_HEADERS` | Extra headers, JSON string |
| `WEB_SEARCH_CUSTOM_TIMEOUT_MS` | Timeout in milliseconds |
## Uninstall
```bash
dsh plugin --profile web remove dsh-web-search-custom
```
Restart dsh and the stock search provider takes over again.
## Limits
- The API key is stored as an ordinary settings field, not in a credential
store. For sensitive deployments prefer `WEB_SEARCH_CUSTOM_API_KEY`.
- The page size is decided by the upstream service; the client cannot request a
specific `count`. The tool layer still truncates to `maxResults`.
- Result mapping is field-based, not query-language aware: exotic payload shapes
need the field mapping (and, if necessary, a `resultsPath`) adjusted.
## Development
```bash
npm install # dev dependencies (dsh packages, for the host-contract test)
npm test # node --test tests/*.test.mjs && node tests/client-smoke.mjs
npm run test:host # real Cordis + WebRuntime + file settings provider
```
| Test | What it proves |
|---|---|
| `tests/entry.test.mjs` | Real entry load (`import('../src/index.js')`), manifest declarations, the engines decision table (hand-rolled comparator + counter-proof + cross-check against the host's real `semver.satisfies`), four-way key parity (host schema ↔ client fields ↔ client views ↔ patch config), settings hot-reload alive reference, dependency hygiene |
| `tests/host-integration.test.mjs` | Driven by the real `@deepseek-ai/cordis` context, the real `ctx.web` runtime and the real file-backed settings provider: provider registration, full `web.search()` path, `maxResults` capping by the seam, live settings → next-call effect, and the seam's selection semantics |
| `tests/client-smoke.mjs` | Browser half: bundle id, short service names, locale parity, slot registration contract, card rendering, save/reset write path, read-only disabling |
| `tests/live-search.mjs` | Opt-in live check against a real endpoint: `DSH_WSC_LIVE_URL='http://host/search?format=json&q={query}' node tests/live-search.mjs` |
```text
src/index.js host half — settings namespace, URL/body templating, mapping
lib/client.js browser half — settings card (hand-written bundle, no build step)
cordis.patch.yml bundle patch: select provider "custom" and mount the plugin
tests/ entry + host-contract + client-smoke + opt-in live check
```
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:runfali/dsh-web-search-custom
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-custom from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.