LLMs Technical Reviews

omdsh-dev/dsh-browser

DeepSeek Harness plugin plus Chrome/Firefox extension that operates your real tabs through text-only, index-addressed browser tools.

GitHub ↗★ 772TypeScriptMITcommit 23895d7 · 2026-10-01

Overview

dsh-browser connects DeepSeek Harness (dsh) to the Chrome or Firefox tabs you already have open. It ships as one pnpm workspace with two halves: a dsh plugin, @yuxianglin/dsh-bridge-browser, that registers 15 browser_* tools with the agent, and an MV3 extension that carries out those tools in your real browser profile. A side panel in the extension doubles as a chat client for dsh. Logins, cookies and open tabs are the user’s own, because no new browser is launched.

The design is deliberately text-only. A page reaches the model as a structured text snapshot: title, URL, a main-content excerpt, a numbered list of interactive elements and a list of form fields. The model then acts by number (“click 7”). No screenshots are taken for page control, and the extension does not use CDP or the debugger permission. Every action is a DOM operation performed by a content script.

This is a tool layer, not an agent. The planning loop, history, model choice and tool calling all belong to dsh. The repository adds the bridge, the extension, an approval system for state-changing actions, and a benchmark suite that compares the extension backend with a Playwright-based backend that uses the same tool contract.

Architecture

flowchart LR
  M["dsh agent loop"] --> T["browser_* tools (plugin)"]
  T --> S["BridgeServer /ext/bridge"]
  S -->|"tool.call over WebSocket"| B["Extension service worker"]
  B --> A["Approval policy + side panel"]
  B --> R["Tab affinity: controlled tab"]
  B -->|"chrome.tabs.sendMessage"| C["Content script per frame"]
  C --> SN["Text snapshot + stable ids"]
  C --> AC["DOM actions: click/type/press"]
  P["Side panel (React)"] -->|"rpc frames"| S
  S --> G["dsh Gateway: sessions, models"]
Component Path Role
Plugin entry packages/browser/bridge-browser/src/index.ts Cordis plugin: config, token, upgrade route, discovery route, tool registration, system-prompt hint
Tool set packages/browser/bridge-browser/src/tools.ts 15 browser_* definitions; each forwards to the bridge and returns one text block
Bridge server packages/browser/bridge-browser/src/server.ts WebSocket auth (hello), single active connection, tool.call/tool.cancel, gateway RPC passthrough
Wire protocol packages/browser/bridge-browser/src/protocol.ts Frame types, error codes, BridgeCaps (text-only, snapshot budgets)
Session helpers session-deferral.ts, session-workspace.ts, browser-context.ts Lazy session creation, workspace grouping, injecting a followed tab’s snapshot into the agent
Service worker extensions/dsh-browser/src/background/index.ts Bridge client, settings, approvals, tab affinity, tool routing
Tool dispatch extensions/dsh-browser/src/background/tools.ts Frame discovery, approval checks, content-script messaging and re-injection, tab-level tools
Authorization background/authorization.ts, src/security/ Read vs action prompts, origin trust, untrusted-content wrapping
Content script extensions/dsh-browser/src/content/ Snapshot, stable element ids, privacy masking, DOM actions, settle detection
Side panel extensions/dsh-browser/src/panel/ React chat UI, approval dialogs, sessions, image attachments
Benchmark benchmark/ Paired extension vs Playwright runs on a local task site

How a request flows

Take a model call to browser_click with index: 7:

  1. Tool call. dsh runs the tool registered in registerBrowserTools, which calls bridge.requestTool(name, args, signal, timeout, sessionId) (tools.ts).
  2. Dispatch. requestTool fails fast with bridge-closed if no extension is connected, then sends a tool.call frame with an expiresAt deadline. On timeout or abort it sends tool.cancel, so a late approval click cannot run an expired action (server.ts).
  3. Route. In the service worker, routeToolCall builds an abort controller and resolves the session’s controlled tab through resolveToolTab before calling dispatchToolCall (background/index.ts).
  4. Validate and authorize. dispatchToolCall lists the tab’s frames, checks that index 7 still refers to a live element in the target document, and asks approvalPromptForCall whether a prompt is needed. If the user approves, it re-lists frames and refuses to proceed when the page changed while the dialog was open (background/tools.ts).
  5. Execute. The worker sends a DSH_ACTION message to the right frame with chrome.tabs.sendMessage (background/tools.ts). If the content script is missing, for example in a tab that was open before the extension loaded, it injects it with chrome.scripting.executeScript and retries once.
  6. Act. clickAction scrolls the element into view and calls el.click(). Links get special handling: the content script dispatches a cancelable click so SPA routers can intercept it, then sets location.href itself for ordinary same-frame links (actions.ts).
  7. Return. After a MutationObserver-based settle wait, the result text includes a delta snapshot of what changed. It travels back as tool.result and becomes a single text content block for the model.

Key components

Text snapshot and stable ids

buildSnapshot collects visible interactive elements, sorts open-dialog elements first and in-viewport elements next, and caps the list at maxItems (60 by default) (snapshot.ts). Ids come from ElementIds, a WeakMap registry that gives each element a number once and keeps it for as long as the element exists. It also writes a data-dsh-el attribute on the element (ids.ts). Because ids survive re-snapshots, renderSnapshot can emit a delta that lists only changed and removed elements, and it warns the model when more than half the set was renumbered (snapshot.ts).

Privacy and prompt-injection boundary

isSensitiveField masks password inputs, cc-* autocomplete fields and any field whose id, name or aria-label matches patterns such as password, card or secret (privacy.ts). The match is coarse, so it also masks harmless fields whose names contain those words. Page text is wrapped in an UNTRUSTED_PAGE_CONTENT block with a fresh nonce, so page content cannot forge the closing tag (untrusted.ts). The code itself calls this defense in depth. The real enforcement is the approval step.

Approvals

approvalPromptForCall classifies reads (browser_snapshot, browser_get_text) and state-changing actions, and computes the origins involved. Cross-origin navigations and back/forward can never be added to the trust list (authorization.ts). authorizeToolCall skips the prompt when unrestricted access is on or the origin is trusted for the session or permanently. Otherwise it shows a side-panel dialog that times out after 60 seconds (background/index.ts). Page reads default to sharePageContent: 'auto', so out of the box only actions prompt (background/index.ts).

Bridge authentication

The first frame must be hello. A loopback socket whose Origin starts with chrome-extension:// may skip the token. Everything else, including Firefox’s per-install moz-extension:// origins, must present the 256-bit bearer token (server.ts). Privileged gateway methods such as settings.* and credentials.* stay loopback-only even with a valid token. Only one extension connection is active at a time, and a new one replaces the old.

Tab affinity and session context

TabAffinityController binds tools to one user-controlled tab per session. When the user switches tabs, dispatch pauses for a keep/follow decision rather than silently acting on the new tab. When the user follows a tab, BrowserContextInjector pushes a fresh snapshot into the agent’s inbox, replacing any older one, so the next turn can use its indices directly (browser-context.ts).

Extending it

  • New tools. Add a definition in tools.ts and a matching action in the content script’s runAction switch. The tool name doubles as the wire action name.
  • Budgets. snapshotMaxChars, maxInteractiveItems and toolTimeoutMs are plugin config. They are negotiated to the extension in hello.ok, so there is no shared config file (index.ts).
  • Model guidance. The plugin adds a one-paragraph system-prompt section telling the model to snapshot on demand and reuse injected snapshots (index.ts).
  • Other clients. The protocol is exported as @yuxianglin/dsh-bridge-browser/protocol, so a different extension or client could speak it.

Running it

  • Requirements. Node.js ^22.19 or >=24, pnpm, Chrome 116+ or Firefox 140+, and dsh 0.2.0-rc.2 or a compatible newer version. The plugin refuses to mount if the gateway lacks wireStream (index.ts).
  • Install. scripts/install.sh or install.ps1 builds the plugin, registers it in the dsh web profile and builds the unpacked Chrome extension, which you then load from chrome://extensions. pnpm start runs dsh web. Firefox needs a source build and a temporary add-on.
  • Discovery. With no bridge URL set, the extension probes 127.0.0.1 on a fixed list of dsh web and desktop ports for /ext/bridge-config (background/index.ts).

Strengths and caveats

  • Strength: the user’s real session. There is no headless copy and no profile import. Whatever the user is logged into, the model can reach, subject to approval.
  • Strength: careful approval semantics. Approvals are origin-scoped, re-validated after the dialog closes, cancelled when the bridge times out, and denied if nobody answers within 60 seconds. With the panel closed, the user gets an OS notification instead of a silent pass.
  • Strength: cheap, stable addressing. Indices persist across snapshots and delta output keeps follow-up turns small. The repo’s own paired benchmark shows fewer tool calls than its Playwright baseline (3.4 vs 4.7), though it is a six-task suite on a local site.
  • Caveat: synthetic events. Clicks, key presses and typing are DOM calls (el.click(), dispatchEvent, native value setters), so isTrusted is false. Sites that check for trusted input, or that rely on real key events for typing, may not respond. browser_press dispatches only keydown/keyup.
  • Caveat: dsh only. The tools are dsh tool definitions and the panel speaks the dsh gateway. Using it with another agent means reimplementing the plugin side.
  • Caveat: broad loopback trust. The token-free path accepts any chrome-extension:// origin, not only this extension’s id. Any other installed Chrome extension can therefore connect to a local bridge without the token.
  • Caveat: one connection. A second browser window or profile replaces the first connection, and non-loopback deployment is explicitly discouraged.

Sources: code at 23895d7, deepwiki-open wiki (12 pages), OpenDeepWiki wiki (15 pages), verified Q&A.

How it answers the Browser & computer control questions

Each answer was drafted by a code-reading agent at commit 23895d7. Its citations were checked mechanically. Compare with the other browser & computer control →

How is the page represented to the model?

answered

The page is represented as structured text only - no screenshots or accessibility tree dump. The BridgeCaps type enforces textOnly=true (protocol.ts:64-66). renderSnapshot() (snapshot.ts:363-420) produces one text block: URL, title, ready-state, main content excerpt, a numbered interactive-element inventory, and form fields. buildSnapshot() (snapshot.ts:180-306) orchestrates: collectInteractive() (extract.ts:146-155) queries the DOM against a fixed CSS selector (a[href], button, input:not([type=hidden]), ARIA roles), filtered by isVisible() (extract.ts:40-46). Elements sort dialog-first then viewport-first (snapshot.ts:195-196). Each element gets a stable numeric id via ElementIds (ids.ts:19-71), backed by WeakMap plus data-dsh-el attribute. Main text uses a readability-lite heuristic preferring main/article/largest-section-with-paragraphs (extract.ts:165-185). Three budgets cap output: maxChars (default 32,000), maxItems (default 60), maxForms (default 30) - set in protocol.ts:37-40 and passed as SnapshotBudget. Truncation notes explain cuts (snapshot.ts:355-361). Delta mode returns only changed/removed elements (snapshot.ts:268-286). Sensitive fields (password, credit-card) are masked via isSensitiveField() (privacy.ts:30-41) with real values replaced by bullets (privacy.ts:49-51).

How are actions executed and how are elements targeted?

answered

Actions execute in the user real browser via the content script (not CDP or Playwright). The bridge sends a tool.call frame over WebSocket (server.ts:226-233); background routeToolCall() (background/index.ts:1058-1199) dispatches via chrome.tabs.sendMessage() (background/tools.ts:157-164). runAction() (actions.ts:168-196) routes to handler functions. Elements are targeted by stable inventory index via elementOrThrow() (actions.ts:126-132) calling ids.elementByIndex() - no CSS selectors or coordinates. clickAction() (actions.ts:226-291) calls scrollIntoView then el.click() with MutationObserver page-settle. Controlled same-frame links dispatch a MouseEvent then set location.href; native activation is preserved when the link has noreferrer/ping attributes (actions.ts:244-254). typeAction() (actions.ts:365-381) uses a native-value setter via Object.getOwnPropertyDescriptor (actions.ts:146-158) for React/Vue inputs, dispatching input/change events. ContentEditable hosts use execCommand(insertText) (actions.ts:338-363) so Lexical/ProseMirror process input. pressAction() sends keydown/keyup events (actions.ts:383-394). scrollAction() uses window.scrollTo/scrollBy (actions.ts:396-417). Tab-level operations (navigate, back, forward, reload, open/close tabs) use chrome.tabs APIs (background/tools.ts:217-241, 864-974) - the only path for protected DOM pages. All actions settle with a MutationObserver-based DOM-quiet detector (actions.ts:53-120) with configurable policies. Actions returning page state include a delta snapshot (actions.ts:216-224).

How is the agent loop / planning implemented?

answered

The agent loop is owned by DeepSeek Harness itself - this repository is a plugin providing browser tools that DSH calls. The bridge registers tools via registerBrowserTools() (tools.ts:83-101) using defineTool() from @deepseek-ai/dsh-tools. Each tool declares typed parameters and timeoutMs. When DSH calls a tool, the bridge dispatches a tool.call frame over the WebSocket (server.ts:226-233) to the connected extension. The extension background receives it in routeToolCall() (background/index.ts:1058-1199), resolves the controlled tab via TabAffinityController (tab-affinity.ts), dispatches to the content script, and sends tool.result back. In-flight calls have a per-call timeout (default 90s, index.ts:56) tracked by setTimeout in BridgeServer.pendingTools (server.ts:111-115). The tool.cancel frame (protocol.ts:101-102) withdraws timed-out or cancelled calls. DSH maintains history between steps; the bridge adds a system-prompt section telling the model to call browser_snapshot on demand rather than hoarding page text (index.ts:262-272). Tab-affinity management ensures manual tab switches create a handoff decision that blocks dispatch until resolved (tab-affinity.ts:6-12). Session-to-tab bindings persist via PageSessionContextTracker (session-continuity.ts:40-88). The TransientEventCache (transient-events.ts:9-53) preserves panel state across reconnects. Sessions are deferred via withSessionDeferral() (session-deferral.ts:54-172) so they materialize only on first prompt.

How are failures, retries and self-healing handled?

answered

Failure handling spans three layers. Bridge layer: BridgeServer.requestTool() sets a per-call timer (server.ts:220-223) and wraps cancellation via AbortSignal. On timeout it sends tool.cancel and rejects with BridgeToolError(timeout) (server.ts:221-222). Socket replacement settles all pending tools as bridge-closed (server.ts:514-518). The BridgeClient (background/bridge.ts:111-230) implements exponential-backoff reconnection: base 500ms, x2 per attempt, capped at 10s, jittered 0.5-1x (bridge.ts:225-229). Extension layer: dispatchToolCall() (background/tools.ts:671-797) handles content-script failures. When sendAction catches Receiving end does not exist (tools.ts:166-167), it auto-injects the content script via injectContentScript() (tools.ts:134-148) and retries the dispatch (tools.ts:756-796). TabManagementContext.commitAction/rollbackActionCommit (background/index.ts:1074-1078) track whether a state-changing operation can be withdrawn. validateElementTarget() (background/tools.ts:809-818) rejects stale element references by comparing frame document keys. Server layer: session-purge.ts (session-purge.ts:76-154) handles durable storage errors with typed codes. Session deferral provides 30-minute TTL for abandoned sessions (session-deferral.ts:62-69). The MutationObserver settle detection (actions.ts:53-120) has hard caps so continuously-animated pages cannot stall tools.

Which models are supported and how are they called?

answered

This repository is a plugin for DeepSeek Harness, which is model-agnostic - it supports any provider through its plugin architecture. The bridge itself never calls models; it only defines browser tools that DSH agent loop invokes. Model selection flows through DSH Host API: session.models calls session/modelCatalog on the gateway (remote-host-api.ts:230-248), returning available model groups, current selection, and failures. adaptModelCatalog() (remote-host-api.ts:947-962) normalises this into a provider/model record. The session-deferral wrapper (session-deferral.ts:128-149) handles model queries for provisional sessions. Model selection (provider, model, optional reasoningEffort) can be set before the first prompt (session-deferral.ts:133-149). There are no vision or screenshot capabilities - BridgeCaps sets textOnly=true (protocol.ts:64-66). The README benchmarks (README.md:46-48) use deepseek-v4-flash. Tools are defined with structured JSON-schema parameters via defineTool() from @deepseek-ai/dsh-tools (tools.ts:117+), and DSH handles tool-calling through its normal agent loop - the bridge just routes calls to the extension and returns text results.

How are browser sessions, profiles, auth and anti-bot handled?

answered

Sessions are grounded in the user real browser - the extension runs as a first-party Chrome/Firefox MV3 extension, so all login state, cookies, and local profiles are inherited naturally. There is no headless browser, no Playwright, no separate profile management. The bridge authenticates the extension via a bearer token presented in the hello frame within HELLO_TIMEOUT_MS (server.ts:276-279). Loopback connections can skip the token if Origin comes from chrome-extension:// (server.ts:304-309); non-loopback and Firefox connections always require the token. The token is generated once and persisted as a 0600 file (token.ts:72-78). Session-to-tab binding is managed by PageSessionContextTracker (session-continuity.ts:40-88), mapping (tabId, windowId, urlKey) to sessionIds, persisted in chrome.storage.session. TabAffinityController (tab-affinity.ts:58+) tracks controlled-tab state and blocks tool dispatch during user tab switches with a handoff decision. Bridge sessions are grouped into a dedicated workspace via withSessionWorkspace() (session-workspace.ts:26-82). Session creation is deferred until first prompt by withSessionDeferral() (session-deferral.ts:54-172); abandoned provisional sessions expire after 30 minutes. Approved action origins are cached per session (background/index.ts:189). The ApprovalCoordinator (approval-coordinator.ts:25-101) manages per-action user approval with a 60s timeout. There are no proxies, CAPTCHA handlers, or stealth measures - the model uses whatever browser state the user already has.