dsh-builtin-browser 0.4.0 → 0.4.2

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.
Files changed (44) hide show
  1. package/README.en.md +68 -30
  2. package/README.md +66 -30
  3. package/client.js +60 -6
  4. package/cordis.patch.yml +21 -0
  5. package/desktop-bridge/plugin-browser-bridge.js +12 -4
  6. package/docs/architecture.md +12 -2
  7. package/docs/tool-reference.md +17 -17
  8. package/docs/user-guide.md +13 -4
  9. package/docs/why-browser.md +2 -2
  10. package/lib/browser/types.d.ts +25 -1
  11. package/lib/browser-electron/bridge-connection.d.ts +31 -5
  12. package/lib/browser-electron/bridge-connection.js +143 -21
  13. package/lib/browser-electron/entry.d.ts +1 -1
  14. package/lib/browser-electron/entry.js +61 -35
  15. package/lib/browser-electron/ephemeral-profile.d.ts +37 -0
  16. package/lib/browser-electron/ephemeral-profile.js +146 -0
  17. package/lib/browser-electron/history-store.d.ts +25 -0
  18. package/lib/browser-electron/history-store.js +51 -4
  19. package/lib/browser-electron/host-main.js +75 -27
  20. package/lib/browser-electron/provider.d.ts +44 -1
  21. package/lib/browser-electron/provider.js +379 -48
  22. package/lib/browser-electron/remote-host.d.ts +52 -3
  23. package/lib/browser-electron/remote-host.js +244 -23
  24. package/lib/browser-electron/settings-route.d.ts +28 -0
  25. package/lib/browser-electron/settings-route.js +28 -0
  26. package/lib/browser-electron/settings-store.d.ts +40 -0
  27. package/lib/browser-electron/settings-store.js +107 -11
  28. package/lib/browser-electron/system-browser.d.ts +71 -3
  29. package/lib/browser-electron/system-browser.js +355 -38
  30. package/lib/tool-browser/index.js +143 -34
  31. package/package.json +5 -3
  32. package/scripts/verify-host-compat.mjs +195 -0
  33. package/src/browser/types.ts +25 -1
  34. package/src/browser-electron/bridge-connection.ts +142 -21
  35. package/src/browser-electron/entry.ts +58 -33
  36. package/src/browser-electron/ephemeral-profile.ts +152 -0
  37. package/src/browser-electron/history-store.ts +51 -4
  38. package/src/browser-electron/host-main.ts +105 -35
  39. package/src/browser-electron/provider.ts +401 -51
  40. package/src/browser-electron/remote-host.ts +261 -36
  41. package/src/browser-electron/settings-route.ts +46 -0
  42. package/src/browser-electron/settings-store.ts +121 -11
  43. package/src/browser-electron/system-browser.ts +342 -21
  44. package/src/tool-browser/index.ts +144 -36
package/README.en.md CHANGED
@@ -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/content/screenshot) are never blocked.</p>
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 / keep cookies / auto-expand the side panel / close the browser when a session ends / show the cursor / vision strategy / allow credential reads — switches take effect <b>immediately</b>, no restart.</p>
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). 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>
@@ -175,9 +175,9 @@ See the full list in [Tool reference](#tool-reference).
175
175
  | --- | --- | --- |
176
176
  | `browser_open` | Open a URL (optionally in a new tab); returns a page snapshot | ✅ |
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; pierces same-origin iframes and Shadow DOM) | – |
179
- | `browser_a11y` | Accessibility tree: semantic role/name/value/states + coordinates per interactive node (pierces same-origin iframes and Shadow DOM) | – |
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) | ✅ |
@@ -192,23 +192,23 @@ See the full list in [Tool reference](#tool-reference).
192
192
  | `browser_check` | Check/uncheck a checkbox or radio (`target`-located) | ✅ |
193
193
  | `browser_select` | Select an option of a `<select>` by value/text/index (`target`-located) | ✅ |
194
194
  | `browser_clear` | Clear an input/textarea/contenteditable, or uncheck (`target`-located) | ✅ |
195
- | `browser_get_value` | Read an element's current value for verification (`target`-located) | – |
196
- | `browser_scrape` | Structured extraction: container selector + field map (`selector@attr`), static CSS only, CSP-safe | – |
195
+ | `browser_get_value` | Read an element's current value for verification (`target`-located) | – exempt |
196
+ | `browser_scrape` | Structured extraction: container selector + field map (`selector@attr`), static CSS only, CSP-safe | – exempt |
197
197
  | `browser_screenshot` | Capture, optional `fullPage`, `savePath`, JPEG (`format`/`quality`) and scaling (`maxWidth`/`maxHeight`); `savePath` shares the download gate (confined to `downloadDir`, never overwrites) | – |
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
- | `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 | ✅ |
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 | – 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
205
  | `browser_replay` | Replay one step by sequence number (navigate/execute/click/type) | ✅ |
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) | ✅ |
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; read-only tools (snapshot/content/screenshot/list_tabs/session/challenge/history) are never blocked.
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,7 +217,7 @@ 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, states (enabled/checked/expanded…) and coordinates — click/type them directly.
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 **non-default** states are 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
 
@@ -229,6 +229,18 @@ See the full list in [Tool reference](#tool-reference).
229
229
  - **Verify before clicking**: use `document.elementFromPoint(x, y)` to confirm the coordinate hits the intended element (button/link), then perform the real click.
230
230
  - **DPR awareness**: CDP input uses CSS pixels; on high-DPI screens calibrate with `elementFromPoint` instead of guessing coordinates.
231
231
 
232
+ ### Output size and truncation
233
+
234
+ Tool output is spent out of the caller's context, so several results have explicit bounds — and a truncation is always stated:
235
+
236
+ - **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.
237
+ - **`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.
238
+ - **`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.
239
+ - **`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.
240
+ - **`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.
241
+ - **`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.
242
+ - **`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.
243
+
232
244
  ## Configuration
233
245
 
234
246
  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 resolves a row's specifier to a package root — a row named after a subpath is skipped, and then the settings panel would never appear) plus three functional rows (`browser` / `browser-electron` / `tool-browser`). Per-row config:
@@ -238,7 +250,7 @@ The plugin mounts through `cordis.patch.yml` (**four rows**): one **inert root r
238
250
  | `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
251
  | `browser-electron` | `httpOnly` | boolean | `true` | Allow HTTP(S) navigation only; other protocols (e.g. `file:`/`data:`) rejected (`BROWSER_NAVIGATION_BLOCKED`) |
240
252
  | `browser-electron` | `snapshotMaxElements` | number | `60` | Max snapshot elements before truncation |
241
- | `browser-electron` | `contentMaxChars` | number | `100000` | Default content character cap |
253
+ | `browser-electron` | `contentMaxChars` | number | **no longer read (dead)** | The old "default content character cap". **Nothing reads it now, so changing it changes nothing** — the caps are fixed per format: html 50 000, json 50 000, txt 20 000, markdown 20 000, overridable per call with `maxChars`. The key is still accepted so an existing config file is not rejected for an unknown key |
242
254
  | `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
255
  | `tool-browser` | `timeoutMs` | number | `60000` | Cooperative tool timeout (ms) |
244
256
  | `tool-browser` | `tabTools` | boolean | `true` | Register tab-management tools (`browser_list_tabs` etc.) |
@@ -292,7 +304,7 @@ node desktop-bridge/install.mjs --revert # roll back
292
304
  > - the agent can read the **cookies and login state** in that partition, and `browser_auth` can export them (controlled by a setting);
293
305
  > - 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
306
  >
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 the agent stops reading cookies and login state, 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.
307
+ > 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
308
 
297
309
  **② Use the browser you already have (Chrome / Edge)**
298
310
 
@@ -300,10 +312,12 @@ The settings panel can point the plugin at an **installed Chrome or Edge** (`bro
300
312
 
301
313
  **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
314
 
315
+ **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.
316
+
303
317
  **What happens to login state**:
304
318
 
305
319
  - `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.
320
+ - `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
321
  - 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
322
 
309
323
  **③ A shell that provides `electronViewHost`** (older desktop shells): that view is used directly.
@@ -312,7 +326,7 @@ The settings panel can point the plugin at an **installed Chrome or Edge** (`bro
312
326
 
313
327
  > The visible view and column layout always belong to the host shell; the plugin owns the seam, the provider and the tools. Across all carriers the **toolset, browsing history, settings panel, synthetic cursor and teardown rules are identical** — only the carrier of the page differs.
314
328
  >
315
- > **Precedence**: an explicitly chosen installed browser > the desktop sidebar > self-hosting. If the chosen browser is **missing or fails to start**, a warning is logged and the bundled browser is kept — it is **never silently swapped** for something else.
329
+ > **Precedence**: an explicitly chosen installed browser > the desktop sidebar > self-hosting. A missing carrier is handled two different ways: `automatic` means "any browser will do", so it **logs a warning and keeps the bundled one**; but when you **named a browser explicitly** and it is absent, the plugin no longer settles for a log line — it adopts a carrier whose only job is to explain, so **every command reports to the caller which browser was missing, the names and locations that were searched, and three ways out**. You are not left staring at an Electron error wondering what actually went wrong.
316
330
 
317
331
  ## Requirements
318
332
 
@@ -326,15 +340,29 @@ The settings panel can point the plugin at an **installed Chrome or Edge** (`bro
326
340
 
327
341
  | Component | Version |
328
342
  | --- | --- |
329
- | DeepSeek Harness (dsh) | `0.2.0-rc.2` (peer range `>=0.1.1-rc.2 <0.3.0`) |
343
+ | 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
344
  | Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
331
345
  | Node.js | `22.20.0` |
332
346
  | Installed Chrome / Edge (optional carriers) | `154.0.8037.58` / `154.0.4258.37` |
333
- | dsh-builtin-browser | `0.4.0` |
347
+ | dsh-builtin-browser | `0.4.1` (the development branch is past that release) |
334
348
  | OS | Windows 10 (10.0.26200) |
335
349
 
336
350
  > 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
351
 
352
+ > **Re-verify host compatibility at any time**: `node scripts/verify-host-compat.mjs` assembles a host out
353
+ > of the `@deepseek-ai/*` packages **actually installed** in a profile, loads this plugin's compiled
354
+ > output, registers its tools through the host's own `defineTool`, and calls two of them over a fake
355
+ > provider (`browser_session` / `browser_a11y`). It answers "did a DSH upgrade break us?" without starting
356
+ > a browser. It reads the profiles under `$DSH_HOME/profiles`; `DSH_PROFILE` / `DSH_HOME` override that.
357
+ >
358
+ > **On semver and prereleases**: the range `>=0.1.1-rc.2 <0.3.0` matches no prerelease-tagged version
359
+ > (such as `0.2.1-alpha.1`) under **default semver semantics** — that is by specification, where a
360
+ > prerelease is only admitted by a comparator carrying the same major.minor.patch tuple. DSH evaluates
361
+ > plugin compatibility with `includePrerelease: true`
362
+ > (`packages/boot/app-boot/src/plugin-compatibility.ts`), so those hosts are in range in practice; if
363
+ > some external tool reads "incompatible" under default semantics, that is its rule differing from DSH's,
364
+ > not a wrong claim here.
365
+
338
366
  ## Updating (the two hosts differ)
339
367
 
340
368
  The plugin has one installation per host, and the two are updated separately — **updating one does not update the other**.
@@ -366,15 +394,15 @@ The plugin has one installation per host, and the two are updated separately —
366
394
 
367
395
  - JPEG screenshots work on the **self-hosted** carrier and with an **installed Chrome / Edge**. The **desktop sidebar** goes through the shell's `webContents.debugger`, whose Electron CDP JPEG encoder hangs, so a JPEG request on that carrier returns PNG. Downscaling (`maxWidth`/`maxHeight`) **works on all three** — through a CDP `clip.scale`, or the native `capturePage` when self-hosted.
368
396
  - Self-hosted captures prefer Electron's native `capturePage` (CDP `captureScreenshot` can hang with multiple views in the window); the target tab is raised before capturing.
369
- - `fullPage` capture is flaky under software compositing on some hosts — that affects the **self-hosted** carrier only (it captures through `capturePage`); the sidebar and an installed Chrome/Edge use CDP `captureBeyondViewport` and are unaffected.
397
+ - `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
398
  - CAPTCHA cannot be solved automatically: snapshots flag detected challenges; ask the human to complete it in the shared window instead of retrying.
371
399
  - Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
372
- - `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).
400
+ - `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
401
  - 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.
402
+ - `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
403
  - 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 `Storage.getCookies` / `Storage.setCookies`.
377
- - 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, host-side commands are bounded at 20 s, and the child's stderr plus exit code/signal are written to `$DSH_HOME/logs/dsh-builtin-browser-host.log` (2 MB self-truncating) so a plain `dsh web` self-hosted setup can diagnose a crash loop itself.
404
+ - 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.
405
+ - 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
406
  - 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
407
  - 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.
380
408
  - **The sidebar carrier does not report user-action events**: a human clicking in that page is the shell's own event, and the bridge has no operation that reports it. Features relying on that event therefore do not fire on the desktop sidebar; they do on the self-hosted carrier.
@@ -409,7 +437,7 @@ Code layout:
409
437
  | 3 | 2026-08 | **browser-bridge parity + review fixes**: `browser_a11y` a11y tree; 6 form-control tools (`browser_set_value`/`check`/`select`/`clear`/`get_value`/`refresh`); semantic `target` (css/text/xpath); `browser_scrape` structured extraction; independent BrowserWindow + real toolbar (address bar, back/forward/reload, tab strip) routed back into the session model; tool count **20 → 33**; CI switched to npm (no lockfile → pnpm cache broken), README corrections |
410
438
  | 4 | 2026-08 | **DSH 0.1.1-rc.2 alignment + review fixes**: peer floor `^0.1.1-rc.2`; fixed `browser_type` dropping text with a target, `browser_key` Space missing CDP `text`, keyUp failure sticking a key, `browser_wait` same-origin URL mis-match, `.part` rename residue, `snapshotMaxElements`/`contentMaxChars` config wiring, missing type exports; 3 regression tests |
411
439
  | 5 | 2026-08 | **Electron 44 compatibility**: `available()` is now side-effect free (no more triggering Electron 44 lazy download); `flushAuth` cookie-domain build fix |
412
- | 6 | 2026-08 | **Windows handshake & tab lookup**: the Electron GUI process never receives piped stdin → RPC token now flows over **stdin + env var**; `browser_switch_tab`/`browser_close_tab` locate tabs across sessions (`locateTab`), `browser_close_tab` no longer fakes success, unknown ids error with the session's actual tab list |
440
+ | 6 | 2026-08 | **Windows handshake & tab lookup**: the Electron GUI process never receives piped stdin → RPC token now flows over **stdin + env var**; `browser_switch_tab`/`browser_close_tab` locate tabs (`locateTab`), `browser_close_tab` no longer fakes success, unknown ids error with the session's actual tab list. That round fell back to searching **every** session; it was tightened later — lookup is now **confined to the calling session** (a stale id must not close or switch another task's tab, or the human's), keeping only the convenience of accepting a bare uuid as well as `tab:<uuid>` |
413
441
  | 7 | 2026-08 | **Toolbar interaction (Windows focus routing)**: keyboard input only reaches the focused view and the page view grabbed it, so the address bar could not receive input → added `wireFocusRouting` (clicking a view focuses it) + window refocus restores the last-clicked view; verified with real OS input probes |
414
442
  | **0.1.16** | 2026-08-26 | **Release**: all seven rounds ship as **0.1.16** (build clean, 21/21 tests pass, `v0.1.16`) |
415
443
  | 8 | 2026-08-27 | **DSH Desktop host-Electron reuse**: running inside an Electron process reuses the host binary directly; when the host runs the plugin in a child Node process, walk the process ancestry to find the host's Electron (PowerShell CIM on Windows, last resort only) — **DSH Desktop works with zero install**; error now hints per active profile; electron shim completed to fix the CI typecheck; docs updated |
@@ -421,10 +449,10 @@ Code layout:
421
449
  | 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
450
  | 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
451
  | 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-27 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
452
+ | **0.1.20** | 2026-08-28 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
425
453
  | 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
454
  | 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-08-28 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
455
+ | **0.1.21** | 2026-09-02 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
428
456
  | 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
457
  | 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
458
  | 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 +475,18 @@ Code layout:
447
475
  | **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
476
  | 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
477
  | 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
- > The npm badge at the top is the authority on the registry's latest version (currently `0.4.0`). 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).
478
+ | 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** |
479
+ | **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` |
480
+ | 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** |
481
+ | **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` |
482
+ | Round 32 | 2026-10-03 | **Output-cost audit + independent verification** (unreleased): ① **`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** |
483
+ | Round 33 | 2026-10-03 | **The system browser carrier: actually usable after it dies** (unreleased): ① **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** |
484
+ | Round 34 | 2026-10-03 | **Operator-level action switches (the real entry point)**: 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** |
485
+ | Round 35 | 2026-10-03 | **DSH 0.2.1 adaption (unreleased)**: 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 |
486
+ | Round 36 | 2026-10-04 | **An external audit, worked through (unreleased)**: 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. |
487
+ | **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` |
488
+
489
+ > The npm badge at the top is the authority on the registry's latest version (currently `0.4.1`). 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
490
 
453
491
  ## Acknowledgements
454
492