メインコンテンツへスキップ
K

dsh-rustxng

kgmkm/dsh-rustxng

Answers the built-in web_search from a self-hosted RustXNG metasearch instance, downloading and starting that binary on demand and stopping it after an idle timeout; an optional fetch provider also decodes pages whose charset is declared only in <meta>.

インストール

dsh plugin --profile web add github:kgmkm/dsh-rustxng

README

dsh-rustxng

Use a self-hosted RustXNG instance from DeepSeek Harness — and answer the built-in web_search with it.

RustXNG is a SearXNG-compatible metasearch engine written in Rust: one query fans out to about 170 search engines, deduplicates the results and returns them without ads or tracking. This plugin connects DSH to a RustXNG you run yourself (Windows, macOS or Linux) and adds:

ContributionWhat it gives you
Search providerA rustxng provider on the web seam. Point web.searchProvider at it and the built-in web_search is answered by your instance — no DeepSeek search API key involved.
Settings cardA card in Plugins with two live switches and the instance URL. No restart, no hand-edited YAML.
Toolsrustxng_search (engine, category, language and time-range scoped) and rustxng_status (reachability, version, engine count, provider state).
Fetch provider (optional)A charset-aware web_fetch: resolves BOM → HTTP header → <meta charset> before decoding, so Shift_JIS pages that declare their encoding only in <meta> stop coming back as �. Does not use RustXNG.
Process supervision (optional)Start the binary when the instance does not answer, and stop it again after an idle timeout (default 60 min, editable in the card). Only a process this plugin started is stopped — this is also the only thing that cleans up after a Harness that was killed.
Binary fetchernode scripts/install-binary.mjs downloads and verifies the right release binary for your OS and CPU.

Nothing in DSH is patched: the plugin uses the documented ctx.web.registerSearchProvider() API and the ordinary web.searchProvider settings field, so it survives Harness upgrades.

Do you need this plugin at all?

RustXNG has its own integration for the rdsh CLI. Pick by what you want to replace:

Your situationUse
You only run rdsh and want rdsh search-web to use RustXNGRustXNG's own guide: docs/RDSH.md — rdsh rustxng install, then rdsh search-web --engine rustxng "query". No plugin needed; it is configuration-free and starts its own sidecar.
You use DSH and want the built-in web_search replacedThis plugin. rdsh's integration is a shell command for the rdsh CLI; it does not touch the Harness's web_search, so inside DSH it changes nothing.
You need web_fetch to stop garbling pages whose charset is declared only in <meta>This plugin (webFetch.enabled) — the rdsh path is search only.
You want the agent to call RustXNG as a tool (rustxng_search / rustxng_status)This plugin.
You use both and want one instance for bothThis plugin for DSH, and point rdsh at it: RUSTXNG_URL=http://127.0.0.1:8888 rdsh search-web --engine rustxng …

In short: replacing DSH's search is something only this plugin does; if you do not need that and you drive RustXNG from rdsh, the upstream guide is the shorter path. See If you also use rdsh for sharing one instance and one binary.


Requirements

  • DeepSeek Harness >= 0.2.0-rc.2 < 0.3.0 (host tools, web and settings services). Verified on 0.2.0-rc.2 and 0.2.1-rc.2; see docs/PLATFORMS.md.
  • Node.js 20+ (the plugin uses the global fetch)
  • A running RustXNG instance — see below

1. Get RustXNG

Easiest: let the plugin fetch a release binary.

node scripts/install-binary.mjs          # into $DSH_HOME/bin (or ~/.dsh/bin)
node scripts/install-binary.mjs --dir ./bin --musl   # Linux without glibc

The script picks the asset for your platform, verifies the published SHA-256, and prints the start command.

Or download it yourself from the releases page:

PlatformAsset
Windows x64rustxng-v<ver>-x86_64-pc-windows-msvc.exe
macOS arm64rustxng-v<ver>-aarch64-apple-darwin
macOS x64rustxng-v<ver>-x86_64-apple-darwin
Linux x64 (glibc)rustxng-v<ver>-x86_64-unknown-linux-gnu
Linux x64 (musl)rustxng-v<ver>-x86_64-unknown-linux-musl

Start it:

rustxng --port 8888                       # listens on 127.0.0.1:8888 by default
curl 'http://127.0.0.1:8888/healthz'      # → OK

RustXNG serves its own search UI at http://127.0.0.1:8888/; this plugin does not replace that page, it just consumes the same JSON API.

2. Install the plugin

# registry install (recommended)
dsh plugin --profile web add dsh-rustxng

# or from a checkout of this repository
git clone https://github.com/<owner>/dsh-rustxng.git
# then: Plugin Manager → install bundle → the checkout directory

Installing from a checkout links the directory, so Node resolves the plugin's real path (.../dsh-rustxng). The plugin copes with that: if @deepseek-ai/schemastery is not resolvable from there, it loads the schema builder through the running Harness's own entry point instead of silently dropping the schema.

3. Turn it on

Open Plugins → RustXNG and switch on Use RustXNG for web_search. Or set it in your profile patch (~/.dsh/profiles/<profile>/cordis.patch.yml):

- id: dsh-rustxng
  config:
    baseUrl: http://127.0.0.1:8888
    webSearch:
      enabled: true
      autoSelect: true

From then on web_search is answered by RustXNG. The enabled and autoSelect switches and the instance URL are volatile: the settings card changes them live, and turning the feature off hands web.searchProvider back to whatever it was before.

Configuration

KeyDefaultMeaning
enabledtrueMaster switch; false unloads the tools and unregisters the provider.
baseUrlhttp://127.0.0.1:8888Where RustXNG listens. Volatile (editable in the card).
timeoutMs20000Timeout for one search.
defaultLimit12Sources handed to web_search (1–50).
language''Search language (ja, en, …); empty = instance default.
timeRange''day / week / month / year; empty = no filter.
safeSearch-1-1 = instance default, else 0 / 1 / 2.
categories[]Category filters applied to every search (general, it, …).
exposeToolstrueRegister rustxng_search / rustxng_status.
webSearch.enabledfalseReplace the built-in web_search. Volatile.
webSearch.autoSelecttrueWrite web.searchProvider = 'rustxng' for you. Volatile.
webFetch.enabledfalseReplace web_fetch with the charset-aware fetcher. Volatile.
webFetch.autoSelecttrueWrite web.fetchProvider = 'rustxng-http' for you. Volatile.
autoStart.enabledtrue (shipped patch)Start the binary when the instance does not answer. Volatile. Off in the schema; the bundle turns it on.
autoStart.binaryPath''Explicit binary path; empty searches $DSH_HOME/bin, ~/.local/bin and PATH.
autoStart.idleMinutes60Stop the started process after this many minutes without a search (0 = never). Volatile.
autoStart.downloadtrueFetch a release binary into $DSH_HOME/bin when none is found (SHA-256 verified).
autoStart.version''Pin the download to one release (e.g. 0.20.1). Empty follows the latest release.
debugfalseLog each provider decision.

Optional: charset-aware fetch

The built-in HTTP fetcher reads charset from the Content-Type header only. Many Japanese sites (JRA, municipal sites, older corporate sites) send text/html with the encoding declared only in <meta charset="Shift_JIS">, so the page arrives as replacement characters. Turning on webFetch.enabled registers a small provider that resolves BOM → HTTP header → <meta> and decodes with TextDecoder (Node ships the legacy encodings), with a size cap and the same html/text classification.

- id: dsh-rustxng
  config:
    webFetch:
      enabled: true
      autoSelect: true
- id: web
  config:
    searchProvider: rustxng
    fetchProvider: rustxng-http   # replaces the whole row, so list both fields

It does not touch RustXNG: it is plain HTTP plus decoding. Leave it off if you prefer the built-in fetcher.

On hosts that hide the web settings row (DSH 0.2.0-rc.2 publishes only entries with a volatile field), autoSelect cannot write the selection — it needs the same context rules as the search side. Set both fields in the profile patch, exactly as above; the card's switches then still control whether the plugin is active. rustxng_status says so: Fetch provider: enabled, but the selection was not written (…).

First run: nothing is installed yet

The plugin does not bundle the binary and ships with the replacement off, so a fresh install answers like this until one of the two paths below is taken:

RustXNG could not be reached: fetch failed (connect ECONNREFUSED 127.0.0.1:8888)
Start it (for example `rustxng --port 8888`), or point the `baseUrl` setting at your instance.

Easiest: the shipped bundle already has Start RustXNG automatically on, so the first search fetches and starts the instance; nothing to click. On the next start it downloads the matching release (SHA-256 verified, ~22-27 MB) into $DSH_HOME/bin, starts it, and stops it again after the idle timeout. Nothing else to install.

Manual: npx dsh-rustxng-fetch drops the binary in the same place; run it yourself, or wire it into launchd / systemd and leave auto-start off.

The download never depends on GitHub's API: if the "latest release" lookup is rate-limited it falls back to a pinned version, and autoStart.binaryPath (or --version) needs no lookup at all.

About the automatic selection

autoSelect writes web.searchProvider = 'rustxng' for you, but a plugin cannot do that while it is mounting: the loader runs apply() inside its own transaction and rejects the settings write with "HMR transactions cannot be nested". 0.3.0 therefore retries the write from a context that is allowed to write — a tool call (rustxng_status / rustxng_search) or a live settings change from the card — and reports the outcome in rustxng_status.

If it still cannot be written (for example on a host that refuses plugin-initiated writes), set the field once by hand; it is remembered:

- id: web
  config:
    searchProvider: rustxng
    fetchProvider: http

Optional: start and stop the instance

If you would rather not run RustXNG as a service, let the plugin own the process:

- id: dsh-rustxng
  config:
    autoStart:
      enabled: true
      binaryPath: /home/you/.dsh/bin/rustxng   # empty = $DSH_HOME/bin, ~/.local/bin, PATH
      idleMinutes: 60                          # 0 = keep it until DSH unloads the plugin

Behaviour:

  • It starts the binary only when the instance does not already answer, and waits for /healthz before using it. Concurrent callers share one attempt, so two starts cannot race for the port.
  • It stops only a process it started — a systemd unit or a manually started instance is never touched.
  • On plugin unload (a normal DSH shutdown) it stops the process immediately.
  • A Harness that is killed never runs that cleanup, so the idle timeout finishes the job: after idleMinutes without a search the process is stopped, and the next search starts it again. Reachability probes do not count as activity.
  • rustxng_status reports Auto-start: on — managed (pid …), idle timeout 60 min (12 min idle) and Selection: web.searchProvider: rustxng (this plugin), web.fetchProvider: http.
  • The rustxng_search tool starts the instance too, so a tool-only setup (no replacement of web_search) also works from the first call. rustxng_status never starts it — it only reports.
  • Which version is downloaded: always the latest release. PINNED_VERSION in lib/install-binary.js is only the offline fallback used when GitHub's API cannot be reached, and it is deliberately an older known-good build. Set autoStart.version when you want a reproducible version instead.
  • How it stops: four paths, because none of them covers everything.
    1. the plugin's disposer, when DSH unloads the plugin;
    2. process.on('exit'), for a normal exit;
    3. SIGTERM / SIGINT / SIGHUP handlers, because a signal ends Node without emitting exit. These stop the child and then re-raise the signal with the default disposition — only when nothing else is listening, so a Harness that shuts itself down keeps control;
    4. the instance's own idle timer: the spawned process is given RUSTXNG_IDLE_TIMEOUT (the same number of minutes), so builds that support it exit by themselves. Path 4 is the only one that survives a SIGKILLed host, which is why it is set whenever idleMinutes is above 0. It needs a build that understands the variable — verified with RustXNG 0.24.0; 0.4.x ignores it, and the plugin's own watchdog (path 3's timer, 30 s checks) covers those. With idleMinutes: 0 nothing is scheduled at all, so a SIGKILLed host leaves the instance running — the trade-off of asking for "never stop it". Under a desktop app or a systemd unit the child also shares the host's cgroup and may disappear with it first — same outcome, but do not rely on that in a plain shell.

Set idleMinutes: 0 when using a service manager instead; the two mechanisms are independent.

Tools

// rustxng_search
{ "query": "rust async runtime", "limit": 8, "engine": "arxiv", "time_range": "year" }
{ "query": "東京 天気", "category": "news", "language": "ja" }

rustxng_status reports whether the instance answers, its version, how many engines are enabled, whether the provider is registered, and whether web.searchProvider points at it — run it first when a search fails.

If you also use rdsh

RustXNG ships its own integration for rdsh: rdsh search-web --engine rustxng needs no configuration (it looks at SEARXNG_URL, RUSTXNG_URL, a local rustxng command, then 127.0.0.1:8888), and rdsh rustxng install puts a binary in ~/.local/bin.

That path is not a replacement for this plugin: it is a shell command for the rdsh CLI, while this plugin plugs into the Harness's web_search/web_fetch seam and adds model-facing tools. Use whichever matches what you run — and if you run both, one instance serves both:

  • point rdsh at the plugin's instance with RUSTXNG_URL=http://127.0.0.1:8888, or
  • let the plugin reuse rdsh's binary: the search order is autoStart.binaryPath → $DSH_HOME/bin → ~/.local/bin → PATH, and rustxng_status prints which file is actually running (Auto-start: on — managed (pid …) [rustxng]).

Set autoStart.binaryPath when you have several builds and want a specific one.

Platform support

PlatformStatus
Windows 10/11 x64Verified: release binary + this plugin (search, provider replacement, settings card) on DSH 0.2.1-rc.2
Linux x64 (glibc / musl)Verified on a standalone Ubuntu 26.04 server (DSH 0.2.0-rc.2: replacement + card) and on WSL2 (both release builds 12/12)
macOS arm64 / x64Verified on macOS 26.6.2 / M1 Max with DSH 0.2.1-rc.3 (Electron desktop app): 0.1.3 answered
ustxng_search in ~3 s across 6-10 engines; 0.2.1 adds first-run download and auto-start

See docs/PLATFORMS.md for how each platform is expected to be verified, binary placement, and the platform-specific notes (PATH, service managers, WSL, musl).

Troubleshooting

SymptomFix
RustXNG is not answering at …Start the instance, or fix baseUrl in the card. rustxng_status shows the probe error.
web_search still fails with a DeepSeek 401The provider is not selected: check the card's first switch, or web.searchProvider on the official Web search page. On DSH 0.2.0-rc.2 that field is not writable from a plugin — set it in the profile patch (see above) or with DSH_WEB_SEARCH_PROVIDER=rustxng.
Card missing from PluginsThe plugin row is not active — check Plugin Manager, and that the DSH version matches dsh.compatibility. On a linked checkout the schema comes through the Harness installation: rustxng_status prints Config schema: loaded via … or the resolution error. 0.1.2 resolves symlinked launchers such as <prefix>/bin/dsh.
rustxng_status says Replace web_search: no (default) although it is onFixed in 0.1.2 (live config values were compared as primitives).
Wanted to register a provider but web is missingThe profile has no @deepseek-ai/dsh-web; this plugin needs it for the replacement (the tools still work).
Results look thinA few engines rate-limit per IP; rustxng_status / RustXNG's /stats show which are suspended. !engine query still works.
A Japanese page comes back as ���� / �NThe site declares its encoding only in <meta>. Turn on webFetch.enabled (see Optional: charset-aware fetch), or read it with curl … | iconv -f SHIFT_JIS -t UTF-8.

Development

node tests/smoke.mjs            # payload normalisation + platform mapping (+ live search if an instance answers)
node tests/apply.test.mjs       # host half against a stub instance: provider gate, selection, tools, live values
node tests/resolve.test.mjs     # host-package resolution through a symlinked launcher (Linux npm-global layout)
node tests/tools.test.mjs       # the search tool triggers auto-start; status stays read-only
node tests/install.test.mjs     # release asset naming, SHA-256 verification, the pinned fallback
node tests/process.test.mjs     # idle rule, binary discovery, a real start/health/idle-stop cycle
node tests/signal.test.mjs      # SIGTERM takes the child with it (POSIX; skips on Windows)

apply.test.mjs and resolve.test.mjs need no running instance and no network.

Layout:

index.js                 host plugin: Config, provider + tools wiring, selection hand-over,
                         symlink-aware host-package resolution
lib/rustxng-client.js    HTTP client: query building, payload normalisation, timeouts
lib/web-provider.js      the `rustxng` seam provider and the web.searchProvider hand-over
lib/fetch-provider.js    optional charset-aware fetch provider (BOM → header → <meta>)
lib/tools.js             rustxng_search / rustxng_status
lib/client.js            settings card (Plugins → RustXNG)
scripts/install-binary.mjs  cross-platform release downloader with SHA-256 check
scripts/verify-linux.sh     12-item instance verification for POSIX hosts

License and attribution

MIT — see LICENSE.

RustXNG itself is AGPL-3.0-or-later and is not bundled here: the plugin talks to a separate process over HTTP, and scripts/install-binary.mjs downloads it from its own release page. If you redistribute a modified RustXNG, AGPL §13 applies to that work, not to this plugin.

関連プラグイン