Vision & Multimodal
dsh-chrome
gemone/dsh-chrome · ★ 2
DeepSeek Harness plugin: model-facing Chrome DevTools Protocol (CDP) tools — navigate, evaluate, screenshot, s
安装
dsh plugin --profile web add github:gemone/dsh-chromeGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
A DeepSeek Harness plugin that gives the agent model-facing tools to drive a real Chrome browser over the Chrome DevTools Protocol (CDP).
10 tools covering navigation, JS execution, page reading (text and structured), clicking, scrolling, typing, keyboard input, screenshots, and tab/browser lifecycle — a complete browser automation surface for AI agents.
Authoring follows the DSH adding-a-tool and plugin packaging guides. Tools are defined with
defineTool; the Chrome lifecycle lives in actx.chromeCordisService.
Tools
| Tool | What it does | CDP |
|---|---|---|
chrome_navigate |
Open a URL, wait for a lifecycle event (load/domcontentloaded/networkidle0/networkidle2). Returns final URL + title. |
Page.navigate |
chrome_evaluate |
Evaluate JS, return JSON value. Exceptions returned as exceptionDetails (not an error). |
Runtime.evaluate |
chrome_snapshot |
Read the page, bounded: format: "text" returns the rendered visible text; format: "markdown" converts the page to GitHub-flavored Markdown (headings, links, lists, tables, code blocks). |
Runtime.evaluate |
chrome_page_snapshot |
use-browser-style structured read: an element tree (roles, names, states) where every interactive element carries a [ref=eN] id. No image support needed. |
Runtime.evaluate |
chrome_click |
Click the center of an element targeted by CSS selector or [ref=eN] (real mouse events). Reports coveredBy when an overlay intercepts the click point. |
Input.dispatchMouseEvent |
chrome_screenshot |
Capture screenshot. Image-capable route → image block; text-only route → described in text by the configured screenshotAnalysis model, or refuses with text-only guidance. Full-page captures exceeding the deployment's admission limits (per-side pixels, total pixels) are scaled at capture time instead of being refused. |
Page.captureScreenshot |
chrome_scroll |
Scroll page by pixel amount or scroll an element (selector or ref) into view. | Runtime.evaluate |
chrome_type |
Type text via real keyboard events (Input.insertText), optionally focusing a selector/ref first. React/Vue/Angular-friendly. |
Input.insertText |
chrome_press_key |
Press a special key: Enter, Tab, Escape, arrows, F1–F12, etc. | Input.dispatchKeyEvent |
chrome_close |
Close current tab (Target.closeTarget, browser stays alive) or entire browser (kill process). Auto-reconnects on next call. |
Target.closeTarget / dispose |
chrome_net_list |
List the network requests the page made since the session connected (URL, method, status, resource type, failures). failedOnly: true filters to failures. |
Network.* events |
chrome_net_body |
Read one captured response body by requestId (base64-decoded, bounded). Chrome evicts bodies as the page proceeds — fetch promptly. |
Network.getResponseBody |
chrome_console_read |
Read console output: console.* calls, uncaught exceptions, browser log entries (CSP, failed loads). level filters by severity (default error); all: true dumps the buffer. |
Runtime/Log events |
Page-activity auto-hints
The observatory (src/observatory.ts) ring-buffers network + console activity per
session (500 entries per stream by default). Reading tools (chrome_navigate,
chrome_snapshot, chrome_page_snapshot) append a [page activity: …] line
when unread console errors or failed requests exist, so the model discovers
page failures without polling. chrome_console_read / chrome_net_list
advance the read cursors.
Each tool registers a systemPrompt.section guidance entry and a UI card presenter.
Install
From GitHub
dsh plugin --profile web add github:gemone/dsh-chrome
pnpm ≥10 blocks git-hosted packages' prepare scripts by default. On first install, pnpm prints an error with the package key — add it to the profile's pnpm-workspace.yaml:
onlyBuiltDependencies:
- dsh-chrome
Then re-run the add. This file lives at $DSH_HOME/profiles/web/pnpm-workspace.yaml.
Pin a commit (
github:gemone/dsh-chrome#<sha>) so a later push cannot change what builds on your machine.
From a local checkout
cd dsh-chrome && pnpm install && pnpm build && cd ..
dsh plugin --profile web add ./dsh-chrome
Local add uses link: and does not run prepare, so you must pnpm build first.
From npm or a tarball
dsh plugin --profile web add dsh-chrome
dsh plugin --profile web add ./dsh-chrome-0.4.0.tgz
No build permission needed — lib/ is pre-built.
Verify and boot
dsh --profile web --dump-config # a "# == dsh-chrome" layer with the chrome-tool row
dsh --profile web
Remove with dsh plugin --profile web remove dsh-chrome.
Configuration
All options live under the chrome-tool: section. Every field is optional with sensible defaults.
| Option | Default | Description |
|---|---|---|
executablePath |
discovered | Chrome/Chromium path. Env DSH_CHROME_EXECUTABLE_PATH → well-known paths → PATH. |
launch |
true |
Launch a managed Chrome. false = attach to cdpUrl. |
cdpUrl |
— | Existing CDP endpoint when launch: false (ws://… or http(s)://…). Required when launch is false. |
port |
9222 |
Remote debugging port. |
host |
127.0.0.1 |
Chrome debug endpoint host. |
headless |
"new" |
true (old headless), "new", or false (visible window). |
userDataDir |
temp dir | Persistent profile directory. |
extraArgs |
[] |
Extra Chrome CLI flags. |
operationTimeoutMs |
30000 |
Per-operation timeout (tool timeoutMs). |
networkIdleMs |
500 |
Quiet window before networkidle*. |
snapshotMaxChars |
200000 |
Cap on chrome_snapshot output. |
pageSnapshotMaxNodes |
500 |
Cap on chrome_page_snapshot tree nodes. |
screenshotAnalysis |
— | { provider, model, prompt?, maxTokens? }: an image-capable route that describes screenshots in text when the agent's own route cannot see images. See below. |
navigate / evaluate / snapshot / pageSnapshot / click / screenshot / scroll / type / pressKey / close |
true |
Enable/disable individual tools. |
Example profile patch (cordis.patch.yml):
- id: chrome-tool
config:
headless: false # visible Chrome window
port: 9333
snapshotMaxChars: 100000
Config edits to cordis.patch.yml hot-apply — no profile restart needed (DSH watches the file transactionally).
Connect to an already-running Chrome
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
- id: chrome-tool
config:
launch: false
cdpUrl: "http://127.0.0.1:9222"
Reading the interface without images
Not every route can see images, and screenshots are not the only way to read a page. The plugin gives a text-only model three interface-acquisition paths, and can optionally delegate screenshots to a vision model:
chrome_page_snapshot(preferred,use-browser-style). One call returns the page as a structured element tree — roles, accessible names, input values/states — where every interactive element carries a[ref=eN]id:- heading "Sign in" [ref=e1] - textbox "Email" value="" [ref=e2] - textbox "Password" [ref=e3] - button "Sign in" disabled [ref=e4] - link "Forgot password?" href="/reset" [ref=e5]Act on what you saw by ref —
chrome_click { "ref": "e4" },chrome_type { "ref": "e2", "text": "a@b.c" },chrome_scroll { "ref": "e1" }— no CSS selector authoring. Refs are invalidated by navigation (and reconnection); acting on a stale ref fails with a "take a fresh snapshot" error instead of clicking something stale.chrome_snapshotwithformat: "markdown". The page converted to GitHub-flavored Markdown — headings, links with absolute URLs, emphasis, nested lists, fenced code blocks, GFM tables, images. Ideal for archiving a page's content or answering "what does this page say" in a form both models and humans read naturally. Cross-origin iframes (unreachable from the top frame) are emitted as<!-- iframe: src -->markers.format: "text"(the default) returns the plain rendered visible text.chrome_evaluate— arbitrary DOM queries for anything the structured tree does not surface. When an expression returns a non-JSON value (DOM node, function), the result hint teaches theJSON.stringify(...)idiom.
chrome_screenshot tries the image path first: on an image-capable route the
capture enters context as an image block. On a text-only route with a
configured screenshotAnalysis model, the capture is durably saved and
described in text by that model (steer it per call with the tool's prompt
argument). On a text-only route without one, it refuses before capturing,
with an error that points at the three text tools above.
- id: chrome-tool
config:
screenshotAnalysis:
provider: openai # any registered provider route
model: gpt-4o-mini # any model that declares image input
# prompt: custom instruction for the description (optional)
# maxTokens: 1024 # output cap for the description (optional)
For the full map of what can fail (launch, endpoint, protocol, page-level, model-route) and how each failure recovers, see docs/failure-modes.md.
Security
Driving a real browser is shell-equivalent power: it can reach the local network, run arbitrary page JavaScript, read rendered content, and exfiltrate data. This plugin is not a sandbox.
- Prefer headless + ephemeral
userDataDir. Warn before attaching to a profile with real credentials. - Enforce allow/ask via
tools/pre-execute(deployment concern); the plugin registers tools and does not invent policy. - Chrome is a child process outside any agent sandbox. Only install plugins you trust.
Architecture
src/
├── index.ts plugin entry: name/inject/Config(schemastery)/apply
├── service.ts ChromeRuntime Service → ctx.chrome (lifecycle + CDP orchestration)
├── cdp.ts CDP client over ws (flattened sessions, abort-aware)
├── launcher.ts executable discovery, Chrome spawn, /json/version polling
├── page-walker.ts in-page DOM walker: element tree + [ref=eN] → selector map
├── markdown.ts in-page DOM→Markdown converter (chrome_snapshot format)
├── analysis.ts one-shot vision-model call that describes a saved screenshot
├── types.ts error codes + shared types
└── tools/ 10 defineTool consumers
├── navigate.ts chrome_navigate
├── evaluate.ts chrome_evaluate
├── snapshot.ts chrome_snapshot (text)
├── page-snapshot.ts chrome_page_snapshot (structured tree + refs)
├── click.ts chrome_click (selector or ref)
├── screenshot.ts chrome_screenshot (image path + analysis path, gated)
├── scroll.ts chrome_scroll (selector or ref)
├── type.ts chrome_type (Input.insertText, focus by selector/ref)
├── press-key.ts chrome_press_key (Input.dispatchKeyEvent)
├── ref-target.ts shared selector/ref argument resolution
├── net.ts chrome_net_list / chrome_net_body (Network events + getResponseBody)
├── console.ts chrome_console_read (Runtime/Log events)
└── close.ts chrome_close (Target.closeTarget / dispose)
- One bundle, provider-shaped seam.
ChromeRuntimeisctx.chrome; its interface allows a future split into capability seam + provider + consumer (mirroringctx.web). - Flattened CDP sessions. One browser-level WebSocket drives a page target via
Target.attachToTarget({ flatten: true }). - Connected-browser state. After
closeTab(), the browser connection stays alive; the next tool call auto-creates a fresh page target — no Chrome restart needed. - Ephemeral page refs.
chrome_page_snapshotkeeps aref → selectormap inside the runtime; navigation clears it, and ref-based tools fail loudly on stale ids. - Lazy, fiber-scoped lifecycle. Chrome starts on first use; torn down (socket closed, child killed) with the owning plugin fiber. HMR-safe.
- Caller-signal honored. Every operation forwards
exec.signal; cancellation stops the wait, not Chrome.
Requirements
- DeepSeek Harness
^0.2.0-rc.2(attachment admission limits and message-source extensibility this plugin builds on) - Chrome or Chromium on the host (or a reachable CDP endpoint)
- Node ≥20
Development
pnpm install
pnpm run typecheck # tsc --noEmit
pnpm run build # tsdown → lib/index.js + lib/index.d.ts
pnpm test # unit + plugin tests (no Chrome needed)
DSH_CHROME_INTEGRATION=1 pnpm run test:integration # real headless Chrome
License
MIT