# dsh-browser-playwright Playwright-powered browser capability for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): the agent drives a **visible, profile-backed browser window** through accessibility snapshots with stable element refs — no CSS-selector guessing, no full-DOM dumps. Login state survives window close and harness restarts (Codex-style personal browser), tabs, screenshots as durable image attachments, structured extraction, gated JavaScript evaluation, and Codex-inspired reliability guards (bounded render-stability waits, post-action verification, fast-fail actionability checks, crash auto-recovery) and native-dialog handling (alert/confirm/prompt/beforeunload are parked as a pending state, reported to the model, and answered only by an explicit `browser_dialog` call — never auto-accepted; see [DIALOG-POLICY.md](DIALOG-POLICY.md)). ## Install ```sh dsh plugin --profile add dsh-browser-playwright ``` The bundle mounts three rows: the `ctx.browser` seam (`service`), the Playwright provider (`playwright`), and the model-facing tool family (`tool`). ### Browser binary The provider probes `chromium` → `chrome` → `msedge` → `edge` channels automatically; any installed Chromium-family browser works with **zero download**. To use Playwright-managed Chromium instead: ```sh npx playwright-core install chromium # Restricted networks (e.g. mainland China): PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright npx playwright-core install chromium ``` Or pin a binary: `launch.executablePath` / `launch.channel` in the plugin config. ## Tools The default `toolPrefix` is `browser_`. Every action returns a fresh snapshot, so refs always come from the latest result. The registered set is capability-gated: `browser_evaluate`, `browser_cdp` and `browser_extract` appear only when their capability is switched on (their schemas are resident in the system prompt on every turn, so a tool that could only answer "disabled" would be pure prompt cost). Count the live surface with `npm run cost` — this package ships `cordis.patch.yml` with `allowEvaluate: true` and `allowCdp: true`, so a profile mounting this bundle sees 22 tools (23 once `extract` is configured). | Tool | Purpose | |---|---| | `browser_navigate` | Open a URL (http/https only) and return the snapshot | | `browser_snapshot` | Snapshot the current page: URL, title, visible elements with `ref=` ids | | `browser_click` | Click the element with a `ref` — or a visible label the snapshot gave no ref, e.g. `{ text: "搜索设置" }` — from the latest snapshot; a dialog raised by a click stays pending — answer it with `browser_dialog` | | `browser_hover` | Rest the pointer on a `ref` — or on a visible label the snapshot gave no ref, e.g. `{ text: "设置" }` — so hover-revealed content appears (submenus, tooltips, previews, player controls); the returned snapshot names the target and notes whether the actionable ref count grew | | `browser_fill` | Replace an input's value; selects match by option label | | `browser_press` | Press a key (Enter, Tab, Escape, ArrowDown, …) on an element | | `browser_scroll` | Scroll the page or bring a ref into view | | `browser_back` / `browser_forward` | History navigation | | `browser_wait` | Wait a bounded duration for lazy content | | `browser_tabs` / `browser_open_tab` / `browser_switch_tab` / `browser_close_tab` | Tab management; `browser_tabs` marks the `[active]` tab the other tools act on | | `browser_screenshot` | PNG capture stored as a durable image attachment the model can view | | `browser_extract` | Structured extraction: natural-language instruction → one parsed JSON value | **Only registered when enabled.** | `browser_evaluate` | Run one JavaScript expression in the page (disabled by default) | **Only registered when enabled.** | `browser_dialog` | Answer the native dialog blocking the page: `{ accept: true }` confirms, `false` cancels; errors with `NO_DIALOG` when nothing is pending | | `browser_click_at` | Precise native click: ref mode (geometric centre, Codex-style) or raw x/y mode; reports the actual landing element | | `browser_cdp` | Send one raw CDP command (DOM inspection / Input simulation allow-list only; disabled by default) | **Only registered when enabled.** | `browser_close` | Close the session's browser window; the profile keeps login state, so the next call reopens signed in | | `browser_console_messages` | Read the page console messages captured for the current page (levels, text, source) | | `browser_network_requests` | Read the network requests captured for the current page (method, URL, status, resource type) | > **使用铁律**: 状态检查 / 无效不盲试 / 页面内容不可信 / 登录红线 / 风控分类等 Codex 文档纪律, > 见 [`codex-rules.md`](codex-rules.md)(A 层已内嵌进各工具 description; B 层供粘贴进 DSH profile / 系统提示)。 ### Snapshot example ```text URL: https://example.com/ Title: Example Domain Refs: 12 - navigation "Main" - link "Home" [ref=e1] -> / - button "Go" [ref=e2] - heading "Welcome" [level=1] - textbox "Search" [ref=e3] ``` The model then calls `browser_click` with `ref: e2`. Invisible content is excluded, node and text caps bound the token cost, and truncation is flagged in the result. An actionable element with no text of its own is named from its descendants — `搜索设置` renders as `link "搜索设置" [ref=e7]`, and `图标链接` as `link "图标链接" [ref=e8]` — so a visible label and the ref that carries it always appear on the same line. A non-actionable child that merely repeats an ancestor name is folded away, so each label appears once, on the line that carries the ref; container roles are never named from their content (a `