dsh-browser
A browser panel for the DeepSeek Harness Web UI. It is a Cordis bundle: a Host half that adds fenced HTTP routes and one agent tool, and a web Client half that registers a browser main panel plus a rail icon.
中文说明见 README.zh.md.
Install
dsh plugin add Plonkloving/dsh-browser
The package declares dsh.bundle (cordis.patch.yml inserts one Host row) and dsh.client (the web half). There is no build step and no runtime dependency: the Client bundle is plain JavaScript loaded through window.__ModuleLoader__.
After installing, the rail icon (a globe, after Plugins and Schedules) opens the panel.
Host-side edits need a row reload — disable and re-enable the plugin row, or restart the Harness: the Loader keeps the imported ES module. Client edits reload through HMR.
What it does
Tabs and the omnibox
- Back / forward / reload / home, a tab strip, and an omnibox that treats input as a URL, a workspace file path, or a search query.
Ctrl/⌘-click, middle-click andAlt+Enteropen a new tab (Shift keeps it in the foreground). The same intent is reported from inside rendered pages.- Browser state — profiles, tabs, bookmarks, history, extension switches, download records — is stored in this origin's
localStorageunderdsh-browser.state.v1.
Search
The omnibox calls POST /dshbrowse/api/search, which runs ctx.web.search through the Harness web seam (the configured provider, deepseek-official, uses the native web_search server tool). The panel renders the returned sources[] natively and can export the list as CSV or JSON. Results are cached per query for the session, so the same search does not cost a second model call.
Browsing profiles bound to folders
Each profile owns its own tabs, bookmarks, history and extension switches. Binding a folder to a profile also registers it as a Harness workspace (workspaces.create({ path })), after which:
- New Agent session opens a session in that folder;
- the page action Send to Agent drafts a prompt from the page title, description and headings into a session in that workspace, then switches to the conversation.
Page rendering
Two views share one tab:
| View | How |
|---|---|
| Page (default) | GET /dshbrowse/api/render?url=… fetches the page with ctx.web.fetch, injects a <base> pointing at the real site so its CSS, images and fonts resolve, strips the site's scripts and its meta CSP / refresh, appends a navigation bridge, and returns the document from the panel's own origin. The frame is sandbox="allow-scripts allow-downloads allow-modals" — no allow-same-origin, so the document keeps an opaque origin. |
| Reader | GET /dshbrowse/api/page returns the same fetch verbatim; the client parses it with DOMParser into a document model (h1–h6, p, li, pre, img, table, links) that extensions can rewrite. |
Because the framed document is served from the panel origin, a site's X-Frame-Options / frame-ancestors no longer applies. The trade-off is that site JavaScript is stripped: a client-rendered SPA may need Reader instead.
Workspace files
Files the Harness writes into a workspace can be opened directly:
GET /dshbrowse/api/files?dir=…lists one directory;GET /dshbrowse/api/file?path=…serves one file. HTML/CSS are rewritten so a generated report keeps its stylesheet, images and scripts; everything else is streamed with the right content type andno-cache.- Root paths only: a canonical path outside every registered workspace directory (and the downloads folder) is refused with 403.
- HTML, SVG/PNG/JPEG/WebP, PDF, Markdown, plain text, JSON/CSV, video and audio are rendered inline; the new-tab page and a dedicated file browser list them.
POST /dshbrowse/api/inboxconsumers: thebrowser_opentool queues a URL or a workspace file and the panel opens it in a tab.
Extensions
A Chrome-style, declarative extension system:
| Field | Meaning |
|---|---|
permissions |
shown in the manager, informational |
contentScripts[].matches |
*, *://host/* patterns decide where a script applies |
contentScripts[].hide |
CSS selectors removed before the document model is built |
contentScripts[].highlight(s) |
words marked in the reader |
contentScripts[].width |
reader width |
action |
gives the extension a toolbar button and a panel |
Extensions cannot execute code. Built in: ad/tracker cleanup, keyword highlighting, focus reading (paragraph limit), page outline. The manager installs from a pasted manifest.json, enables/disables, removes, and offers three suggested manifests.
Downloads
Everything is written to a real folder, never overwritten (note (1).txt):
| Entry point | Product |
|---|---|
| Page → download | page HTML (relative references rewritten to absolute URLs), article Markdown, article plain text |
| File page → download | the workspace file; a copied HTML page has its relative references rewritten to file:/// paths so it still renders |
| Search results | CSV or JSON of the source list |
| Settings | one JSON with profiles, bookmarks, history, extensions and settings |
The target is $DSH_BROWSER_DOWNLOAD_DIR, else the system Downloads folder (Downloads, 下载, downloads). That folder is also a browse root, so a saved file can be reopened in a tab.
Routes
| Route | Purpose |
|---|---|
POST /search |
ctx.web.search |
GET /page |
ctx.web.fetch, raw body |
GET /render |
fetched page as a framed, script-free document |
GET /files |
list a directory inside an allowed root |
GET /file |
serve/stream one allowed file |
POST /download |
write {kind: file | url | text} to the Downloads folder |
GET /inbox |
queued browser_open requests |
All of them hang off /dshbrowse/api and run behind the Harness browser-trust fence (ctx.connection.requestRejection), the same check the built-in /api channel uses. Nothing is proxied through a third party.
Security model
- Every route is refused unless the request passes
ctx.connection.requestRejection. - Path admission:
realpaththe request, then require containment in a registered workspace directory or the downloads folder; traversal and symlink escapes answer 403. - Rendered and local pages run in a sandboxed frame without
allow-same-origin, so their markup cannot reach the Harness API even though it is served from the panel origin. Proxied pages additionally have their scripts stripped. - Extension manifests only contribute selectors, words and a width; there is no code execution path.
- Download file names are sanitized and writes never overwrite.
Layout
dsh-browser/
├── package.json dsh.bundle.patch + dsh.client
├── cordis.patch.yml inserts one Host row (id: dsh-browser)
├── index.js Host: routes + browser_open tool
├── lib/files.js path admission, listing, HTML/CSS rewriting, streaming
├── lib/downloads.js downloads folder, name sanitizing, unique targets
├── client.js Client: rail icon + browser panel (UI, document model, extensions)
├── locale/zh.json, en.json
└── icon.svg
Compatibility
- Host services:
web,webServer,connection(required),tools(optional — enablesbrowser_open). - Client injections:
@deepseek-ai/dsh-client-ui-layout(declaresmainandsidebar.panellist),@deepseek-ai/dsh-client-locale. - No
dependencies; the official@deepseek-ai/*packages resolve from the installation.
License
MIT — see LICENSE.
No comments yet. Be the first to write one.