Bundle
dsh-web-automation
DeepSeek Harness plugin: local-first web stack — multi-engine keyless search (DuckDuckGo/Bing + optional Exa/DeepSeek/Jina), a SQLite-cached fetch provider, the web_platform_search tool, and history/stats/cache tools. No paid search API or third-party data broker required for the keyless engines.
- Source
- stelmakhdigital
- stars
- 1 stars
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-web-automation
> 🇬🇧 **English** | 🇷🇺 [Русский](README.ru.md)
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that gives a **local model** a **local-first web stack** — no paid search API, no third-party data broker, no cloud required for the keyless engines.
It bundles four capabilities into one installable plugin:
| Capability | What it does | Keyless? |
|---|---|---|
| **Multi-engine search** | The `web_search` tool, routed across DuckDuckGo + Bing (keyless) and Exa / DeepSeek / Jina (opt-in, when their API keys are present). Fallback or fuse (parallel + RRF) routing, cooldowns, BM25 enrichment. | ✅ DDG + Bing |
| **Cached fetch** | The `web_fetch` tool backed by a SQLite page cache with ETag/Last-Modified revalidation. Repeats within the TTL make no network request. | ✅ |
| **Web platforms** | The `web_platform_search` tool: search a specific platform (GitHub, Reddit, YouTube, Bilibili, V2EX, RSS, …) via its own public endpoint. New platforms are added **without code** via config or versioned rule packs (hot-reloaded). | ✅ |
| **History / stats / cache** | `web_history`, `web_search_stats`, `web_cache_clear` — read the shared local store; no network. | ✅ |
All state is local: the store is `$DSH_HOME/web.db`. Outbound traffic for the keyless engines is limited to DuckDuckGo and Bing.
## Quick start
**Prerequisites:**
- **Node.js 22.19+ or 24+** — the same requirement as DSH itself; the plugin uses the built-in `node:sqlite` store.
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — either via `npx @deepseek-ai/dsh` (no install) or a source checkout (`pnpm install && pnpm run build`, then `pnpm dsh ...`).
1. **Install the plugin into a profile** (the bundle overlay is applied automatically):
```sh
dsh plugin --profile tui add git+https://github.com/stelmakhdigital/dsh-web-automation.git
```
2. **Boot the profile**:
```sh
dsh --profile tui # TUI
dsh web # Web UI at http://127.0.0.1:3080
```
At boot the plugin loads with the host: the `web` seam is pinned to the `multi` search / `cached-http` fetch providers, `web_fetch` is enabled, and `web_platform_search` plus the history tools are registered. No extra configuration is needed — every field is defaulted (keyless DuckDuckGo + Bing).
3. **Verify** — run the [smoke test](#smoke-test) from a DSH session (e.g. `web_search "hello world"`).
Replace `tui` with your profile name. To inspect the composed profile tree: `dsh --profile tui --dump-config`.
## Install
Plugins are installed **into a DSH profile** with `dsh plugin` — each profile is its own pnpm workspace under `$DSH_HOME/profiles/<name>`. At boot, DSH symlinks the host's `@deepseek-ai/*` packages into the profile's `node_modules`, so the plugin's peer dependencies resolve to the host's own copies.
This package is a **DSH bundle**: its `package.json` declares `dsh.bundle`, so installing it automatically applies the shipped [`local-web.cordis.yml`](local-web.cordis.yml) overlay — it pins the `web` seam to the plugin's providers, enables `web_fetch` in the host `tool-web` row, and registers the plugin. No manual `--patch` needed:
```sh
dsh plugin --profile tui add git+https://github.com/stelmakhdigital/dsh-web-automation.git
```
(Replace `tui` with your profile name. The seam pin is **required**: without it the seam sees two usable search providers (the deployment default plus `multi`) and fails with `WEB_PROVIDER_AMBIGUOUS`.)
Manual alternative — if you want to tweak the config before applying, apply the overlay yourself:
```sh
dsh --profile tui --patch "$PWD/local-web.cordis.yml"
```
### Optional: browser automation
The [`dsh-web-browser`](browser/) sub-package adds local Chromium (Playwright) automation behind the `browser_*` tools (`browser_open`, `browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_screenshot`). It is separate because it pulls in Playwright + a Chromium download.
It is a sub-directory of this repo, and pnpm cannot install a sub-directory of a git repo directly. Install it from a local clone as a **tarball** (`npm pack`): unlike a `link:` install, the tarball is unpacked into the profile's `node_modules`, so the package's own dependencies (playwright) are installed and its `@deepseek-ai/*` imports resolve to the host's packages:
```sh
git clone --depth 1 https://github.com/stelmakhdigital/dsh-web-automation.git ~/dsh-plugins/dsh-web-automation
cd ~/dsh-plugins/dsh-web-automation
npm pack ./browser # → dsh-web-browser-0.3.0.tgz (lib/ is prebuilt in the repo)
dsh plugin --profile tui add ./dsh-web-browser-0.3.0.tgz
# one-time: install the Chromium binary
dsh plugin --profile tui exec playwright install chromium
```
The browser package is a bundle too — its patch (`cordis.patch.yml` in the tarball) registers the browser plugin row automatically. To update: `git pull` in the clone, `npm pack ./browser` again, and `dsh plugin add` the new tarball.
## Configure
Add a row to your deployment's `cordis.yml` (or an overlay applied with `dsh --patch ...`). See [`cordis.yml.example`](cordis.yml.example) for the full reference.
```yaml
- id: web-automation
name: 'dsh-web-automation'
config:
search:
engines: [ddg, bing, exa, deepseek, jina] # tried in order
mode: fallback # fallback | fuse
region: '' # region/market hint (DDG kl, Bing setmkt)
# exa: { apiKeyEnv: EXA_API_KEY } # or apiKey: '...'
# deepseek: { apiKeyEnv: DEEPSEEK_API_KEY }
# jina: { apiKeyEnv: JINA_API_KEY }
fetch:
revalidate: true # conditional revalidation for fresh-but-expired pages
platforms:
tool: true # register web_platform_search
maxResults: 20
# platforms: # override built-ins by id, or add new platforms
# - id: my-site
# name: My Site
# format: json
# searchUrl: 'https://my-site.example/search?q={query}'
# fields: { items: 'data.results', url: 'link', title: 'title' }
history:
history: true # web_history
cacheClear: true # web_cache_clear
stats: true # web_search_stats
```
Every field is defaulted, so an empty `config: {}` (or no `config` at all) enables the full local web stack with the keyless engines.
## Relationship to DSH's built-in web packages
This plugin is an **externalized, standalone copy** of DSH's internal web packages (`web-search-multi`, `web-fetch-cached`, `web-platforms`, `web-store`, `web-browser`, `tool-web-history`) and is currently **ahead of upstream** (SearXNG engine, news-mode freshness, embedding re-rank, LRU eviction, SSRF guard, inline screenshots).
- **Mutual exclusion**: the plugin and the built-in packages register the same provider ids (`multi`, `cached-http`) and tool names. A deployment that loads both fails at startup with `WEB_DUPLICATE_PROVIDER` — keep one. If you use this plugin, do **not** apply DSH's `examples/web-local` overlay (or its preset copies), and vice versa.
- **The tools come from the host**: `web_search` and `web_fetch` are registered by the host's `tool-web` plugin; this plugin registers the **providers** behind them (plus `web_platform_search` and the history tools). The overlay above enables `web_fetch` in the `tool-web` row.
- **Upstream drift**: because the plugin evolves independently, its behavior may diverge from the built-in packages over time. The module headers in `src/` mark the upstream package each module mirrors.
## API keys (optional)
The keyless engines (DuckDuckGo, Bing) work with no configuration. To opt in to Exa / DeepSeek / Jina, provide their API keys either:
- in the launch environment (`EXA_API_KEY`, `DEEPSEEK_API_KEY`, `JINA_API_KEY`), or
- in the plugin config (`search.exa.apiKey`, etc.), or
- via the DSH credentials domain (a key written to the credentials store takes effect per-search, without a restart).
## Usage
Once installed and configured, the model can:
- **Search the web** — `web_search "query"` (multi-engine, enriched snippets).
- **Fetch a page** — `web_fetch <url>` (cached; repeats within the TTL make no network request).
- **Search a platform** — `web_platform_search { platform: "github", query: "..." }`.
- **Review history / stats** — `web_history`, `web_search_stats`, `web_cache_clear`.
- **Drive a browser** (with `dsh-web-browser`) — `browser_open`, `browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_screenshot`.
## Privacy model
- **Queries** go to the configured search engines only — unavoidable with any search engine. With the default engine list and **no API keys**, outbound traffic is limited to DuckDuckGo and Bing; inference stays local.
- **No credentials required** for basic use.
- **All state is local**: the store is `$DSH_HOME/web.db`, nothing is sent anywhere else.
- **Caveat**: scraping public SERPs may violate a search engine's terms of service; the provider sends an explicit product `User-Agent`, rate-limits itself (1 req/s per engine by default), and cools down blocked engines. Use responsibly.
## Security
- **SSRF guard** (on by default): requests to loopback, private, link-local, and otherwise reserved network targets (IPv4 `0/8`, `10/8`, `127/8`, `172.16/12`, `169.254/16`, `192.168/16`; IPv6 `::1`, `::/128`, `fe80::/10`, `fc00::/7`) are blocked. The check runs on the literal host **and** after DNS resolution (against rebinding), and for `web_fetch`/enrichment it re-checks every redirect hop (max 5). Guarded paths and their flags:
| Path | Flag |
|---|---|
| `web_fetch` (cached fetch provider) | `fetch.allowPrivateNetworks` |
| search enrichment (page fetches for snippets) | `search.allowPrivateNetworks` |
| `web_platform_search` fetches (incl. RSS feed URLs) | `platforms.allowPrivateNetworks` |
| `browser_navigate` (Playwright) | `allowPrivateNetworks` in the `dsh-web-browser` config |
Set the relevant flag to `true` only in a trusted, network-isolated environment.
- **Browser approval** (fail-closed): `browser_open`/`browser_navigate` require approval per the `dsh-web-browser` `approval` setting (`never` | `navigate` | `all` — `navigate` gates navigation and script evaluation, `all` adds clicks and typing). If the approval service is unavailable or the call has no agent to route it through, the action is **denied**, not silently allowed.
- **Cache eviction** (LRU by usage): the store keeps at most `fetch.cacheMaxPages` page records (default 500) and `search.cacheMaxSearches` search records (default 1000), evicting the least-recently-accessed beyond the cap after each write. This keeps `web.db` bounded over time.
## Known limitations
- HTML SERP parsing is brittle; markup changes degrade to zero results until the parser updates (block detection converts silent empties into cooldowns).
- The plugin runs in the host DSH process with the host's privileges (a trusted static package); it is not sandboxed. Run DSH as a normal user, and in a network-isolated container/VM if the plugin may reach sensitive targets.
- Browser automation: one tab per agent session. Screenshots are saved to a file by default; pass `inline: true` to `browser_screenshot` to get base64 in the model context.
## Examples
### Keyless-only (no API keys, no SearXNG)
```yaml
dsh-web-automation:
search:
engines: [ddg, bing] # keyless only
enrich: true
```
### Full (all engines + SearXNG)
```yaml
dsh-web-automation:
search:
engines: [ddg, bing, exa, deepseek, jina, searxng]
searxng:
endpoint: http://localhost:8080 # your SearXNG instance
embedding:
endpoint: http://localhost:11434 # Ollama (or any /embeddings server)
model: nomic-embed-text
```
### News mode (time-filtered)
```yaml
dsh-web-automation:
search:
engines: [bing] # Bing supports the freshness filter
freshness: 24h # 24h | week | month | year
```
## Smoke test
After installing and applying the overlay, verify the stack end to end (in a DSH session):
1. `web_search "hello world"` — returns sources (DDG/Bing keyless).
2. `web_fetch https://example.com` twice — the second call is a cache hit (no network; check `web_search_stats`).
3. `web_platform_search { platform: "github", query: "schemastery" }` — returns GitHub sources.
4. `web_history` — shows the searches/fetches above.
5. `web_fetch http://127.0.0.1/` — fails with `WEB_SSRF_BLOCKED` (the SSRF guard).
6. (with `dsh-web-browser`) `browser_open` → `browser_navigate https://example.com` → `browser_screenshot` → `browser_close` — the screenshot file appears in the temp dir.
## Developer workflow
For working on the plugin itself (build, test, typecheck, local install):
```sh
git clone https://github.com/stelmakhdigital/dsh-web-automation.git
cd dsh-web-automation
npm install --legacy-peer-deps # @deepseek-ai/* peers are provided by the host DSH at runtime
npm run build && npm run build --prefix browser # bundle src/ → lib/ (both packages)
npm test # vitest (103 tests)
npm run typecheck # tsc against the local @deepseek-ai/* stubs
DSH_HOST=/path/to/deepseek-harness npm run typecheck:host # strict check against the real host types
```
`lib/` and `browser/lib/` are committed and CI enforces they stay in sync with `src/`, so git/tarball installs work without esbuild. To try a local copy in a profile:
```sh
npm pack # → dsh-web-automation-0.3.0.tgz
dsh plugin --profile tui add ./dsh-web-automation-0.3.0.tgz
```
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| `ERESOLVE` peer conflict on install | Peer deps absent outside DSH deployment | `npm install --legacy-peer-deps` |
| `Cannot find module '@deepseek-ai/...'` | Plugin installed without DSH's packages | Install DSH first (peers resolve to host's versions) |
| `WEB_PROVIDER_AMBIGUOUS` at startup | The `web` seam sees two usable search providers | Add the `web` seam pin row (`searchProvider: multi`, `fetchProvider: cached-http`) — see the overlay |
| `WEB_DUPLICATE_PROVIDER` at startup | Both the plugin and DSH's built-in web packages are loaded | Keep one — remove the built-in rows (or the plugin row); see "Relationship to DSH's built-in web packages" |
| `web_fetch` blocked (SSRF) | Target is loopback/private/link-local | Set `fetch.allowPrivateNetworks: true` (trusted env only) |
| SearXNG returns non-JSON | JSON API not enabled on the instance | Add `search.formats: [html, json]` to SearXNG's `settings.yml` |
| Embedding re-rank falls back to BM25 | Embedding endpoint unreachable | Check the endpoint URL + model name; BM25 is the fallback |
| Browser: `Chromium not found` | Playwright browser not installed | `npx playwright install chromium` |
| `web.db` grows large | Cache eviction caps too high | Lower `fetch.cacheMaxPages` / `search.cacheMaxSearches` |
## License
MIT — see [LICENSE](LICENSE).
Install
dsh plugin --profile web add github:stelmakhdigital/dsh-web-automation
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-automation 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.