DSH Browser

August 28, 2026 · View on GitHub

English | 中文

Embedded browser for the DSH Web GUI: browse the web and your workspace files inside the chat interface — multi-tab, address bar, per-workspace tab persistence — plus agent tools (browser_open, browser_read). Pages are rendered by a headless Chromium (Puppeteer) on the host, so sites that send X-Frame-Options load correctly.

An external plugin package for DeepSeek Harness (DSH). It is a single dual-face cordis bundle: the host half owns the agent tools, the /api/dsh-browser route family (Puppeteer page proxy + SSE open-event stream

  • workspace file listing/serving), the settings namespace, and the system-prompt announcement; the browser half renders the sidebar entry, the multi-tab panel, and the plugin settings card. Hot-pluggable — mounted via dsh plugin --profile <name> add link:<repo>.

Platform support. Works with both DSH Web and Desktop (requires DSH v0.1.1-rc.1 or later). The visual settings card works out of the box on both platforms — no DSH source changes needed. On Web it appears under Settings → Plugins; on Desktop it appears as a standalone "Embedded browser" entry in the left nav.

Prerequisites

A Chromium-based browser must be installed on the host machine (Chrome, Edge, or Chromium). The plugin auto-detects the executable on Windows, macOS, and Linux; you can also set an explicit path in the settings card. The plugin uses puppeteer-core (not puppeteer), so it never downloads its own Chromium.

What it does

  • Entry: a "Browser" row in the sidebar, below the New Session button.
  • Panel: takes over the center column with a tab strip, a toolbar (back / forward / reload / home / open-in-system-browser), an address bar (URL or search, Enter opens), and an iframe content area. Each page is rendered by a shared headless Chromium on the host — the proxy route waits for networkidle, reads the fully-executed DOM, injects a <base> and a link-interception script, and returns it to the iframe. Inactive tabs stay mounted and stateful; iframes lazy-load on first activation.
  • Link interception: clicks on http(s) links inside a proxied page are caught and posted to the panel — target="_blank" / window.open opens a new tab, ordinary links navigate the current tab. Nothing ever pops the system browser.
  • Tabs per workspace: the tab set is persisted per project root (localStorage, debounced + flushed on page hide). Switching sessions swaps the whole tab set; switching back restores it. A configurable cap (default 10) trims the oldest inactive tab.
  • Workspace browsing: the new-tab page lists the current workspace directory (folders navigate, breadcrumbs, up button); clicking a file opens it in the panel through the host's file route. HTML previews get a <base> injection so relative images/styles resolve, and a CSP sandbox header so a previewed file can never run scripts in the GUI origin.
  • Agent tools: browser_open pushes a URL into the panel (a new tab opens and the panel gains focus); browser_read fetches a page from the host and returns extracted readable text (static-HTML approximation, no JavaScript).
  • Settings card: On Web, an "Embedded browser" card appears under Settings → Plugins; on Desktop, a standalone "Embedded browser" page appears in the left navigation. Both support staged edits, save/discard, and inherit/reset semantics. Fields: enable, agent announcement, home page, tab cap, private-address override, browser executable path, proxy server.
  • Agent announcement: a system-prompt section tells every agent the plugin exists, what its tools do, and its limits (same mechanism dsh-ssh uses).

Install

# from a local checkout (development)
dsh plugin --profile <name> add link:<repo>

# from npm (once published)
dsh plugin --profile <name> add @nono-neko/dsh-browser

Restart dsh web; the sidebar entry appears. The web profile needs the @deepseek-ai/* client packages the bundle injects (any rc.6 web deployment has them). Make sure a Chromium-based browser is installed on the host.

Uninstall

# remove from a profile
dsh plugin --profile <name> remove @nono-neko/dsh-browser

# if installed from a local checkout
dsh plugin --profile <name> remove link:<repo>

Restart dsh web after removal.

Configuration

The plugin reads its settings from a layered source: schema defaults, then the plugin's cordis.yml entry (composition base), then the user settings document. All fields are optional.

FieldTypeDefaultDescription
enabledbooleantrueMount the sidebar entry, tools, and proxy routes.
announceToAgentbooleantrueInject a system-prompt section telling agents about browser_open / browser_read.
defaultHomestringhttps://www.bing.comURL loaded by the new-tab / home button.
maxTabsnumber10Per-workspace tab cap; oldest inactive tab is trimmed.
allowPrivateAccessbooleanfalseLet browser_read fetch private / loopback addresses.
browserExecutablestringauto-detectAbsolute path to a Chromium-based browser (Chrome / Edge / Chromium).
proxyServerstringemptyRoute Puppeteer traffic through a proxy, e.g. http://127.0.0.1:7890.

Visual settings card

The plugin provides an interactive settings form out of the box (requires DSH v0.1.1-rc.1 or later):

  • Web: Settings → Plugins → Embedded browser
  • Desktop: standalone Embedded browser entry in the left nav
Web settings cardDesktop settings page
Web settings cardDesktop settings page

Config file method (without the visual settings card)

If you prefer not to use the visual settings card, set the same fields directly. Two layers are available:

Plugin entry config (cordis.yml or your profile's plugin config) — the composition base, applies to every user of that profile:

plugins:
  dsh-browser:
    defaultHome: https://www.google.com
    maxTabs: 20
    proxyServer: http://127.0.0.1:7890

User settings document (~/.dsh/settings.yaml) — per-user overrides that layer on top of the entry config:

dsh-browser:
  browserExecutable: C:\Program Files\Google\Chrome\Application\chrome.exe
  allowPrivateAccess: true

FAQ

Q: Install fails with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED?

A: This happens when installing from git — pnpm blocks the prepare build script by default. Recommended fix: install from npm instead (pre-built, no build needed):

dsh plugin --profile <name> add @nono-neko/dsh-browser

If you prefer git install, add the package to allowBuilds in your profile's pnpm-workspace.yaml:

allowBuilds:
  - '@nono-neko/dsh-browser'

Development

pnpm install    # @deepseek-ai/* SDK packages are public on npm (or a mirror)
pnpm build      # tsc types + tsdown dual-half bundle (lib/index.js + lib/client.js)
pnpm typecheck  # tsc --noEmit
pnpm test       # vitest

The build emits two artifacts from one config: the node half (lib/index.js, esm) and the browser half (lib/client.js, a window.__ModuleLoader__ closure-factory served at /plugins/dsh-browser/client.js). CSS Modules are compiled into the client bundle by lightningcss; the client bundle enforces a purity gate — value imports from @deepseek-ai/* are only allowed for the platform seed modules, everything else must inline or go through cordis services.

Security model

  • Loopback fence: every /api/dsh-browser route (proxy, SSE, file) refuses non-loopback clients (socket address + Host header + same-origin markers). A LAN-exposed dsh web cannot serve workspace files or the proxy to unpaired devices.
  • Workspace gate: file listing and serving canonicalize the requested root (realpath) and require it to be a registered workspace or inside one; every requested path is re-checked after resolution, so symlinks cannot escape the root.
  • Served HTML sandbox: workspace-previewed HTML is served with Content-Security-Policy: sandbox — scripts never execute in the GUI origin (which holds the session's loopback API access).
  • Proxied pages are not sandboxed: the Puppeteer-rendered HTML is returned without CSP / X-Frame-Options so it can render in the panel iframe. The loopback fence is the security boundary — only local clients can reach the proxy route. Proxied pages cannot access the GUI origin's APIs because they are served from a different path and the browser's same-origin policy applies to the iframe content.
  • SSRF guard on browser_read: the target hostname is resolved through DNS before the request leaves the process and every address must be public (private/loopback/link-local/reserved ranges are refused). Redirects are followed manually and each hop is re-checked. The allowPrivateAccess setting is an explicit override; the risk is yours.
  • Proxy route uses Puppeteer: the headless Chromium fetches the page, so the SSRF guard from browser_read does not apply to the panel proxy. The proxyServer setting lets you route browsing traffic through a local VPN / proxy.
  • Size/time caps: browser_read bodies over 2 MB answer an error before being read; served workspace files over 64 MB are refused; each Puppeteer render times out after 30 seconds.

Limitations

  • No persistent login: each proxied page opens a fresh Puppeteer page and closes it after rendering. Cookies and login state are not retained between requests, so sites that require authentication will show a logged-out view.
  • GET only: the panel proxy supports GET requests. Form submissions (POST) and file uploads are not proxied — they will execute inside the iframe and may be blocked by the target site's X-Frame-Options.
  • JavaScript-rendered navigation: the initial page is fully rendered by Puppeteer, but subsequent in-page navigation (SPA routing, form posts) happens inside the iframe and may hit X-Frame-Options on the new URL. Ordinary <a> links are intercepted and re-proxied.
  • browser_read sees only static HTML: JavaScript-rendered pages come back without their client-side content, and it cannot use your logins.
  • Browsing consumes real network traffic on the host machine.

License

Apache-2.0