dsh-builtin-browser 0.4.1 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1699 -0
- package/README.en.md +114 -33
- package/README.md +108 -32
- package/client.js +478 -29
- package/cordis.patch.yml +21 -0
- package/desktop-bridge/install.mjs +66 -6
- package/desktop-bridge/plugin-browser-bridge.js +426 -167
- package/docs/architecture.md +14 -4
- package/docs/tool-reference.md +13 -13
- package/docs/user-guide.md +58 -3
- package/docs/why-browser.md +1 -1
- package/lib/browser/types.d.ts +34 -3
- package/lib/browser-electron/bridge-connection.d.ts +49 -26
- package/lib/browser-electron/bridge-connection.js +185 -22
- package/lib/browser-electron/desktop-bridge-host.d.ts +69 -36
- package/lib/browser-electron/desktop-bridge-host.js +151 -86
- package/lib/browser-electron/entry.d.ts +1 -1
- package/lib/browser-electron/entry.js +133 -40
- package/lib/browser-electron/ephemeral-profile.d.ts +37 -0
- package/lib/browser-electron/ephemeral-profile.js +146 -0
- package/lib/browser-electron/history-store.d.ts +36 -2
- package/lib/browser-electron/history-store.js +76 -7
- package/lib/browser-electron/host-main.js +86 -28
- package/lib/browser-electron/provider.d.ts +106 -3
- package/lib/browser-electron/provider.js +407 -60
- package/lib/browser-electron/remote-host.d.ts +52 -3
- package/lib/browser-electron/remote-host.js +259 -25
- package/lib/browser-electron/rpc-ops.d.ts +18 -0
- package/lib/browser-electron/rpc-ops.js +36 -0
- package/lib/browser-electron/settings-route.d.ts +28 -0
- package/lib/browser-electron/settings-route.js +68 -0
- package/lib/browser-electron/settings-store.d.ts +40 -0
- package/lib/browser-electron/settings-store.js +160 -23
- package/lib/browser-electron/system-browser.d.ts +102 -12
- package/lib/browser-electron/system-browser.js +411 -51
- package/lib/index.js +0 -1
- package/lib/tool-browser/element-fields.d.ts +145 -0
- package/lib/tool-browser/element-fields.js +152 -0
- package/lib/tool-browser/index.js +149 -74
- package/package.json +10 -4
- package/scripts/verify-host-compat.mjs +195 -0
- package/src/browser/types.ts +34 -3
- package/src/browser-electron/bridge-connection.ts +179 -22
- package/src/browser-electron/desktop-bridge-host.ts +146 -84
- package/src/browser-electron/entry.ts +132 -39
- package/src/browser-electron/ephemeral-profile.ts +152 -0
- package/src/browser-electron/history-store.ts +81 -9
- package/src/browser-electron/host-main.ts +115 -36
- package/src/browser-electron/provider.ts +492 -69
- package/src/browser-electron/remote-host.ts +276 -38
- package/src/browser-electron/rpc-ops.ts +41 -0
- package/src/browser-electron/settings-route.ts +87 -0
- package/src/browser-electron/settings-store.ts +173 -24
- package/src/browser-electron/system-browser.ts +408 -39
- package/src/index.ts +3 -2
- package/src/tool-browser/element-fields.ts +167 -0
- package/src/tool-browser/index.ts +155 -76
package/README.en.md
CHANGED
|
@@ -93,7 +93,7 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
93
93
|
<tr>
|
|
94
94
|
<td width="50%" valign="top">
|
|
95
95
|
<h3>Multi-tab sessions</h3>
|
|
96
|
-
<p>Open URLs in parallel tabs; list/switch/close/reset tabs while each session keeps its own state. On the desktop <b>each session gets its own tab in the sidebar</b>, so ending one session does not disturb another.</p>
|
|
96
|
+
<p>Open URLs in parallel tabs; list/switch/close/reset tabs while each session keeps its own state. On the desktop <b>each session gets its own tab in the sidebar, and can open further tabs within it</b>, so ending one session does not disturb another.</p>
|
|
97
97
|
</td>
|
|
98
98
|
<td width="50%" valign="top">
|
|
99
99
|
<h3>Multi-format content</h3>
|
|
@@ -103,7 +103,7 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
103
103
|
<tr>
|
|
104
104
|
<td width="50%" valign="top">
|
|
105
105
|
<h3>Per-task session isolation</h3>
|
|
106
|
-
<p>Each DSH task (session)
|
|
106
|
+
<p>Each DSH task (session) drives <b>its own sidebar panel and its own page</b>: which conversation the shell happens to be displaying makes no difference, so a page requested in one conversation can never turn up in another's. Concurrent tasks never fight over the page or pollute each other (cookies, history and logins are shared across the session by design).</p>
|
|
107
107
|
</td>
|
|
108
108
|
<td width="50%" valign="top">
|
|
109
109
|
<h3>Login persistence</h3>
|
|
@@ -133,7 +133,7 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
133
133
|
<tr>
|
|
134
134
|
<td width="50%" valign="top">
|
|
135
135
|
<h3>Safety restriction</h3>
|
|
136
|
-
<p><code>browser_restrict</code> limits which browser actions are allowed (allow-list) to prevent stray clicks/navigation; read-only tools (snapshot / a11y / content / scrape / screenshot / get_value / challenge / list_tabs / session / history / visited
|
|
136
|
+
<p><code>browser_restrict</code> limits which browser actions are allowed (allow-list) to prevent stray clicks/navigation; read-only tools (snapshot / a11y / content / scrape / screenshot / get_value / wait / challenge / list_tabs / session / history / visited) plus the "lift the restriction / reset the session" tools are always exempt.</p>
|
|
137
137
|
</td>
|
|
138
138
|
<td width="50%" valign="top">
|
|
139
139
|
<h3>Screenshot, save and read</h3>
|
|
@@ -153,7 +153,7 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
153
153
|
<tr>
|
|
154
154
|
<td width="50%" valign="top">
|
|
155
155
|
<h3>A "Browser" section in Settings</h3>
|
|
156
|
-
<p><b>Which browser</b> (bundled Electron / installed Chrome / installed Edge / automatic), keep history, auto-expand the side panel, close the browser when a session ends, show the cursor, vision strategy, allow credential reads — every one of these is <b>read fresh on each use</b>, so a change takes effect immediately with no restart. <b>Two exceptions</b>: <code>browser.channel</code> (which browser) and <code>cookies.persist</code> (keep cookies) are read once when the plugin is applied, so changing either needs a <b>plugin reload</b> (restart dsh / refresh the page).</p>
|
|
156
|
+
<p><b>Which browser</b> (sidebar browser / bundled Electron / installed Chrome / installed Edge / automatic), keep history, auto-expand the side panel, close the browser when a session ends, show the cursor, vision strategy, allow credential reads — every one of these is <b>read fresh on each use</b>, so a change takes effect immediately with no restart. <b>Two exceptions</b>: <code>browser.channel</code> (which browser) and <code>cookies.persist</code> (keep cookies) are read once when the plugin is applied, so changing either needs a <b>plugin reload</b> (restart dsh / refresh the page). The section also carries a group of <b>"What the agent may do"</b> switches — <b>run scripts in the page</b>, <b>download files to disk</b>, <b>write login state</b>. They are not the same thing as <code>browser_restrict</code>: that one is the model's own soft guardrail and the model can lift it whenever it likes, while these live in the settings document, which <b>no tool can write</b> — the model can only be refused.</p>
|
|
157
157
|
</td>
|
|
158
158
|
<td width="50%" valign="top">
|
|
159
159
|
<h3>Unambiguous teardown</h3>
|
|
@@ -173,11 +173,11 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
173
173
|
|
|
174
174
|
| Tool | Purpose | Guard |
|
|
175
175
|
| --- | --- | --- |
|
|
176
|
-
| `browser_open` | Open a URL
|
|
176
|
+
| `browser_open` | Open a URL; returns a page snapshot. **A further call in the same session opens a new tab** (the first call opens the side panel automatically) | ✅ |
|
|
177
177
|
| `browser_wait` | Wait for page load (optional expected URL / CSS selector), returns readiness | – |
|
|
178
|
-
| `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links
|
|
179
|
-
| `browser_a11y` | Accessibility tree: semantic role/name/value/states
|
|
180
|
-
| `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]` | ✅ |
|
|
178
|
+
| `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links), each line carrying a `{#id}` / `{[name=x]}` selector when one is derivable (pierces same-origin iframes and Shadow DOM) | – |
|
|
179
|
+
| `browser_a11y` | Accessibility tree: semantic role/name/value/states per interactive node, plus a `{#id}` / `{[name=x]}` selector when derivable; coordinates only on request (`coords: true`, off by default, pierces same-origin iframes and Shadow DOM) | – |
|
|
180
|
+
| `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]`. Governed by the "Run scripts in the page" switch — off refuses it outright (`BROWSER_EXECUTE_DISABLED`) | ✅ |
|
|
181
181
|
| `browser_visited` | Read the persistent browsing history (visited pages, filterable by domain / capped); reopen with `browser_open` | – |
|
|
182
182
|
| `browser_content` | Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) | – |
|
|
183
183
|
| `browser_click` | Click a semantic target (`target`: css/text/xpath, scrolls into view and clicks center) or viewport coordinates (vision-located) | ✅ |
|
|
@@ -198,17 +198,17 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
198
198
|
| `browser_list_tabs` | List the session's tabs | – |
|
|
199
199
|
| `browser_switch_tab` | Switch to a tab by id (also switches the visible view when self-hosted) | ✅ |
|
|
200
200
|
| `browser_close_tab` | Close a tab by id; closing the active tab activates the next | ✅ |
|
|
201
|
-
| `browser_reset` | Close all tabs of this task, back to one blank tab |
|
|
201
|
+
| `browser_reset` | Close all tabs of this task, back to one blank tab | – exempt |
|
|
202
202
|
| `browser_session` | Show this task's browser session and tabs | – |
|
|
203
|
-
| `browser_reset_session` | Close and rebuild this task's browser session |
|
|
203
|
+
| `browser_reset_session` | Close and rebuild this task's browser session | – exempt |
|
|
204
204
|
| `browser_history` | Operation log (newest last), with per-step success/error and result summary | – |
|
|
205
|
-
| `browser_replay` | Replay one step by sequence number (navigate/
|
|
206
|
-
| `browser_download` | Download an HTTP(S) URL with session cookies to a local file (absolute `savePath` inside `downloadDir`, never overwrites, 256 MB cap) | ✅ |
|
|
207
|
-
| `browser_auth` | Export/restore cookies (login persistence; **works on all three carriers** — self-hosted reads its own session, the sidebar and an installed browser go through CDP) | ✅ |
|
|
205
|
+
| `browser_replay` | Replay one step by sequence number (navigate/click/type/scroll/key/execute) | ✅ |
|
|
206
|
+
| `browser_download` | Download an HTTP(S) URL with session cookies to a local file (absolute `savePath` inside `downloadDir`, never overwrites, 256 MB cap). Governed by the "Download files to disk" switch (`BROWSER_DOWNLOAD_DISABLED`) | ✅ |
|
|
207
|
+
| `browser_auth` | Export/restore cookies (login persistence; **works on all three carriers** — self-hosted reads its own session, the sidebar and an installed browser go through CDP). Export is governed by the Credentials switch (`BROWSER_AUTH_DISABLED`) and writing by "Write login state" (`BROWSER_AUTH_WRITE_DISABLED`); the two are independent | ✅ |
|
|
208
208
|
| `browser_challenge` | Detect a human-verification challenge (CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile) | – |
|
|
209
209
|
| `browser_restrict` | Restrict allowed browser actions (allow-list; empty list lifts it). **Soft guardrail** — the model can lift it itself; not a security boundary | – |
|
|
210
210
|
|
|
211
|
-
> "Guard" column: ✅ actions are governed by the `browser_restrict` allow-list; **rows marked "– exempt" are never blocked, whatever the allow-list says**, and **a plain "–" means the tool only observes — it performs no action, so there is nothing to restrict**. The exempt set is `READ_ONLY_TOOLS` (`src/tool-browser/index.ts`): `snapshot` / `a11y` / `content` / `scrape` / `screenshot` / `get_value` / `wait` / `challenge` / `list_tabs` / `session` / `history` / `visited
|
|
211
|
+
> "Guard" column: ✅ actions are governed by the `browser_restrict` allow-list; **rows marked "– exempt" are never blocked, whatever the allow-list says**, and **a plain "–" means the tool only observes — it performs no action, so there is nothing to restrict**. The exempt set is `READ_ONLY_TOOLS` (`src/tool-browser/index.ts`): `snapshot` / `a11y` / `content` / `scrape` / `screenshot` / `get_value` / `wait` / `challenge` / `list_tabs` / `session` / `history` / `visited`, plus `restrict` (the restriction itself must stay liftable, or a task that restricted everything could never get out), `reset_session` and `reset`. `browser_close_tab` is **not** exempt: like `open`/`click`/`switch_tab` it can be restricted; neither is `browser_auth`, whose `restore` writes cookies to arbitrary domains — an action, not an observation.
|
|
212
212
|
|
|
213
213
|
### Waiting for the page
|
|
214
214
|
|
|
@@ -217,31 +217,77 @@ See the full list in [Tool reference](#tool-reference).
|
|
|
217
217
|
|
|
218
218
|
### Semantic targets and the a11y tree
|
|
219
219
|
|
|
220
|
-
- **`browser_a11y` is the best way to understand a page**: every interactive node carries its semantic role (button/textbox/checkbox…), accessible name, current value
|
|
220
|
+
- **`browser_a11y` is the best way to understand a page**: every interactive node carries its semantic role (button/textbox/checkbox…), accessible name, current value and states (checked/expanded/required…) — it explains *what this is and what it does* better than a numbered snapshot. **150 nodes by default** (`maxNodes` 10-5000; ask for more explicitly), and **coordinates are no longer included by default** (pass `coords: true` when you actually need a pixel to click). Only the literal `enabled` is omitted from `states` (`unchecked`, `collapsed` and others are still printed) — no `states=` means the node is enabled. A node's `name` is exactly what `browser_click`/`browser_type` match when you target by `text`.
|
|
221
221
|
- **`browser_click`/`browser_type` accept a `target`**: `{by: css|text|xpath, value, index?}` — `text` matches an element's own visible text (exact first, then contains, deepest preferred); clicks scroll the element to the viewport center first; typing focuses it first.
|
|
222
222
|
- **Use the single-control tools for one field** (`browser_set_value`/`browser_check`/`browser_select`/`browser_clear`/`browser_get_value`), `browser_fill` for batches, and `browser_scrape` for structured list extraction.
|
|
223
223
|
|
|
224
|
-
### Operating discipline (
|
|
224
|
+
### Operating discipline (locating/clicking/filling)
|
|
225
225
|
|
|
226
|
+
- **Semantics first, coordinates last**: `browser_a11y` gives roles and accessible names, `browser_snapshot` gives refs and coordinates, and **both carry a selector** (see the next section) — **use it when it exists**, because scrolling and layout changes do not invalidate it.
|
|
227
|
+
- **`browser_click`'s `target` (css/text) IS semantic location**; use it instead of computing coordinates yourself. Scrolling into view, clicking the centre, iframe offsets and device pixel ratio are all handled for you.
|
|
226
228
|
- **Prefer DOM semantics over coordinates**: submit forms with `form.requestSubmit()`; click with `element.click()`; coordinate clicks are the last resort.
|
|
227
229
|
- **Target the right element**: pages often have hidden duplicates (e.g. mobile buttons); filter visible elements with `browser_execute` (`getBoundingClientRect()` w/h > 0, `getComputedStyle` not `display:none`), then take coordinates.
|
|
228
230
|
- **Click right after taking coordinates**: do not insert other operations in between (filling/scrolling moves elements and invalidates old coordinates).
|
|
229
231
|
- **Verify before clicking**: use `document.elementFromPoint(x, y)` to confirm the coordinate hits the intended element (button/link), then perform the real click.
|
|
230
232
|
- **DPR awareness**: CDP input uses CSS pixels; on high-DPI screens calibrate with `elementFromPoint` instead of guessing coordinates.
|
|
233
|
+
- **Two failures of the same action means change the approach**: a different selector, a semantic target, or a fresh snapshot — not the same coordinate a third time.
|
|
234
|
+
|
|
235
|
+
### Vision strategy: know which one you are in first
|
|
236
|
+
|
|
237
|
+
The **`vision.strategy`** setting decides whether coordinate clicks are usable, and **that is the only thing it affects**:
|
|
238
|
+
|
|
239
|
+
| Strategy | Coordinate clicks | Suited to |
|
|
240
|
+
| --- | --- | --- |
|
|
241
|
+
| `auto` (default) | allowed | **A model that can read images** — screenshots are a valid way to check layout |
|
|
242
|
+
| `nonVisual` | **refused** | **A model with no image input** |
|
|
243
|
+
|
|
244
|
+
**Under `nonVisual` a coordinate click is refused outright**, reported as `BROWSER_NON_VISUAL_COORDINATES`, and the error names the fix: pass a semantic target, e.g. `target { by: "text", value: "Sign in" }`, which locates the element from the DOM and clicks its centre. **That is a configuration, not a failure — do not retry the same coordinate.**
|
|
245
|
+
|
|
246
|
+
**DOM queries, the accessibility tree, text extraction and structured scraping are identical under both strategies** — that is, **the page can be operated completely without image input**. So the "when not to screenshot" advice below is the default for **every** model, not a concession to the ones that cannot see.
|
|
247
|
+
|
|
248
|
+
### When NOT to take a screenshot
|
|
249
|
+
|
|
250
|
+
Tool output is spent out of the caller's context, and **a screenshot is the most expensive kind**:
|
|
251
|
+
|
|
252
|
+
- **If text will do, use text.** `browser_snapshot` / `browser_a11y` / `browser_content` carry the same page as text; **a model without image input gains nothing from a screenshot** and only pays for it. Keep screenshots for what genuinely needs pixels: layout, charts, design checks.
|
|
253
|
+
- **A downscaled capture returns `width`/`height`** (the real pixel size). When you use `maxWidth`/`maxHeight` to control cost, read those to confirm it took effect rather than decoding the data URL yourself.
|
|
254
|
+
|
|
255
|
+
### The operator's three switches, which no tool can change
|
|
256
|
+
|
|
257
|
+
**Running page scripts**, **downloading to disk** and **writing login state** are controlled by the operator in the settings page, and **no `browser_*` tool can change them**. When one is off the error code is unambiguous — **do not retry and do not route around it**; say that the human needs to enable it:
|
|
258
|
+
|
|
259
|
+
`BROWSER_EXECUTE_DISABLED` (scripts), `BROWSER_DOWNLOAD_DISABLED` (downloads), `BROWSER_AUTH_WRITE_DISABLED` (writing login state), `BROWSER_AUTH_DISABLED` (reading cookies).
|
|
260
|
+
|
|
261
|
+
Do not confuse those with `BROWSER_DOWNLOAD_BLOCKED` / `BROWSER_SCREENSHOT_BLOCKED`, which mean **the save path was refused by the sandbox** (change the path, not a setting), or `BROWSER_DOWNLOAD_UNSUPPORTED`, which means **this carrier has no such capability**.
|
|
262
|
+
|
|
263
|
+
**`browser_restrict` is different** — it is a temporary allow-list the model sets and lifts itself, and **it is not a way around the three switches above**.
|
|
264
|
+
|
|
265
|
+
### Output size and truncation
|
|
266
|
+
|
|
267
|
+
Tool output is spent out of the caller's context, so several results have explicit bounds — and a truncation is always stated:
|
|
268
|
+
|
|
269
|
+
- **Snapshots and the a11y tree hand you a selector**: when an element has an `id` or a `name`, its line ends with `{#id}` / `{[name=x]}`, usable directly as `browser_click`/`browser_type`'s `target {by: "css", value: "#id"}` — no guessing at text from an accessible name. Elements with neither carry no selector segment: the tool does not invent one it cannot derive.
|
|
270
|
+
- **`browser_content` caps are per format (defaults)**: **html 50 000, json 50 000, txt 20 000, markdown 20 000 characters**; override per call with `maxChars`. When the result is truncated, the notice says what to do about it; `selector` narrows the fetch to one region first.
|
|
271
|
+
- **`browser_content` with `json` returns the element's markup**: `{"html": "<element outerHTML>", "tag": "<tag name>"}`, not an empty object — a DOM node has no own enumerable properties, so `JSON.stringify` on one yields `{}` for every element.
|
|
272
|
+
- **`browser_execute` caps its return value at 50 000 characters**, and states the cut in the result ("truncated at 50 000 of N") so a shortened value is never silently short.
|
|
273
|
+
- **`browser_scrape` returns compact JSON** (no indentation) with the item count in front — indentation carries nothing a model can use and measured over a tenth of the output.
|
|
274
|
+
- **`browser_history` shows the last 20 operations and omits each operation's parameters** (they are what you just sent), leading with the total so a tail is not mistaken for the whole; pass `verbose: true` when you actually want the parameters.
|
|
275
|
+
- **`browser_visited` timestamps are minute-granular**, and the session tag prints only when it changes, shortened to 8 characters — not a full session id per row.
|
|
231
276
|
|
|
232
277
|
## Configuration
|
|
233
278
|
|
|
234
|
-
The plugin mounts through `cordis.patch.yml` (**four rows**): one **inert root row** (it exists only to name the package, because the host's client-module scan
|
|
279
|
+
The plugin mounts through `cordis.patch.yml` (**four rows**): one **inert root row** (it exists only to name the package, because the host's client-module scan reads a row's `dsh.client` only when that row resolves to an **exact package name** — a row named after a subpath is skipped entirely, and then the settings panel would never appear) plus three functional rows (`browser` / `browser-electron` / `tool-browser`). Per-row config:
|
|
235
280
|
|
|
236
281
|
| Row | Key | Type | Default | Description |
|
|
237
282
|
| --- | --- | --- | --- | --- |
|
|
238
283
|
| `browser-electron` | `viewHost` | object | optional | `ElectronBrowserViewHost` instance supplied by the host (typically `!!js ctx.get('electronViewHost')`). **When it is absent the plugin picks the carrier itself** — the desktop sidebar if that is where it runs, otherwise self-hosting; a browser chosen in settings outranks both |
|
|
239
284
|
| `browser-electron` | `httpOnly` | boolean | `true` | Allow HTTP(S) navigation only; other protocols (e.g. `file:`/`data:`) rejected (`BROWSER_NAVIGATION_BLOCKED`) |
|
|
240
285
|
| `browser-electron` | `snapshotMaxElements` | number | `60` | Max snapshot elements before truncation |
|
|
241
|
-
| `browser-electron` | `contentMaxChars` | number |
|
|
286
|
+
| `browser-electron` | `contentMaxChars` | number | unset (per-format defaults) | The default content character cap. **Unset**, the caps are per format (html 50 000, json 50 000, txt 20 000, markdown 20 000); **set**, it is the operator's value and overrides them. A call's own `maxChars` outranks both |
|
|
242
287
|
| `browser-electron` | `downloadDir` | string | system Downloads folder (`Downloads`/`下载`/`下載`, or `XDG_DOWNLOAD_DIR`, auto-detected) | Confine `browser_download` AND `browser_screenshot` save paths to this directory, never overwriting an existing file (stops a prompt-injected agent writing or replacing arbitrary paths); override for a sandbox dir |
|
|
243
288
|
| `tool-browser` | `timeoutMs` | number | `60000` | Cooperative tool timeout (ms) |
|
|
244
289
|
| `tool-browser` | `tabTools` | boolean | `true` | Register tab-management tools (`browser_list_tabs` etc.) |
|
|
290
|
+
| `tool-browser` | `allowedActions` | string[] | unrestricted | **Deployment-level** action allow-list, applied to every task in the deployment (a task can still narrow it further with `browser_restrict`); `READ_ONLY_TOOLS` and `browser_restrict` are never blocked, whatever the list says. `cordis.patch.yml` ships an empty `config: {}` (i.e. unrestricted); its comments show how to tighten it |
|
|
245
291
|
|
|
246
292
|
## How it works
|
|
247
293
|
|
|
@@ -275,6 +321,10 @@ DSH Desktop is two layers: an Electron shell plus an `--expose-internals` **Node
|
|
|
275
321
|
|
|
276
322
|
The result: **the page the agent works on is the page the human looks at**. The plugin no longer spawns its own Electron and no second window appears.
|
|
277
323
|
|
|
324
|
+
**Which conversation a page lands in is decided by the conversation that asked, never by what is on screen.** The shell mounts one sidebar per conversation, and the plugin's client half opens that conversation's own panel **by session id** (DSH's `openTabIn`) without reading which conversation is displayed — so "ask in A, then switch to B" cannot deliver the page to B, and there is no path from one conversation's request to another's sidebar.
|
|
325
|
+
|
|
326
|
+
> **One behaviour inherited from the shell**: it renders only the **displayed** conversation's sidebar. When the asking conversation is not the one on screen, its panel is still created (the tab really does land in that conversation), but the page attaches only once that conversation is shown. In practice: after switching away, a further request from A reports "a panel was placed, but the page can only attach while the conversation is displayed" — a notice, not a cross-conversation write.
|
|
327
|
+
|
|
278
328
|
Install that bridge (it modifies an **installed** desktop app, so it is replayable):
|
|
279
329
|
|
|
280
330
|
```bash
|
|
@@ -292,7 +342,7 @@ node desktop-bridge/install.mjs --revert # roll back
|
|
|
292
342
|
> - the agent can read the **cookies and login state** in that partition, and `browser_auth` can export them (controlled by a setting);
|
|
293
343
|
> - your actions in the sidebar and the agent's actions act on **the same page** and can affect each other (the agent will not overwrite what you are typing, but navigation changes what you both see).
|
|
294
344
|
>
|
|
295
|
-
> That is the inherent cost of one shared page. We think it is worth it — it turns "the agent is doing something in a window you cannot see" into "you can watch it work and take over" — but you are entitled to know it exists, so there are switches: with **credential access** off, `browser_auth` **refuses
|
|
345
|
+
> That is the inherent cost of one shared page. We think it is worth it — it turns "the agent is doing something in a window you cannot see" into "you can watch it work and take over" — but you are entitled to know it exists, so there are switches: with **credential access** off, `browser_auth` **refuses the export** (`BROWSER_AUTH_DISABLED`) — it governs reading only, while writing login state (restore) answers to its own switch and reports `BROWSER_AUTH_WRITE_DISABLED`; and with the **vision strategy** set to non-visual any coordinate click that depends on a screenshot is refused. If you would rather not accept the boundary change at all, removing the plugin from the desktop profile returns you to the old separate-window shape.
|
|
296
346
|
|
|
297
347
|
**② Use the browser you already have (Chrome / Edge)**
|
|
298
348
|
|
|
@@ -300,10 +350,12 @@ The settings panel can point the plugin at an **installed Chrome or Edge** (`bro
|
|
|
300
350
|
|
|
301
351
|
**Your data is not touched**: the plugin launches it with a **separate profile** (`$DSH_HOME/dsh-builtin-browser-host/<chrome|edge>-profile`). Your everyday windows, bookmarks and logins are never opened, locked or modified, and closing the plugin never closes your browser.
|
|
302
352
|
|
|
353
|
+
**It comes back usable after it dies**: this carrier watches its child's exit and drops the **stale session mappings** along with the cached connection, so the restarted browser can actually be driven instead of answering every command with "Session not found". Before relaunching it kills the old process and **waits up to 3 seconds** for it to really exit (on Windows `kill` is asynchronous, and spawning immediately let the replacement hand its command line to the old instance and exit — which was then reported as "check that the path is a runnable browser"), and it **deletes the previous launch's `DevToolsActivePort` and trusts only a file written after this spawn** (otherwise it connects to whatever else holds that port — another Chromium, or a `node --inspect` — and publishes a client it does not own and can never kill). Conversely, **a slow page is not a dead browser**: a command **timeout** only marks the connection suspect, and the next call probes it once with `Browser.getVersion` before deciding to drop it; only a **closed socket** restarts the browser. One slow page therefore cannot make the plugin kill the window you are using, tabs and half-filled forms included.
|
|
354
|
+
|
|
303
355
|
**What happens to login state**:
|
|
304
356
|
|
|
305
357
|
- `cookies.persist` **on** (default) → that fixed profile is kept, so **you stay signed in across DSH restarts**, and `browser_auth` can still export/restore its cookies.
|
|
306
|
-
- `cookies.persist` **off** → a **throwaway profile** each time, deleted when the browser is released; no login trace is left behind.
|
|
358
|
+
- `cookies.persist` **off** → a **throwaway profile** each time, deleted when the browser is released; no login trace is left behind. A process killed outright never reaches that release path, so the next start **sweeps** the throwaway profiles an earlier run left behind — only those whose owner is gone AND which have not been written to for an hour, so the profile an orphaned browser is still working in is left alone.
|
|
307
359
|
- The trade-off, stated plainly: a separate profile **does not see** the sites you are signed into in your everyday browser. Sign in once in the window the plugin opens and the session stays in its own profile.
|
|
308
360
|
|
|
309
361
|
**③ A shell that provides `electronViewHost`** (older desktop shells): that view is used directly.
|
|
@@ -317,24 +369,39 @@ The settings panel can point the plugin at an **installed Chrome or Edge** (`bro
|
|
|
317
369
|
## Requirements
|
|
318
370
|
|
|
319
371
|
- DeepSeek Harness (dsh) with the matching profile (`web` / `desktop`, etc.)
|
|
320
|
-
- **Electron runtime** (required dependency, installed automatically with the plugin, ≥ 40 recommended; the 44+ binary is not downloaded at install time — if missing, follow the error and run `npx install-electron` first, needs network)
|
|
372
|
+
- **Electron runtime** (required dependency, installed automatically with the plugin, ≥ 40 recommended; the 44+ binary is not downloaded at install time — if missing, follow the error and run `npx install-electron` first, needs network) — **only the self-hosted carrier needs it**:
|
|
321
373
|
- `ELECTRON_PATH` can point at another binary explicitly (highest priority);
|
|
322
|
-
- **DSH Desktop**: the packaged host exe (`DSH Desktop.exe`) is **never reused** — packaged apps cannot be spawned with a script argument and misuse exits instantly (issue #6);
|
|
374
|
+
- **DSH Desktop**: the official sidebar by default, so **no Electron is needed**; the bundled electron is used only when it falls back to self-hosting. The packaged host exe (`DSH Desktop.exe`) is **never reused** — packaged apps cannot be spawned with a script argument and misuse exits instantly (issue #6); dev-mode **bare** Electron hosts are still reusable;
|
|
375
|
+
- **an installed Chrome / Edge needs no Electron either**;
|
|
323
376
|
- **plain `dsh web` self-hosted**: uses the bundled electron package directly
|
|
324
377
|
|
|
325
378
|
### Verified versions
|
|
326
379
|
|
|
327
380
|
| Component | Version |
|
|
328
381
|
| --- | --- |
|
|
329
|
-
| DeepSeek Harness (dsh) | `0.2.
|
|
382
|
+
| DeepSeek Harness (dsh) | `0.2.1-alpha.1` (peer range `>=0.1.1-rc.2 <0.3.0`; `0.2.0-rc.2` and `0.1.x` were verified earlier) |
|
|
330
383
|
| Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
|
|
331
384
|
| Node.js | `22.20.0` |
|
|
332
385
|
| Installed Chrome / Edge (optional carriers) | `154.0.8037.58` / `154.0.4258.37` |
|
|
333
|
-
| dsh-builtin-browser | `0.4.
|
|
386
|
+
| dsh-builtin-browser | `0.4.4` (unreleased, on the development branch) |
|
|
334
387
|
| OS | Windows 10 (10.0.26200) |
|
|
335
388
|
|
|
336
389
|
> The plugin declares `electron >= 30`. The **core path is fully verified on Windows**; system-browser detection is now adapted for Linux and macOS (PATH first, then each platform's conventional install locations, all overridable with `DSH_BROWSER_CHROME_PATH` / `DSH_BROWSER_EDGE_PATH`), but the end-to-end path on those platforms has not been measured, so no promise is made yet.
|
|
337
390
|
|
|
391
|
+
> **Re-verify host compatibility at any time**: `node scripts/verify-host-compat.mjs` assembles a host out
|
|
392
|
+
> of the `@deepseek-ai/*` packages **actually installed** in a profile, loads this plugin's compiled
|
|
393
|
+
> output, registers its tools through the host's own `defineTool`, and calls two of them over a fake
|
|
394
|
+
> provider (`browser_session` / `browser_a11y`). It answers "did a DSH upgrade break us?" without starting
|
|
395
|
+
> a browser. It reads the profiles under `$DSH_HOME/profiles`; `DSH_PROFILE` / `DSH_HOME` override that.
|
|
396
|
+
>
|
|
397
|
+
> **On semver and prereleases**: the range `>=0.1.1-rc.2 <0.3.0` matches no prerelease-tagged version
|
|
398
|
+
> (such as `0.2.1-alpha.1`) under **default semver semantics** — that is by specification, where a
|
|
399
|
+
> prerelease is only admitted by a comparator carrying the same major.minor.patch tuple. DSH evaluates
|
|
400
|
+
> plugin compatibility with `includePrerelease: true`
|
|
401
|
+
> (`packages/boot/app-boot/src/plugin-compatibility.ts`), so those hosts are in range in practice; if
|
|
402
|
+
> some external tool reads "incompatible" under default semantics, that is its rule differing from DSH's,
|
|
403
|
+
> not a wrong claim here.
|
|
404
|
+
|
|
338
405
|
## Updating (the two hosts differ)
|
|
339
406
|
|
|
340
407
|
The plugin has one installation per host, and the two are updated separately — **updating one does not update the other**.
|
|
@@ -369,11 +436,12 @@ The plugin has one installation per host, and the two are updated separately —
|
|
|
369
436
|
- `fullPage` capture is flaky under software compositing on some hosts — with `fullPage` the native `capturePage` path is **skipped entirely** (`capturePage` cannot reach content beyond the viewport), so **all three carriers go through CDP `captureBeyondViewport`** and the flakiness is not carrier-specific: any carrier can hit it. Only viewport captures prefer the native `capturePage`.
|
|
370
437
|
- CAPTCHA cannot be solved automatically: snapshots flag detected challenges; ask the human to complete it in the shared window instead of retrying.
|
|
371
438
|
- Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
|
|
372
|
-
-
|
|
439
|
+
- A settings document that **exists but cannot be read** (corrupt JSON, a half-written file, unreadable permissions) resolves the four **gate** switches (`credentials.allowRead` plus the three action switches) to **off**, with every other setting at its default — "the operator's intent cannot be read" is not "the operator permits everything". A file that is simply **absent** (first run) still takes the defaults. A gate field of the wrong type (`"false"` as a string, `null`, a number) reads as **off** as well, and so does a **gate-holding section** written as a non-object (`actions: false`, `null`, a stray string): such a section is read as unreadable rather than as absent, so every gate inside it is **off** instead of taking its permissive default.
|
|
440
|
+
- `browser_download` fetches in the page context (keeps logins) and is subject to same-origin/CORS constraints; HTTP(S) targets only; `savePath` must be absolute and inside `downloadDir` (default: the system Downloads folder, auto-detecting `Downloads`/`下载`/`下載` and `XDG_DOWNLOAD_DIR`; override with `downloadDir`) and never replaces an existing file; `browser_screenshot`'s `savePath` goes through the same gate; single files are capped at 256 MB (streamed with a Content-Length early reject) and are written by the browser child itself (temp file + atomic rename). Admission also resolves where a path really lands: a symlink inside `downloadDir` cannot carry the write out of it, and a dangling link is refused too (it reads as a name already taken, not as a free path).
|
|
373
441
|
- The self-hosted browser's cookies are stored in plaintext on disk (Electron default); deployments that need encrypted-at-rest should integrate a system keychain / DPAPI at the host layer.
|
|
374
|
-
- `browser_restrict` is a **soft guardrail** against accidental actions, not a security boundary: the model can lift it itself.
|
|
442
|
+
- `browser_restrict` is a **soft guardrail** against accidental actions, not a security boundary: the model can lift it itself. For a limit the model cannot remove with a tool, use the three action switches in Settings ("What the agent may do") — they live in the settings document, which no `browser_*` tool writes — or the deployment-level `tool-browser` config `allowedActions` (see the comments in `cordis.patch.yml`). **Be precise about that boundary**: settings travel over a local HTTP endpoint whose writes require a same-origin `Origin` (so a bare HTTP client cannot write them), but a **same-origin page still can** — that is inherent to exposing an endpoint at all, and it is **not a credential system**. The panel's own text says exactly this, and it is worth reading as what it is rather than as a lock.
|
|
375
443
|
- Popups (`window.open` / `target=_blank`) no longer overwrite the current view: HTTP(S) popups open as a **new tab** in the same session window, recorded in the session history, keeping the original page and its opener context alive. Non-HTTP(S) popups (empty-URL popup handoffs, `mailto:`, custom schemes) are still **allowed as native windows** and handed to the system — such windows are simply not part of the session model.
|
|
376
|
-
- The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies). It **works on all three carriers**: self-hosted reads its own session, while the sidebar and an installed Chrome/Edge go through CDP's `
|
|
444
|
+
- The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies). It **works on all three carriers**: self-hosted reads its own session, while the sidebar and an installed Chrome/Edge go through CDP's `Network.getCookies` / `Storage.setCookies`. **`flush` exports only the cookies of the site the current page is on**, not the machine-wide shared jar — the export is scoped to the URL of the page this session is sitting on, and when that URL is unknown the tool **refuses rather than widening** (the self-hosted sidebar path reports that the cookie scope is unknown; the CDP path raises `BROWSER_AUTH_SCOPE_UNKNOWN`). `browser_auth` **is subject to the `browser_restrict` allow-list** too, because `restore` is an action that writes cookies to arbitrary domains.
|
|
377
445
|
- After a self-hosted child crash (or a DSH restart that kills it) the browser host restarts automatically, and sessions opened before the crash **rebuild on their next use** — only page state is lost, no manual `browser_reset_session` needed (it still works for an explicit reset). A new view preloads `about:blank` (bounded 3 s) before creation so it always has a live renderer, and host-side commands are bounded at 20 s. The child's stderr plus exit code/signal go to `$DSH_HOME/logs/dsh-builtin-browser-host.log`; before appending, a file already over 2 MiB is **truncated: the old contents are discarded and replaced by a single timestamped rotation line** (`<ISO timestamp> log rotated: previous content exceeded 2097152 bytes and was discarded`). Nothing is kept from before the rotation — but that line records that it happened, so a plain `dsh web` self-hosted setup can still diagnose a crash loop itself.
|
|
378
446
|
- The electron package ships with the plugin, but Electron 44+ no longer downloads its binary at install time (~100 MB, needs network) — the probe is filesystem-only and never triggers its lazy download, so a missing binary surfaces as a clear error on first use telling you to run `npx install-electron` first; alternatively pre-install a binary and point `ELECTRON_PATH` at it.
|
|
379
447
|
- This plugin draws **no browser interface of its own** (no address bar, no tab strip, no side panel): on the desktop that interface is the **shell's own official sidebar**, and the plugin merely drives the page inside it. On the self-hosted carrier the window is drawn by the Electron child the plugin spawns — that is the carrier, not plugin UI. Do not treat "the sidebar" or "browser column" as a plugin feature.
|
|
@@ -394,9 +462,11 @@ Code layout:
|
|
|
394
462
|
| Directory | Responsibility |
|
|
395
463
|
| --- | --- |
|
|
396
464
|
| `src/browser/` | The `ctx.browser` seam and all request/result types |
|
|
397
|
-
| `src/browser-electron/` |
|
|
398
|
-
| `src/tool-browser/` | Model-facing `browser_*` tools |
|
|
465
|
+
| `src/browser-electron/` | The provider and its three carriers — the desktop sidebar bridge (`desktop-bridge-host.ts`), an installed browser (`system-browser.ts`), the self-hosted child (`host-main.ts`); plus the transport (`bridge-connection.ts`) and the RPC operation vocabulary (`rpc-ops.ts`), settings, history and the synthetic cursor |
|
|
466
|
+
| `src/tool-browser/` | Model-facing `browser_*` tools; the element/node field set (`element-fields.ts`), where the item schema and the projection come from one declaration |
|
|
399
467
|
| `src/types/` | Electron ambient types (shim; no hard electron type dependency) |
|
|
468
|
+
| `desktop-bridge/` | The bridge installed into the desktop app, with its idempotent installer (`install.mjs`) |
|
|
469
|
+
| `tools/` | Operational scripts (e.g. `install-web-plugin.mjs`: pin → install → repair the profile → verify) |
|
|
400
470
|
|
|
401
471
|
## Update history
|
|
402
472
|
|
|
@@ -421,10 +491,10 @@ Code layout:
|
|
|
421
491
|
| 11 | 2026-08-27 | **Self-hosted session self-healing after host death** (issue #5): after the host dies (DSH restart / checkpoint restore / crash), already-open sessions rebuild the host and retry on their **next call** — no more "browser host is not running", no more half-dead state; host-gone logging + a fake-child regression test added |
|
|
422
492
|
| 12 | 2026-08-27 | **resolveElectronPath excludes packaged apps** (issue #6): added `isBareElectron` (a sibling `app.asar` means packaged — never reused, all platforms incl. macOS bundle layout); bundled-electron filesystem probe goes first; `ELECTRON_PATH` override wins first; missing dist errors clearly (`npx install-electron` hint) |
|
|
423
493
|
| Hardening | 2026-08-27 | **Three review passes hardened**: concurrent-recovery double-rebuild race (child `createView` made idempotent), macOS bundle-path detection, `dispose()` vs `start()` zombie-child race triple-guard, pendingSocket leak, fully unit-tested lookup order (24→25 tests) |
|
|
424
|
-
| **0.1.20** | 2026-08-
|
|
494
|
+
| **0.1.20** | 2026-08-28 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
|
|
425
495
|
| 13 | 2026-08-28 | **macOS/Linux untypeable inputs fix** (issue #7): the Windows focus routing from 0.1.16 (mousedown force-focus + window-refocus restore) shipped without a platform guard and fought macOS native click-to-focus, leaving login inputs untypeable → both handlers are now gated to `win32`; non-Windows restores native behavior |
|
|
426
496
|
| 14 | 2026-08-28 | **window.open/target=_blank opens a new tab** (issue #8): HTTP(S) popups no longer loadURL over the current view; they are handed to the parent and open as a **new tab in the same session window** — the opener page and its context survive (portal "workspace" jumps no longer 403) and the jump lands in session history; ungrouped views keep the fallback; non-HTTP popups still go to the system |
|
|
427
|
-
| **0.1.21** | 2026-
|
|
497
|
+
| **0.1.21** | 2026-09-02 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
|
|
428
498
|
| macOS binary probe | 2026-09-09 | **Electron.app layout detection** (issues #9 / #14): `electronDistExe()` only probed `dist/electron(.exe)`, so macOS's `dist/Electron.app/Contents/MacOS/Electron` was never found → darwin candidate added to the shared platform probe (bundled + profile/anchor layers both benefit); regression test added |
|
|
429
499
|
| 15 | 2026-09-16 | **Toolbar parse-time SyntaxError** (issue #11): the inline script's `const bridge = window.bridge` collided with the non-configurable global installed by `contextBridge.exposeInMainWorld` (`HasRestrictedGlobalProperty`) → a **parse-time** early error, so not one line ran and the address bar, the four nav buttons, the tab strip and the error bar were all dead → the whole script is wrapped in an IIFE and the handle renamed `tb`, making the class of collision structurally impossible; 3 toolbar regression tests parse the snippets out of the **published artifact** and execute them in a `vm` under contextBridge semantics |
|
|
430
500
|
| 16 | 2026-09-16 | **Self-hosted trio fixes** (issue #10): (1) `browser_open` waits (bounded 5 s) for the new document to settle via a `performance.timeOrigin` fingerprint + `readyState`, so it no longer returns a titled-but-empty snapshot; (2) a new waiting `presentView` barrier (materialize the view, then showView, then a ping barrier; child dispatch is strictly serial) is required before `click`/`type`/`key` dispatch `Input.*`, which now fail loudly with `BROWSER_VIEW_NOT_PRESENTED` instead of faking success; (3) `createView` preloads `about:blank` (bounded 3 s) so a fresh view always has a live renderer (the step that wedged after a host restart); (4) `did-navigate` forces re-presentation, host commands are bounded at 20 s, child stderr plus exit code/signal go to `$DSH_HOME/logs/dsh-builtin-browser-host.log` (2 MB self-truncating), and `locateTab` accepts a bare uuid as well as `tab:<uuid>` |
|
|
@@ -447,8 +517,19 @@ Code layout:
|
|
|
447
517
|
| **0.3.1** | 2026-10-01 | **Release**: round 27 (lazy startup + refusing to start after release) ships as **0.3.1**. **100/100 tests pass**, tag `v0.3.1` |
|
|
448
518
|
| Round 28 | 2026-10-01 | **Two "limits" were implementation limits**: reviewing the documented limitations showed they were never about the carriers. ① `browser_auth` called a native method only the self-hosted handle implements and refused otherwise — but **cookies are part of CDP** (`Storage.getCookies` / `Storage.setCookies`) and **all three carriers speak CDP**, so export and restore now go through it when no native method exists, **working on all three** (domain+path vs URL, the leading-dot spelling and seconds vs milliseconds are handled, and cookies that cannot form a usable URL are dropped). ② JPEG was denied to every CDP path because Electron's CDP JPEG encoder hangs — true for Electron, irrelevant to an installed Chrome/Edge — so a view now advertises `supportsCdpJpeg` and only then is the format passed through; the sidebar keeps PNG. ③ The tool description promised downscaling that existed only on the native path, so the CDP path now scales through `clip.scale` (reading `Page.getLayoutMetrics` first, and capturing unscaled rather than failing when the size is unavailable). ④ Two carrier differences that were never documented are now written down: **the sidebar does not report user-action events**, and **an installed Chrome/Edge uses a plugin-owned profile**. 11 new tests over the pure parts, **124/124** |
|
|
449
519
|
| Round 29 | 2026-10-01 | **Independent code review**: a separate reviewer read the whole tree and reproduced the findings. ① **`spawn` had no `error` listener** — a launch failure arrives as an asynchronous event, and without a listener Node takes **the entire DSH host process down**; it now fails the command instead. ② **`dispose()` during `start()`'s 250 ms poll** left the launched browser alive with nobody to kill it; the poll now kills it before stopping. ③ Three different failures all reported "did not expose CDP within 30s", in half a second; each now says what happened. ④ **An explicit `chrome`/`edge` choice was still overridden by the desktop sidebar**, because discovery never checked the channel — which also meant the "your browser is not installed" explanation could never appear, cancelling the previous round's fix. ⑤ `CdpClient.whenReady()` awaited an unbounded promise, so a dropped connection could hang a tool call forever. ⑥ `focus()` used `kill('SIGCONT')`, a no-op on Windows, while its comment promised to raise the window. ⑦ Removed an unreachable `brave` branch. **125/125** |
|
|
450
|
-
|
|
451
|
-
|
|
520
|
+
| Round 30 | 2026-10-02 | **The second independent review's 4 HIGH + 2 MEDIUM**: ① **read-only tools were in fact blocked by the allow-list** — `assertAllowed` exempted only `browser_restrict` itself, so a narrow restriction also locked away `browser_history` / `browser_reset_session` / `browser_auth` with no way back → an explicit read-only set (plus the tools that lift a restriction) now exists. ② **A cancelled navigation completed as success** — `navigate()` checked the signal once at the top while `settleDocument` returned quietly on abort, so the call finished with no exception, was recorded as success, and wrote the page into the persistent browsing history. ③ **The settings switch nothing read** — `credentials.allowRead` existed only in the settings store and the panel, so turning it off still let `browser_auth` export every cookie; both auth paths are genuinely gated now. ④ **A stale tab id could close someone else's tab** — `locateTab` searched every other session when the id was not local and reported success, so isolation was broken; it is confined to the calling session now (a bare uuid and `tab:<uuid>` are still both accepted). ⑤ With an empty `target.value`, `browser_type` fell back to the focused element (possibly a password field) and `browser_click` dropped the semantic target for coordinates, both reporting success → an empty value is an error. ⑥ `scrape`'s outer budget equalled the page-side deadline, so a page-side "no elements matched" verdict could never travel back → the outer call now leaves room for the transfer. **130/130** |
|
|
521
|
+
| **0.4.0** | 2026-10-02 | **Release**: rounds 27–31 ship together — the theme is **separating real limits from implementation limits**: `browser_auth` works on all three carriers, an installed Chrome/Edge can encode JPEG, and downscaling works everywhere; alongside fixes for defects that crashed the host, silently did the wrong thing, or hung forever. **130/130 tests pass**, tag `v0.4.0` |
|
|
522
|
+
| Round 31 | 2026-10-02 | **Third review round + atomic settings writes**: ① **settings writes go through a temporary file and a same-directory rename** (the rename is the commit point, so a crash or a full disk cannot leave half a JSON document that the reader then silently replaces with defaults), and a failed write cleans up only the temporary and **no longer publishes the new value as cached**; ② fixed an `el` never rebound after a filter (a selector matching several elements wrote the value onto the first one while reporting success); ③ `handle.focus()` got a 5 s budget (the only unbounded await in the file, and it runs before typing); ④ `browser_wait`'s poll leaked an abort listener every 200 ms (~150 for a 30 s wait) and now reuses `delay()`; ⑤ with `autoExpandOnce` off, `showActive` treated "show the view" and "fold the carrier" as exclusive branches and returned after the second — on carriers with no collapse capability the view was **never shown at all**, so the agent drove a view the human could not see and every screenshot failed with "view not painted" (an error that blamed the page for a setting); ⑥ assorted `bridge-connection` / `scrape` timeout clean-up. **130/130** |
|
|
523
|
+
| **0.4.1** | 2026-10-02 | **Release**: issue #21 — **the system browser carrier (chrome/edge) neither reconnected nor restarted after its process died**, so every `browser_*` call hung for 30 seconds and only restarting DSH helped. Launch-time failures were handled in the previous round, but death **after a successful launch** was never observed: there was no `exit` listener, `this.client` was never cleared once assigned, and `ensureClient()` kept returning that dead connection forever. Fixed along the two failure shapes reported: process death → the child is observed throughout, its exit clears the cache and the next call relaunches; socket death with the process alive → the cached client's liveness is checked before use; process and socket both alive but the command never answered → **a failed command drops the client**. Commands sent on an already-closed socket now **fail immediately and say why**, instead of waiting out the budget and reporting a "timeout" that points nowhere near the real cause. **132/132 tests pass**, tag `v0.4.1` |
|
|
524
|
+
| Round 32 | 2026-10-03 | **Output-cost audit + independent verification** (shipped in **0.4.2**): ① **`browser_a11y`**: the default node count went **500 → 150**, indentation two spaces → one, `states=[…]` prints omitting the literal `enabled` (unchecked, collapsed and others are still printed) (**no `states=` means enabled**) and the trailing hint is short; ② **snapshots and the a11y tree now hand you a citable selector**: `{#id}` / `{[name=x]}` — the page-side script had always computed it and the tool layer threw it away, while the targeting tools accept only css/text/xpath, leaving the model to guess at text or fall back to a screenshot — and the field is **declared in the output schema**, without which `additionalProperties: false` rejects the whole result, which is exactly how the gap was found on the first real call; ③ **`browser_content` caps are per format** (html/json 50 000, the rest 20 000, overridable with `maxChars`), **`json` no longer returns `{}`** (a DOM node serialises to nothing) but `{"html":…,"tag":…}`, and the truncation notice says how to narrow; ④ `browser_scrape` returns compact JSON; ⑤ `browser_execute`'s return value is capped at 50 000 characters with the cut stated; ⑥ `browser_history` shows the last 20 entries without parameters and leads with the total — `verbose: true` brings the parameters (the parameter had been declared and never read); ⑦ `browser_visited` timestamps are minute-granular and the session tag prints once per change, shortened to 8 characters; ⑧ **`browser_auth`'s `flush` exports only the cookies of the site the current page is on** (it used to be the machine-wide shared jar) and **refuses rather than widening** when the scope is unknown, while `restore`'s count now reflects what was really submitted (`Storage.setCookies` returns nothing, so it was always 0 and read as failure) and **`browser_auth` left the read-only set** — its `restore` writes cookies to arbitrary domains, an action rather than an observation; ⑨ **atomic settings writes**, a settings cache that returns before reading the file, a browsing history that no longer rewrites the whole file on every navigation (648 KB per navigation once past the cap, measured at 17.5–20.6 ms), the `browser_content` page-script syntax error that broke all four formats, `browser_visited` printing blank rows, bridge replies correlated by request id, and `tools/install-web-plugin.mjs` refusing to damage a profile it cannot repair. Two unbounded host awaits (self-hosted capture/download, and cookie export/restore) got timeouts. **132 → 134** |
|
|
525
|
+
| Round 33 | 2026-10-03 | **The system browser carrier: actually usable after it dies** (shipped in **0.4.2**): ① **one failed command no longer tears down the connection** — a CDP protocol error means the browser answered and refused that one command, so the connection is healthy; ② **a restart now drops the stale session mappings**, not just the cached connection and child, because every command the replacement received carried a session id issued by the dead process — that is the reported "it restarts, and every call fails forever"; ③ **a slow page is not a dead browser** — deciding liveness by error text turned the 5 s/15 s outer timeouts into "the browser is dead" and killed the window you were using, tabs and half-filled forms with it; the state is now three-way — socket closed → discard, answered and refused → keep, **no answer → mark suspect** and probe once with `Browser.getVersion` on the next call; ④ **after the kill it waits up to 3 seconds for a real exit** (on Windows the kill is asynchronous, and not waiting handed the new process's command line to the old instance, whose resulting exit was misreported as "check that the path is a runnable browser"); ⑤ **only the launch's own `DevToolsActivePort` counts** (a killed browser never cleans it up, and the stale file made the host connect to another Chromium or a `node --inspect`); ⑥ **the port-discovery regression test now uses a fixture that really speaks CDP** (the old `.cmd` stub was refused by Node's spawn on Windows, and all three assertions were satisfied by that single synchronous failure — CI was green while the exit handling never ran once). **134/134** |
|
|
526
|
+
| Round 34 | 2026-10-03 | **Operator-level action switches (the real entry point)** (shipped in **0.4.2**): the settings panel gains "What the agent may do" — **run scripts in the page** / **download files to disk** / **write login state**. Switching one off refuses that capability outright (`BROWSER_EXECUTE_DISABLED` / `BROWSER_DOWNLOAD_DISABLED` / `BROWSER_AUTH_WRITE_DISABLED`). What separates them from `browser_restrict` is **who can change them**: that one is set by the model and can be lifted by the model, while these live in the settings document, which no tool can write. Also fixes a read/write mix-up: restoring login state (`browser_auth restore`) answered to the READ switch and reported "reading cookies is switched off", so an operator who allowed reading had no way to refuse writes — it now answers to the write switch. **148 → 157** |
|
|
527
|
+
| Round 35 | 2026-10-03 | **DSH 0.2.1 adaption** (shipped in **0.4.2**): the host moved to `0.2.1-alpha.1` (266 commits past rc.2), so every host-facing dependency was checked item by item — cordis `Context`/`Service` (vendor 4.0.5-alpha.1), `dsh-tools` `defineTool` and every `DefineToolOptions` field, `dsh-system-prompt` `section()`, `dsh-llm` `HarnessError`, the schemastery default export, and on the client side `slots.inject('settings.section')` with `locale.register`/`bind`. **None of the shapes changed** (the client calls match DSH's own plugin word for word). Added `scripts/verify-host-compat.mjs`: it assembles a host from the `@deepseek-ai/*` packages **actually installed** in a profile, loads this build, registers 34 tools through the host's own `defineTool`, and calls `browser_session` plus `browser_a11y` over a fake provider — **all green**. `dsh.compatibility.dshReleases` now records `0.2.1-alpha.1 = compatible`. One semantic trap recorded too: that range matches no prerelease under **default semver semantics**, while DSH evaluates with `includePrerelease: true`, so these hosts are in range in practice |
|
|
528
|
+
| Round 36 | 2026-10-04 | **An external audit, worked through** (shipped in **0.4.2**): eight findings with line numbers and evidence, each verified against the code and fixed in three batches — (1) **storage and settings**: a settings document that EXISTS but cannot be parsed no longer resolves to "everything on" (a torn write used to reopen every capability the operator had switched off), a gate field of the wrong type now reads as OFF, `history.jsonl` uses the settings store's atomic write for its whole-file rewrite, and `contentMaxChars` — assigned but never read since the caps became per-format — works again; (2) **lifecycle**: the browser child gets the socket close first and time to `app.quit()` its profile out, and only then is killed (the old comment claiming `app.exit(0)` "flushes the profile" was false, and on Windows `kill()` is TerminateProcess, which lands before any handler runs), and the child no longer calls `app.exit(0)` either; (3) **usability**: click/type/fill resolved targets against the top document only while the snapshot and `waitFor` walked shadow roots and iframes — **visible, waitable, unclickable** — so the resolve scripts now collect every root; on Windows the per-user `%LOCALAPPDATA%` install locations are searched, and the "exited immediately (exit code 0)" message no longer points at a broken path when it means the browser handed the request to an instance already holding its profile. Two places that silently wreck someone else's work are fixed as well: `install-web-plugin.mjs` refuses to overwrite a `link:` pin with a registry version, and a failed adopt disposes its half-built host. **Two findings I do not agree with**: a zero-hit grep for `window-all-closed`/`before-quit` says nothing about behaviour (Electron's default is already right), and a `requestSingleInstanceLock` would break normal restarts rather than fix orphans. |
|
|
529
|
+
| **0.4.2** | 2026-10-04 | **Release**: rounds 32-36 in one release — the theme is **making invisible errors visible**. (1) **Output cost**: what every call pays dropped to a fraction (a11y defaults to **500 → 150** nodes, indentation and `states` carry only information, `content` caps are per format, `history` shows 20 entries by default). (2) **Action switches**: three capability gates held by the **operator** that no `browser_*` tool can change (run page scripts / download to disk / write login state), kept clearly separate from `browser_restrict`, which the model sets and lifts itself. (3) **Recovery semantics completed**: a dead system-browser process now takes its sessions down with it (previously the browser could restart while the view still held the dead one's session id — "fails fast and keeps failing"), a slow page is no longer mistaken for a dead browser, and `kill` waits for the process to actually exit. (4) **An external audit, worked through**: a settings document that EXISTS but cannot be parsed is now fail-closed (a torn write used to reopen **every** capability the operator had switched off, and the next save wrote those defaults to disk), `history.jsonl` is written atomically, the client panel — read at request time — no longer fails to open because the host lacks a field, click/type/fill reach shadow-DOM and iframe elements the snapshot already shows, and Windows user-level installs (`%LOCALAPPDATA%`) are searched. **171/171 passing** (172 total, 1 skipped by platform), tag `v0.4.2` |
|
|
530
|
+
| Round 37 | 2026-10-04 | **Architecture refactor + an external review worked through** (no version bump; these changes ship with the next release): the theme is **one fact, no longer hand-copied in four places** — both published bugs (`browser_a11y`'s `selector`, `browser_screenshot`'s `width`/`height`) came out of the same mechanism, "the provider computed it, the schema declared it, and the tool layer's hand-written allow-list dropped it", and two full test runs stayed green because **no test ever ran a tool**. (1) **One field set, declared once**: new `src/tool-browser/element-fields.ts` (the element/node field tables, plus the item schema and the projections derived from them); `browser_snapshot`'s schema joined `browser_open`'s, which the earlier pass had done only half of; and new `tests/element-fields.test.mjs` (7 cases: schema ⟷ tables equal in both directions, the projection carries every declared field, the projection drops undeclared fields, and all 34 registered tools are walked to assert that what the schema requires the tool really produces). (2) **Page-script parsing goes from 4/16 to full coverage**: new `tests/page-scripts-parse.test.mjs` scans the source directly (the provider, `host-main`'s toolbar and download, the virtual cursor, the sidebar bridge) instead of relying on driving a behaviour to capture a script — the cursor, download and bridge scripts had never been parsed at all. (3) **Two real self-hosted bugs**: `materializeOnce`'s "reset on failure" was dead code (it stored a derived promise and compared the original, so the branch could never be taken), letting a child that is alive but not answering **poison that handle permanently** while the retry its comment promised was unreachable; and `fail()` cleared `kill()`'s 500 ms backstop — a dead socket is not a dead child — so it now clears only on an observed exit or the release path. (4) **The seam stops lying**: the handle's `download`/`flushAuth`/`restoreAuth`/`capture` and the host's `notifyUserActionError` were promoted from structural casts to interface declarations (so a new host written against the interface no longer gets a silent no-op), `capture`'s contract gained the size the `browser_screenshot` schema had always declared, and 8 empty methods that existed only to satisfy the interface plus 2 host-level `focus()` methods with no caller anywhere were deleted. (5) **Screenshot size is no longer CDP-carrier-only**: the earlier fix did only the CDP half — the native `capturePage` path never passed its size back, so `width`/`height` were **always missing on the default self-hosted carrier** — and the child now measures and reports them after any downscale, wired through `capture()`'s type, `screenshot()`'s return type and the native branch (reported when the capture was measured; `width?`/`height?` were optional all along). (6) **Settings and storage**: a **gate-holding section** that is present but not an object (`actions: false`, `null`, a string) is no longer treated as absent (which took each gate's permissive default) — every gate inside it now reads as **off**; and `history.maxEntries`/`maxAgeDays` stopped being dead knobs (parsed and persisted, but never handed to the `HistoryStore`), the reader now passing the live values to `updateLimits()`. (7) **System-browser carrier**: the `'exited'` error no longer reads every exit code as "handed the request to an instance holding the profile" — that story holds only for code 0, while a crash code sent people looking for a leftover process that was not there; the launch poll **checks `signalCode` as well** (a browser killed by a signal never sets `exitCode`, so checking only that let an instant death run out the full 30 seconds and report "did not expose CDP"); and the port probe's `fetch` gained an `AbortSignal`. (8) **RPC vocabulary**: new `src/browser-electron/rpc-ops.ts` gives the child's switch and the parent's senders one list of op names, with `tests/rpc-ops.test.mjs` reading both sources and asserting they agree (it caught two ops named from memory that the child does not handle). **183/183 passing** (**184** total, 1 skipped: creating a file symlink on Windows needs elevation) |
|
|
531
|
+
|
|
532
|
+
> The npm badge at the top is the authority on the registry's latest version (currently `0.4.2`). After a desktop upgrade, re-run `node desktop-bridge/install.mjs` once; the web side has no such step — see [Updating (the two hosts differ)](#updating-the-two-hosts-differ).
|
|
452
533
|
|
|
453
534
|
## Acknowledgements
|
|
454
535
|
|