dsh-web-search-ddg
aooyoo
Zero-token DuckDuckGo search provider for the DeepSeek Harness (DSH) web seam — local headless browser, no API key, no m…
PROJECT TOPICS
PROJECT README
Install · Tools · Security model · Configuration · 中文文档
A DeepSeek Harness plugin that gives the model a real browser — navigate, click, fill, read, screenshot, tabs, downloads — backed by Chromium through playwright-core.
Its differentiator is a security-first design: every URL is re-checked against a host-label allowlist and a private-network block on every action and again after every navigation — including redirects, link clicks, popups, background loads, and downloads. And no tool ever accepts a filesystem path from the model: every file the plugin writes lands in a constrained directory under a name the plugin picked itself.
dsh plugin --profile web add dsh-plugin-browser-use
dsh --profile web --dump-config # should show the "# == dsh-plugin-browser-use" layer
A Chromium is required. The plugin resolves one in this order:
executablePath config$DSH_BROWSER_EXECUTABLEnpx playwright install chromium)| key | type | default | meaning |
|---|---|---|---|
allowedHosts |
string[] |
[] |
Host-label allowlist. example.com matches example.com and *.example.com; matching is on host labels, never substrings. Empty = any public host. |
allowPrivateNetwork |
boolean |
false |
Permit loopback / private / link-local targets. Off by default. |
headless |
boolean |
true |
Run without a window. |
executablePath |
string |
— | Explicit Chromium binary. Falls back to $DSH_BROWSER_EXECUTABLE, OS locations, then Playwright. |
userAgent |
string |
dsh-plugin-browser-use/<version> |
User-Agent for the browser context. |
proxyServer |
string |
— | Proxy for browser traffic (e.g. http://127.0.0.1:7890, socks5://…). Chromium ignores $http_proxy-style env, so set this (or $DSH_BROWSER_PROXY) on machines that need a proxy. |
proxyBypass |
string |
— | Comma-separated hosts that bypass the proxy. |
navigationTimeoutMs |
int ≥ 1 |
30000 |
Page-load budget per navigation. |
actionTimeoutMs |
int ≥ 1 |
15000 |
Budget per click / fill / read / screenshot. |
maxTextChars |
int ≥ 1 |
20000 |
Hard bound on extracted page text and snapshots. |
artifactsDir |
string |
private temp dir | Where screenshots / PDFs / downloads land. The plugin names every file itself. |
storageStatePath |
string |
— | Persist cookies/localStorage here on close, load at launch. The file holds live credentials — protect it. Last close wins across agents. |
uploadsDir |
string |
— (uploads disabled) | Only files directly inside this directory can be uploaded, by bare filename. |
| tool | model-facing result |
|---|---|
browser_navigate |
{ url, title, statusCode, text, truncated } of the landed page |
browser_navigate_background |
{ jobId, index, url } — loads in a new unfocused tab as a background job |
browser_click |
{ url, title, text, truncated } after the click's navigation settles |
browser_fill |
{ url, title } after filling a field |
browser_read_text |
{ url, title, text, truncated } re-read of the current page |
browser_read_snapshot |
{ url, title, snapshot, truncated } — aria role/name outline, selector-stable |
browser_screenshot |
{ path, url, title } — PNG under artifactsDir, plugin-named |
browser_pdf |
{ path, url, title } — PDF under artifactsDir (headless only) |
browser_downloads |
{ downloads: [{ index, url, suggestedFilename, state, path?, error? }] } |
browser_upload |
{ url, title } — attach a file from uploadsDir to an <input type=file> |
browser_tab_new |
{ index, url, title, statusCode? } — new active tab, policy-checked before creation |
browser_tab_list |
{ tabs: [{ index, active, url, allowed, title? }] } |
browser_tab_select |
{ index, url, title } — off-policy tabs cannot be focused |
browser_tab_close |
{ closed: true, remaining } |
browser_close |
{ closed: true } — closes the whole browser, idempotent |
Each agent session owns an isolated browser with an ordered tab list; calls from one owner are serialized in submission order (browser_navigate_background is the deliberate exception — its load runs outside the queue). All failures surface as ordinary isError tool results with a BROWSER_* code (never a thrown exception), e.g. BROWSER_HOST_NOT_ALLOWED, BROWSER_PRIVATE_NETWORK, BROWSER_NO_PAGE, BROWSER_NO_SUCH_TAB, BROWSER_UPLOAD_NOT_ALLOWED, BROWSER_JOBS_UNAVAILABLE, BROWSER_NAVIGATION_FAILED, BROWSER_ACTION_FAILED, BROWSER_LAUNCH_FAILED, BROWSER_CLOSED, BROWSER_ABORTED, BROWSER_UNSUPPORTED_SCHEME, BROWSER_INVALID_URL.
Background loads need the jobs registry (@deepseek-ai/dsh-jobs + @deepseek-ai/dsh-tool-jobs) in the profile; without it the tool fails closed with BROWSER_JOBS_UNAVAILABLE. Jobs register under kind browser.
browser_navigate checks the landed URL, so a redirect (or a link click) that crosses into a disallowed host is refused even when the requested URL was allowed. Background loads get the same pre-flight and post-landing checks.allowedHosts: ["example.com"] matches example.com and sub.example.com but not evil-example.com or evil.example.com.attacker.test.127.0.0.1, 10.0.0.5, ::1, ::ffff:127.0.0.1, fc00::/7 hex forms, …) are refused unless allowPrivateNetwork is explicitly enabled — and when an allowlist is set, private hosts must be on it too.http: and https: navigate; file:, data:, javascript:, and other schemes are refused outright.artifactsDir under names the plugin builds (download filenames are sanitized to one safe path segment). Uploads accept only a bare filename directly inside the operator-configured uploadsDir — separators and .. are refused, and without that config the tool is disabled.refused and never written to disk.HOME/XDG tree under the OS temp dir (removed on close), so profile, crashpad, and caches never touch the operator's real home.What the model sees: fifteen browser_* tool names; navigation and reads return the landed URL, title, HTTP status (navigation only), and bounded visible text or an aria outline. Screenshots/PDFs return paths, not image payloads — pair with an attachment-capable setup to show them to a vision model.
Token effect: each navigate / read / snapshot returns up to maxTextChars (default 20,000) characters; shrink it for cheaper sessions, grow it for dense pages. Click returns a fresh reading; fill, tab, and artifact tools return only small fixed shapes.
KV cache effect: none — results are ordinary tool results appended to the conversation; nothing rewrites the prompt prefix.
ctx.attachments for vision models is future work.goto cannot be interrupted mid-flight: cancelling a background job marks it and the navigation timeout bounds settlement.playwright-core needs a browser binary present (see Install).npm install
npm run build # tsc → lib/
npm test # vitest run (96 tests; live smoke self-skips)
The live smoke suite runs a real Chromium when DSH_BROWSER_LIVE=1 and a browser resolves (set DSH_BROWSER_EXECUTABLE to pin one):
DSH_BROWSER_LIVE=1 npm test
Layout: src/policy.ts (pure URL policy) → src/session.ts (browser session: tabs, artifacts, downloads, per-action re-checks) → src/tool.ts (dsh tool definitions, owner registry) → src/index.ts (plugin entry). CI runs typecheck + the keyless suite on Node 22/24.
MIT
This project is not affiliated with the browser-use project or company. Under the hood it drives Chromium directly through playwright-core; no browser-use code is involved.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。