dsh-builtin-browser 0.4.7 → 0.4.8

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 (41) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.en.md +62 -491
  3. package/README.md +44 -470
  4. package/client.js +10 -10
  5. package/cordis.patch.yml +4 -4
  6. package/desktop-bridge/plugin-browser-bridge.js +1 -1
  7. package/docs/README.md +8 -4
  8. package/docs/architecture.md +82 -11
  9. package/docs/development.md +38 -0
  10. package/docs/limitations.md +49 -0
  11. package/docs/tool-reference.md +42 -42
  12. package/docs/user-guide.md +93 -37
  13. package/docs/why-browser.md +3 -3
  14. package/lib/browser/types.d.ts +2 -2
  15. package/lib/browser-electron/entry.d.ts +2 -2
  16. package/lib/browser-electron/entry.js +1 -1
  17. package/lib/browser-electron/history-store.d.ts +1 -1
  18. package/lib/browser-electron/history-store.js +1 -1
  19. package/lib/browser-electron/host-main.js +3 -3
  20. package/lib/browser-electron/provider.d.ts +9 -9
  21. package/lib/browser-electron/provider.js +14 -14
  22. package/lib/browser-electron/remote-host.js +1 -1
  23. package/lib/browser-electron/settings-store.d.ts +4 -4
  24. package/lib/index.d.ts +1 -1
  25. package/lib/index.js +1 -1
  26. package/lib/tool-browser/element-fields.d.ts +4 -4
  27. package/lib/tool-browser/element-fields.js +6 -6
  28. package/lib/tool-browser/index.d.ts +6 -6
  29. package/lib/tool-browser/index.js +161 -139
  30. package/package.json +2 -2
  31. package/scripts/verify-host-compat.mjs +6 -6
  32. package/src/browser/types.ts +2 -2
  33. package/src/browser-electron/entry.ts +2 -2
  34. package/src/browser-electron/history-store.ts +1 -1
  35. package/src/browser-electron/host-main.ts +3 -3
  36. package/src/browser-electron/provider.ts +15 -15
  37. package/src/browser-electron/remote-host.ts +1 -1
  38. package/src/browser-electron/settings-store.ts +4 -4
  39. package/src/index.ts +1 -1
  40. package/src/tool-browser/element-fields.ts +6 -6
  41. package/src/tool-browser/index.ts +164 -140
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-builtin-browser",
3
- "version": "0.4.7",
4
- "description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (browser_auth), CAPTCHA detection, batch form fill (browser_fill), authenticated download, history/replay, action restriction",
3
+ "version": "0.4.8",
4
+ "description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (dsh_browser_auth), CAPTCHA detection, batch form fill (dsh_browser_fill), authenticated download, history/replay, action restriction",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -169,20 +169,20 @@ try {
169
169
 
170
170
  const exec = { agent: { id: 'verify-host-compat' } }
171
171
  try {
172
- const value = await registered.get('browser_session').execute({}, exec)
173
- ok('browser_session ran', value !== undefined, JSON.stringify(value).slice(0, 100))
172
+ const value = await registered.get('dsh_browser_session').execute({}, exec)
173
+ ok('dsh_browser_session ran', value !== undefined, JSON.stringify(value).slice(0, 100))
174
174
  } catch (error) {
175
- ok('browser_session ran', false, String(error && error.message))
175
+ ok('dsh_browser_session ran', false, String(error && error.message))
176
176
  }
177
177
 
178
178
  try {
179
- const tool = registered.get('browser_a11y')
179
+ const tool = registered.get('dsh_browser_a11y')
180
180
  const value = await tool.execute({}, exec)
181
181
  const text = JSON.stringify(tool.output.render({}, value))
182
- ok('browser_a11y ran and rendered', value?.nodes?.length === 1, '')
182
+ ok('dsh_browser_a11y ran and rendered', value?.nodes?.length === 1, '')
183
183
  ok('its renderer emits the element selector', text.includes('#probe'), text.slice(0, 120))
184
184
  } catch (error) {
185
- ok('browser_a11y ran', false, String(error && error.message))
185
+ ok('dsh_browser_a11y ran', false, String(error && error.message))
186
186
  }
187
187
 
188
188
  console.log('')
@@ -65,7 +65,7 @@ export interface BrowserScrollRequest {
65
65
 
66
66
  /** Press one named key (Enter, Tab, arrows, …). */
67
67
  export interface BrowserKeyRequest {
68
- /** Key name from the supported set (see the browser_key tool). */
68
+ /** Key name from the supported set (see the dsh_browser_key tool). */
69
69
  readonly key: string
70
70
  }
71
71
 
@@ -361,7 +361,7 @@ export interface BrowserSnapshotElement {
361
361
  /**
362
362
  * True when the element lives inside an (same-origin) iframe: coordinates
363
363
  * are still top-document, but DOM selectors are frame-scoped — reach the
364
- * element via `iframe.contentDocument` in browser_execute.
364
+ * element via `iframe.contentDocument` in dsh_browser_execute.
365
365
  */
366
366
  readonly frame?: boolean
367
367
  }
@@ -5,7 +5,7 @@
5
5
  * `ctx.electronViewHost`, that host is used (embedded, human-machine shared
6
6
  * view). Otherwise the plugin self-hosts: it spawns its own Electron child
7
7
  * (`host-main.js`) and drives it over a local TCP JSON-RPC socket, so
8
- * installing the plugin is enough for `browser_*` tools to work on any
8
+ * installing the plugin is enough for `dsh_browser_*` tools to work on any
9
9
  * surface.
10
10
  * @module dsh-browser/browser-electron
11
11
  */
@@ -48,7 +48,7 @@ export interface Config {
48
48
  /** Allow navigation only to HTTP(S) URLs. Default true. */
49
49
  readonly httpOnly?: boolean
50
50
  /**
51
- * Directory `browser_download` save paths must resolve inside (prevents a
51
+ * Directory `dsh_browser_download` save paths must resolve inside (prevents a
52
52
  * prompt-injected agent from writing arbitrary machine paths). Default:
53
53
  * the user's Downloads folder; override to confine downloads elsewhere.
54
54
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Persistent browsing history: which pages the shared browser actually visited,
3
- * kept independently from any session's operation log (`browser_history`) and
3
+ * kept independently from any session's operation log (`dsh_browser_history`) and
4
4
  * from the browser process's lifetime.
5
5
  *
6
6
  * It lives beside the browser profile (`$DSH_HOME/dsh-builtin-browser-host/
@@ -41,7 +41,7 @@ import { dirname, join } from 'node:path'
41
41
  // several Electron instances sharing Roaming\Electron fight over the GPU
42
42
  // cache/session locks, which can leave the window without a display surface
43
43
  // (capturePage then fails). A dedicated userData also persists cookies across
44
- // host restarts (on top of browser_auth). Must run before app is ready.
44
+ // host restarts (on top of dsh_browser_auth). Must run before app is ready.
45
45
  try {
46
46
  const base = process.env.DSH_HOME ?? app.getPath('appData')
47
47
  app.setPath('userData', join(base, 'dsh-builtin-browser-host'))
@@ -769,7 +769,7 @@ async function handle(op: string, msg: { id: number; viewId?: string; windowId?:
769
769
  const entry = win?.views.get(viewId)
770
770
  if (win === undefined || entry === undefined) throw new Error(`focus: unknown view ${viewId}`)
771
771
  // Keys go to the view that holds web focus. A view that was never
772
- // clicked holds none, so the FIRST browser_key of a session was dropped
772
+ // clicked holds none, so the FIRST dsh_browser_key of a session was dropped
773
773
  // by the renderer while CDP still answered the dispatch with success —
774
774
  // and `document.hasFocus()` inside the page even read true.
775
775
  const contents = entry.webContentsView.webContents
@@ -881,7 +881,7 @@ async function handle(op: string, msg: { id: number; viewId?: string; windowId?:
881
881
  throw new Error('capture produced no image (view not painted)')
882
882
  }
883
883
  // The measured size travels with the image. Without it the native path — which is the
884
- // default self-hosted one — could never answer `browser_screenshot`'s width/height,
884
+ // default self-hosted one — could never answer `dsh_browser_screenshot`'s width/height,
885
885
  // because the seam's only channel was {base64, mime} and the provider had nothing to
886
886
  // forward. The caller's `maxWidth`/`maxHeight` are applied here, so this is the only
887
887
  // place that knows what came back.
@@ -393,7 +393,7 @@ export interface ElectronBrowserProviderConfig {
393
393
  */
394
394
  readonly contentMaxChars?: number
395
395
  /**
396
- * Directory `browser_download` save paths must resolve inside (prevents a
396
+ * Directory `dsh_browser_download` save paths must resolve inside (prevents a
397
397
  * prompt-injected agent from writing arbitrary machine paths). Default:
398
398
  * `~/Downloads`. NOTE: this default is a convenience, not a security
399
399
  * boundary — on Windows it may resolve to OneDrive/redirected paths;
@@ -477,7 +477,7 @@ export function layoutSize(metrics: Record<string, unknown>): { width: number; h
477
477
  }
478
478
 
479
479
  /**
480
- * Map CDP cookies onto the shape `browser_auth` exports.
480
+ * Map CDP cookies onto the shape `dsh_browser_auth` exports.
481
481
  *
482
482
  * CDP reports a cookie as domain + path rather than a URL, and in seconds rather than
483
483
  * milliseconds, so both are converted. Entries missing a name, value or domain are
@@ -955,9 +955,9 @@ export class ElectronBrowserProvider implements BrowserProvider {
955
955
  /**
956
956
  * Page.navigate resolves at navigation COMMIT, not at load: the new document
957
957
  * is already current (so location.href and document.title are the new page's)
958
- * while its DOM is still being parsed. browser_open snapshots immediately
958
+ * while its DOM is still being parsed. dsh_browser_open snapshots immediately
959
959
  * after navigating, which is why it could report the right title with zero
960
- * interactive elements — and why a separate browser_snapshot right after
960
+ * interactive elements — and why a separate dsh_browser_snapshot right after
961
961
  * always found them.
962
962
  *
963
963
  * Wait — bounded, best-effort — for the new document to settle: readyState
@@ -1077,7 +1077,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
1077
1077
  *
1078
1078
  * A renderer only delivers key events to the view that holds web focus. A
1079
1079
  * view that was created but never clicked holds none — and `Input` events
1080
- * injected over CDP do not grant it — so the FIRST `browser_key` of a fresh
1080
+ * injected over CDP do not grant it — so the FIRST `dsh_browser_key` of a fresh
1081
1081
  * session vanished inside the renderer while the command still answered
1082
1082
  * success. Hosts that cannot focus a view (a test double, a different shell
1083
1083
  * adapter) simply have no `focus`, and nothing changes for them.
@@ -1086,7 +1086,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
1086
1086
  if (handle.focus === undefined) return
1087
1087
  try {
1088
1088
  // Bounded, unlike the rest of this call: the child RPC has no timeout of its own,
1089
- // and this runs before typing, so an unresponsive child would hang browser_key for
1089
+ // and this runs before typing, so an unresponsive child would hang dsh_browser_key for
1090
1090
  // good. A timeout is caught below exactly like any other focus failure, so the
1091
1091
  // best-effort contract is unchanged.
1092
1092
  await withTimeout(handle.focus(), 5_000, undefined, 'browser: focusing the view timed out')
@@ -1328,12 +1328,12 @@ export class ElectronBrowserProvider implements BrowserProvider {
1328
1328
  // A timeout leaves the wedged view in the session, so every later read would pay the same
1329
1329
  // 30 seconds again — and `terminatePage` only interrupts the running script, it does not
1330
1330
  // make the view readable. Replacing it is what turns a permanent loss into one bad call:
1331
- // the tab keeps the id the caller holds, and the next browser_open with the real URL
1331
+ // the tab keeps the id the caller holds, and the next dsh_browser_open with the real URL
1332
1332
  // repopulates it.
1333
1333
  if (error instanceof Error && error.name === 'TimeoutError') {
1334
1334
  this.replaceTabWithBlank(s, tab)
1335
1335
  throw new BrowserError(
1336
- 'browser: snapshot timed out; the stuck tab was reset to blank. Call browser_open with the real target URL before reading it again.',
1336
+ 'browser: snapshot timed out; the stuck tab was reset to blank. Call dsh_browser_open with the real target URL before reading it again.',
1337
1337
  'BROWSER_SNAPSHOT_TIMEOUT',
1338
1338
  { cause: error },
1339
1339
  )
@@ -1536,7 +1536,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
1536
1536
  if (error instanceof Error && error.name === 'TimeoutError') {
1537
1537
  this.replaceTabWithBlank(s, tab)
1538
1538
  throw new BrowserError(
1539
- 'browser: a11y timed out; the stuck tab was reset to blank. Call browser_open with the real target URL before reading it again.',
1539
+ 'browser: a11y timed out; the stuck tab was reset to blank. Call dsh_browser_open with the real target URL before reading it again.',
1540
1540
  'BROWSER_A11Y_TIMEOUT',
1541
1541
  { cause: error },
1542
1542
  )
@@ -2003,7 +2003,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
2003
2003
  placeholder: f.placeholder ?? null,
2004
2004
  // A field with NO kind is a field with no kind FILTER: absent, not 'text'. Defaulting
2005
2005
  // it to 'text' here made `if (spec.kind)` above always true, so every unspecified
2006
- // field was narrowed to an <input> — browser_fill on a <textarea> reported
2006
+ // field was narrowed to an <input> — dsh_browser_fill on a <textarea> reported
2007
2007
  // `no "text" control matched`, and a contenteditable could not match any kind at all.
2008
2008
  // The value stays a string or null (never undefined): the in-page check tests truthiness,
2009
2009
  // and JSON drops an undefined member entirely, which is the same thing here.
@@ -2138,7 +2138,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
2138
2138
  el.dispatchEvent(new Event('change', { bubbles: true }))
2139
2139
  out.push({ ok: true, method: 'select', target: describe(spec) })
2140
2140
  } else if (type === 'file') {
2141
- out.push({ ok: false, error: 'file inputs cannot be set from script; use browser_download or ask the human', target: describe(spec) })
2141
+ out.push({ ok: false, error: 'file inputs cannot be set from script; use dsh_browser_download or ask the human', target: describe(spec) })
2142
2142
  } else if (type === 'checkbox') {
2143
2143
  const want = spec.value === true || spec.value === 'true' || spec.value === 'on'
2144
2144
  if (el.checked !== want) el.click()
@@ -2592,7 +2592,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
2592
2592
  * Refuse an action the settings have switched off.
2593
2593
  *
2594
2594
  * These switches belong to the OPERATOR. The settings document is written through the
2595
- * plugin's own panel and no tool can reach it, so — unlike `browser_restrict`, which the
2595
+ * plugin's own panel and no tool can reach it, so — unlike `dsh_browser_restrict`, which the
2596
2596
  * model owns and can lift at will — a refusal here stands for as long as the setting does.
2597
2597
  * That is the point of them: a deployment can take page-script execution, downloads, or
2598
2598
  * login-state writes off the table without relying on the model's cooperation.
@@ -2619,7 +2619,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
2619
2619
 
2620
2620
  /**
2621
2621
  * Admit a caller-supplied save path for a file the browser writes to disk.
2622
- * ONE gate for both `browser_download` and `browser_screenshot`: the path
2622
+ * ONE gate for both `dsh_browser_download` and `dsh_browser_screenshot`: the path
2623
2623
  * must be absolute, must resolve inside `downloadDir`, and must not already
2624
2624
  * exist. Without it a prompt-injected agent could write — or silently
2625
2625
  * replace — any file the DSH process can reach, which also escapes the file
@@ -3053,7 +3053,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
3053
3053
  }
3054
3054
 
3055
3055
  /**
3056
- * List persisted visits, newest first — what `browser_visited` reads back.
3056
+ * List persisted visits, newest first — what `dsh_browser_visited` reads back.
3057
3057
  * @param options - result cap and an optional hostname filter.
3058
3058
  * @returns the matching visits, or an empty list when history is disabled.
3059
3059
  */
@@ -3200,7 +3200,7 @@ export class ElectronBrowserProvider implements BrowserProvider {
3200
3200
  *
3201
3201
  * A view whose renderer never commits cannot be read again: every evaluate into it runs out its
3202
3202
  * whole budget. Replacing it is what turns that from permanent into one bad call — the tab id
3203
- * callers hold stays valid, and the next `browser_open` with the real URL re-populates it.
3203
+ * callers hold stays valid, and the next `dsh_browser_open` with the real URL re-populates it.
3204
3204
  *
3205
3205
  * The old view is destroyed AFTER the new one is in place, so a failure here cannot leave the
3206
3206
  * session holding a handle that is already gone.
@@ -1327,7 +1327,7 @@ class DeferredRemoteView implements ElectronViewHandle {
1327
1327
  * with "browser host is not running". Instead: drop the cache, rebuild the
1328
1328
  * view against a freshly spawned child, and retry the operation exactly
1329
1329
  * once — so sessions that survive a host death self-heal on their FIRST
1330
- * call after it, without a manual browser_reset_session. Page-level errors
1330
+ * call after it, without a manual dsh_browser_reset_session. Page-level errors
1331
1331
  * are never retried (the view itself is fine).
1332
1332
  */
1333
1333
  private async withRecovery<T>(op: (view: RemoteView) => Promise<T>): Promise<T> {
@@ -55,15 +55,15 @@ export interface BrowserSettings {
55
55
  *
56
56
  * These are OPERATOR switches: the settings panel is not reachable from the model, so a
57
57
  * value here refuses whatever the model asks for, and no tool can lift it. That is what
58
- * separates them from `browser_restrict`, which is the model's own soft guardrail. All
58
+ * separates them from `dsh_browser_restrict`, which is the model's own soft guardrail. All
59
59
  * three default to on, which is the documented out-of-the-box behaviour.
60
60
  */
61
61
  readonly actions: {
62
- /** Run arbitrary page scripts in the active tab (`browser_execute`). */
62
+ /** Run arbitrary page scripts in the active tab (`dsh_browser_execute`). */
63
63
  readonly allowExecute: boolean
64
- /** Download a URL to disk (`browser_download`). */
64
+ /** Download a URL to disk (`dsh_browser_download`). */
65
65
  readonly allowDownload: boolean
66
- /** Write cookies — importing login state (`browser_auth` restore). */
66
+ /** Write cookies — importing login state (`dsh_browser_auth` restore). */
67
67
  readonly allowCredentialWrite: boolean
68
68
  }
69
69
  }
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * pieces. The cordis.patch.yml rows reference subpath exports:
4
4
  * - `dsh-builtin-browser/browser` -> the ctx.browser seam (Service)
5
5
  * - `dsh-builtin-browser/browser-electron` -> the Electron CDP provider
6
- * - `dsh-builtin-browser/tool-browser` -> the model-facing browser_* tools
6
+ * - `dsh-builtin-browser/tool-browser` -> the model-facing dsh_browser_* tools
7
7
  * The capability itself is composed by those rows; this root entry exists for
8
8
  * programmatic imports AND because the host's client-module scan identifies a
9
9
  * package's browser half through a loader row whose specifier resolves to the
@@ -8,9 +8,9 @@
8
8
  * and the tool's `output.schema` declared the properties. Nothing connected them, and the
9
9
  * result is this project's most repeatable bug:
10
10
  *
11
- * - `browser_a11y`'s `selector` — computed by the provider, declared by the schema,
11
+ * - `dsh_browser_a11y`'s `selector` — computed by the provider, declared by the schema,
12
12
  * printed by the renderer, and dropped by the projection. 134 tests stayed green.
13
- * - `browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
13
+ * - `dsh_browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
14
14
  * schema, and dropped by the projection. 172 tests stayed green. It was found a round
15
15
  * later, by reading the three places that agree and noticing the fourth did not.
16
16
  *
@@ -33,7 +33,7 @@ interface FieldSpec {
33
33
  }
34
34
 
35
35
  /**
36
- * The fields of one entry in `browser_snapshot` / `browser_open`'s `elements`.
36
+ * The fields of one entry in `dsh_browser_snapshot` / `dsh_browser_open`'s `elements`.
37
37
  *
38
38
  * `selector` is the reason this file exists; see the header. It was added to the schema and
39
39
  * the projection separately, and only one of them landed.
@@ -49,7 +49,7 @@ const ELEMENT_FIELDS = {
49
49
  } as const satisfies Record<string, FieldSpec>
50
50
 
51
51
  /**
52
- * The fields of one entry in `browser_a11y`'s `nodes`.
52
+ * The fields of one entry in `dsh_browser_a11y`'s `nodes`.
53
53
  *
54
54
  * Different shape from an element on purpose: a role and an accessible name carry meaning a
55
55
  * tag and a label string do not, and `depth` is what renders the tree.
@@ -91,7 +91,7 @@ function project(fields: Record<string, FieldSpec>, source: Record<string, unkno
91
91
  }
92
92
 
93
93
  /**
94
- * The `elements` item schema for browser_snapshot and browser_open.
94
+ * The `elements` item schema for dsh_browser_snapshot and dsh_browser_open.
95
95
  *
96
96
  * Written as a literal rather than generated, because `defineTool` validates the output schema
97
97
  * against its own `ValueSchemaSpec` and a generated `Record<string, …>` is too wide to satisfy
@@ -114,7 +114,7 @@ export const ELEMENT_ITEM_SCHEMA = {
114
114
  } as const
115
115
 
116
116
  /**
117
- * The `nodes` item schema for browser_a11y.
117
+ * The `nodes` item schema for dsh_browser_a11y.
118
118
  *
119
119
  * Same reasoning as {@link ELEMENT_ITEM_SCHEMA}: literal for the type checker, table-backed by
120
120
  * test.