dsh-browser-plus 0.0.0-stage → 0.5.0

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 (76) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/LICENSE +22 -0
  3. package/NOTICE.md +7 -0
  4. package/README.en.md +100 -0
  5. package/README.md +99 -2
  6. package/assets/dsh-browser-plus-256.png +0 -0
  7. package/assets/dsh-browser-plus-512.png +0 -0
  8. package/assets/dsh-browser-plus-small.svg +9 -0
  9. package/assets/dsh-browser-plus.ico +0 -0
  10. package/assets/dsh-browser-plus.svg +11 -0
  11. package/assets/readme-workspace.png +0 -0
  12. package/cordis.patch.yml +17 -0
  13. package/docs/MIGRATION.md +48 -0
  14. package/docs/README.md +22 -0
  15. package/docs/SOAK-CHECKLIST.md +98 -0
  16. package/docs/architecture.md +88 -0
  17. package/docs/tool-reference.md +124 -0
  18. package/docs/user-guide.md +121 -0
  19. package/docs/why-browser.md +45 -0
  20. package/lib/browser/runtime.d.ts +225 -0
  21. package/lib/browser/runtime.js +302 -0
  22. package/lib/browser/types.d.ts +668 -0
  23. package/lib/browser/types.js +18 -0
  24. package/lib/browser-electron/auth-cookies.d.ts +54 -0
  25. package/lib/browser-electron/auth-cookies.js +83 -0
  26. package/lib/browser-electron/chrome-state.d.ts +187 -0
  27. package/lib/browser-electron/chrome-state.js +12 -0
  28. package/lib/browser-electron/entry.d.ts +66 -0
  29. package/lib/browser-electron/entry.js +62 -0
  30. package/lib/browser-electron/fingerprint.d.ts +29 -0
  31. package/lib/browser-electron/fingerprint.js +42 -0
  32. package/lib/browser-electron/host-main.d.ts +18 -0
  33. package/lib/browser-electron/host-main.js +2494 -0
  34. package/lib/browser-electron/icon.d.ts +11 -0
  35. package/lib/browser-electron/icon.js +23 -0
  36. package/lib/browser-electron/page-chrome.d.ts +21 -0
  37. package/lib/browser-electron/page-chrome.js +2034 -0
  38. package/lib/browser-electron/provider.d.ts +709 -0
  39. package/lib/browser-electron/provider.js +2575 -0
  40. package/lib/browser-electron/remote-host.d.ts +143 -0
  41. package/lib/browser-electron/remote-host.js +952 -0
  42. package/lib/browser-electron/task-summary.d.ts +2 -0
  43. package/lib/browser-electron/task-summary.js +12 -0
  44. package/lib/browser-electron/task-thumbnail.d.ts +11 -0
  45. package/lib/browser-electron/task-thumbnail.js +9 -0
  46. package/lib/browser-electron/write-guard.d.ts +41 -0
  47. package/lib/browser-electron/write-guard.js +123 -0
  48. package/lib/index.d.ts +16 -0
  49. package/lib/index.js +14 -0
  50. package/lib/tool-browser/index.d.ts +31 -0
  51. package/lib/tool-browser/index.js +1931 -0
  52. package/package.json +95 -4
  53. package/screenshots.json +3 -0
  54. package/scripts/build-icons.mjs +80 -0
  55. package/scripts/capture-window.ps1 +79 -0
  56. package/scripts/crop-image.ps1 +20 -0
  57. package/scripts/smoke-browser-tools.mjs +1968 -0
  58. package/scripts/smoke-chrome-world.mjs +63 -0
  59. package/scripts/smoke-electron-host.mjs +50 -0
  60. package/src/browser/runtime.ts +470 -0
  61. package/src/browser/types.ts +649 -0
  62. package/src/browser-electron/auth-cookies.ts +125 -0
  63. package/src/browser-electron/chrome-state.ts +174 -0
  64. package/src/browser-electron/entry.ts +115 -0
  65. package/src/browser-electron/fingerprint.ts +45 -0
  66. package/src/browser-electron/host-main.ts +2330 -0
  67. package/src/browser-electron/icon.ts +26 -0
  68. package/src/browser-electron/page-chrome.ts +2046 -0
  69. package/src/browser-electron/provider.ts +3088 -0
  70. package/src/browser-electron/remote-host.ts +1004 -0
  71. package/src/browser-electron/task-summary.ts +10 -0
  72. package/src/browser-electron/task-thumbnail.ts +17 -0
  73. package/src/browser-electron/write-guard.ts +134 -0
  74. package/src/index.ts +52 -0
  75. package/src/tool-browser/index.ts +1974 -0
  76. package/src/types/electron-shim.d.ts +143 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Verify chromeWorld: 'isolated' in a real window.
3
+ *
4
+ * This is the one declared feature that had never been exercised: the chrome is
5
+ * injected into an isolated world so the page's own scripts cannot read its
6
+ * globals. The claim has two halves -- the chrome still mounts, and the page's
7
+ * main world cannot see it -- and an A/B against the default mode proves the
8
+ * switch actually changes something rather than being inert.
9
+ *
10
+ * Run: npm run smoke:chrome-world
11
+ */
12
+ import { mkdtempSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+
16
+ import { RemoteElectronViewHost, defaultHostMainPath } from '../lib/browser-electron/remote-host.js'
17
+
18
+ const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
19
+ const results = []
20
+
21
+ const probe = async (label, options) => {
22
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-chrome-world-'))
23
+ process.env.DSH_BROWSER_PLUS_USER_DATA = profile
24
+ const host = new RemoteElectronViewHost(defaultHostMainPath(), options)
25
+ try {
26
+ const view = host.createView('chrome-world', 'Chrome World')
27
+ await view.sendCommand('Page.enable', {})
28
+ await view.sendCommand('Page.navigate', { url: 'https://example.com/' })
29
+ await wait(2500)
30
+ const reply = await view.sendCommand('Runtime.evaluate', {
31
+ expression: 'JSON.stringify({ chromeHost: document.getElementById("__dsh_browser_chrome_host__") !== null, binding: typeof window.__dshBrowserTaskAction, tasks: typeof window.__dshTasks, trail: typeof window.__dshTrail })',
32
+ returnByValue: true,
33
+ })
34
+ const value = JSON.parse(reply?.result?.value ?? '{}')
35
+ results.push({ label, ...value })
36
+ console.log(' ' + label.padEnd(22) + JSON.stringify(value))
37
+ return value
38
+ } finally {
39
+ host.dispose()
40
+ await wait(1500)
41
+ try { rmSync(profile, { recursive: true, force: true }) } catch { /* briefly locked */ }
42
+ }
43
+ }
44
+
45
+ const main = await probe('main (default)', {})
46
+ const isolated = await probe('isolated', { chromeWorld: 'isolated' })
47
+
48
+ // Assert only what chromeWorld claims. window.__dshBrowserTaskAction is reported
49
+ // but not asserted: it appears asynchronously after load, so a fixed wait
50
+ // measures it inconsistently (observed both ways at 2.5s and 3s).
51
+ const failures = []
52
+ if (main.chromeHost !== true) failures.push('the chrome did not mount in main mode')
53
+ if (main.tasks !== 'object' || main.trail !== 'object') failures.push('main mode should leak the chrome globals, or the A/B proves nothing')
54
+ if (isolated.chromeHost !== true) failures.push('the chrome did not mount in isolated mode')
55
+ for (const key of ['binding', 'tasks', 'trail']) {
56
+ if (isolated[key] !== 'undefined') failures.push('isolated mode still leaks window.' + (key === 'binding' ? '__dshBrowserTaskAction' : '__dsh' + key[0].toUpperCase() + key.slice(1)))
57
+ }
58
+ console.log('')
59
+ if (failures.length > 0) {
60
+ console.log(JSON.stringify({ ok: false, failures }))
61
+ process.exit(1)
62
+ }
63
+ console.log(JSON.stringify({ ok: true, note: 'the chrome mounts in both modes; only the default leaks its globals to the page' }))
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict'
2
+
3
+ import { RemoteElectronViewHost, defaultHostMainPath } from '../lib/browser-electron/remote-host.js'
4
+
5
+ function withTimeout(promise, timeoutMs, label) {
6
+ return new Promise((resolve, reject) => {
7
+ const timer = setTimeout(() => reject(new Error(label + ' timed out after ' + String(timeoutMs) + 'ms')), timeoutMs)
8
+ promise.then(
9
+ value => { clearTimeout(timer); resolve(value) },
10
+ error => { clearTimeout(timer); reject(error) },
11
+ )
12
+ })
13
+ }
14
+
15
+ function delay(ms) {
16
+ return new Promise(resolve => setTimeout(resolve, ms))
17
+ }
18
+
19
+ const host = new RemoteElectronViewHost(defaultHostMainPath())
20
+ try {
21
+ const view = host.createView('electron-host-smoke', 'Electron Host Smoke')
22
+ await withTimeout(view.sendCommand('Page.navigate', { url: 'http://127.0.0.1:3080/' }), 15_000, 'Page.navigate')
23
+ const binding = await withTimeout(view.sendCommand('Runtime.evaluate', {
24
+ expression: 'typeof window.__dshBrowserTaskAction',
25
+ returnByValue: true,
26
+ }), 10_000, 'binding probe')
27
+ assert.equal(binding.result?.value, 'function', 'page task binding must be present')
28
+ const chromeReady = await withTimeout(view.sendCommand('Runtime.evaluate', {
29
+ expression: "new Promise(resolve => { const deadline = Date.now() + 3000; const inspect = () => { const host = document.getElementById('__dsh_browser_chrome_host__'); if (host?.getAttribute('data-dsh-browser-ready') === '1' || Date.now() >= deadline) { resolve(host?.getAttribute('data-dsh-browser-ready') || null); return } setTimeout(inspect, 50) }; inspect() })",
30
+ returnByValue: true,
31
+ awaitPromise: true,
32
+ }), 5_000, 'chrome ready probe')
33
+ assert.equal(chromeReady.result?.value, '1', 'page chrome must finish wiring interactive controls')
34
+
35
+ // The renderer's direct DOM interaction path is covered by page-chrome
36
+ // regression tests. CDP Input.dispatchMouseEvent does not surface a DOM
37
+ // pointer event in Electron 42, so it cannot faithfully stand in for a hand.
38
+ await delay(300)
39
+ await withTimeout(view.sendCommand('Runtime.evaluate', {
40
+ expression: "window.__dshBrowserTaskAction(JSON.stringify({ type: 'set-control-owner', taskKey: 'electron-host-smoke', control: 'human' })); 'sent'",
41
+ returnByValue: true,
42
+ }), 10_000, 'binding action')
43
+ await delay(300)
44
+ const task = await withTimeout(host.getTask('electron-host-smoke'), 10_000, 'task state query')
45
+ assert.equal(task?.control, 'human', 'Host must receive the page handoff')
46
+ assert.equal(task?.status, 'waiting-user', 'Host must show the handoff state')
47
+ console.log(JSON.stringify({ ok: true, task }))
48
+ } finally {
49
+ host.dispose()
50
+ }
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Service Definition for the browser capability seam (`ctx.browser`): the
3
+ * provider registry and provider-selecting execution for browser sessions.
4
+ * Duplicate ids are rejected. At execution time, a configured provider must
5
+ * exist and be usable; without one, exactly one usable provider is required,
6
+ * so selection never depends on registration order.
7
+ * @module dsh-browser-plus/browser
8
+ */
9
+
10
+ import { Context, Service } from '@deepseek-ai/cordis'
11
+ import z from '@deepseek-ai/schemastery'
12
+ import type {
13
+ BrowserContentRequest,
14
+ BrowserHandoffState,
15
+ BrowserContentResult,
16
+ BrowserDownloadRequest,
17
+ BrowserExecuteRequest,
18
+ BrowserExecuteResult,
19
+ BrowserFillRequest,
20
+ BrowserFillResult,
21
+ BrowserHistoryEntry,
22
+ BrowserNavigateRequest,
23
+ BrowserOpenOptions,
24
+ BrowserOpenRequest,
25
+ BrowserDragRequest,
26
+ BrowserDragResult,
27
+ BrowserPointerResult,
28
+ BrowserPointerTarget,
29
+ BrowserPressKeyRequest,
30
+ BrowserClearAuthRequest,
31
+ BrowserScrapeRequest,
32
+ BrowserScrapeStatus,
33
+ BrowserClearAuthResult,
34
+ BrowserProvider,
35
+ BrowserRefRequest,
36
+ BrowserScreenshotRequest,
37
+ BrowserScreenshotResult,
38
+ BrowserScrollIntoViewRequest,
39
+ BrowserScrollRequest,
40
+ BrowserScrollResult,
41
+ BrowserSessionId,
42
+ BrowserSnapshotResult,
43
+ BrowserSpaceInfo,
44
+ BrowserTaskInfo,
45
+ BrowserTaskUpdate,
46
+ BrowserTab,
47
+ BrowserTypeRequest,
48
+ BrowserUploadFileRequest,
49
+ BrowserUploadFileResult,
50
+ BrowserWaitForRequest,
51
+ BrowserWaitForResult,
52
+ BrowserChallenge,
53
+ ExportedCookie,
54
+ } from './types.ts'
55
+ import { BrowserError } from './types.ts'
56
+
57
+ export {
58
+ BrowserError,
59
+ } from './types.ts'
60
+ export type {
61
+ BrowserChallenge,
62
+ BrowserContentFormat,
63
+ BrowserControlOwner,
64
+ BrowserContentRequest,
65
+ BrowserContentResult,
66
+ BrowserDownloadRequest,
67
+ BrowserExecuteRequest,
68
+ BrowserExecuteResult,
69
+ BrowserFillField,
70
+ BrowserFillRequest,
71
+ BrowserFillResult,
72
+ BrowserHandoffState,
73
+ BrowserHistoryEntry,
74
+ BrowserNavigateRequest,
75
+ BrowserOpenOptions,
76
+ BrowserOpenRequest,
77
+ BrowserDragRequest,
78
+ BrowserDragResult,
79
+ BrowserPointerResult,
80
+ BrowserPointerTarget,
81
+ BrowserPressKeyRequest,
82
+ BrowserClearAuthRequest,
83
+ BrowserScrapeRequest,
84
+ BrowserScrapeStatus,
85
+ BrowserClearAuthResult,
86
+ BrowserProvider,
87
+ BrowserRefRequest,
88
+ BrowserScreenshotRequest,
89
+ BrowserScreenshotResult,
90
+ BrowserScrollIntoViewRequest,
91
+ BrowserScrollRequest,
92
+ BrowserScrollResult,
93
+ BrowserSessionId,
94
+ BrowserSnapshotElement,
95
+ BrowserSnapshotResult,
96
+ BrowserSpaceInfo,
97
+ BrowserTaskInfo,
98
+ BrowserTaskStatus,
99
+ BrowserTaskUpdate,
100
+ BrowserTab,
101
+ BrowserTypeRequest,
102
+ BrowserUploadFileRequest,
103
+ BrowserUploadFileResult,
104
+ BrowserWaitForRequest,
105
+ BrowserWaitForResult,
106
+ ExportedCookie,
107
+ } from './types.ts'
108
+
109
+ declare module '@deepseek-ai/cordis' {
110
+ interface Context {
111
+ browser: BrowserRuntime
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Config for the browser seam. `browserProvider` pins which provider wins;
117
+ * it is optional (a single registered usable provider auto-selects).
118
+ */
119
+ export interface BrowserRuntimeConfig {
120
+ /** Explicit browser provider id. Omitted = auto-select when exactly one usable. */
121
+ readonly browserProvider?: string
122
+ }
123
+
124
+ /**
125
+ * The browser access service. Registered as `ctx.browser` (one instance per
126
+ * context).
127
+ *
128
+ * Selection semantics (resolved at execution time, never order-dependent):
129
+ * - A configured id that is registered and `available()` → that provider.
130
+ * - A configured id not registered → `BROWSER_PROVIDER_CONFIGURED_MISSING`.
131
+ * - A configured id registered but unavailable → `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`.
132
+ * - No id configured, exactly one registered usable provider → that provider.
133
+ * - No id configured, multiple usable providers → `BROWSER_PROVIDER_AMBIGUOUS`.
134
+ * - No id configured, no usable provider → `BROWSER_PROVIDER_UNAVAILABLE`.
135
+ */
136
+ export class BrowserRuntime extends Service {
137
+ /** Provider selection config. */
138
+ static Config: z<BrowserRuntimeConfig> = z.object({
139
+ browserProvider: z.string(),
140
+ })
141
+
142
+ private providers = new Map<string, BrowserProvider>()
143
+ private readonly providerId: string | undefined
144
+
145
+ constructor(ctx: Context, config: BrowserRuntimeConfig = {}) {
146
+ super(ctx, 'browser')
147
+ this.providerId = config.browserProvider
148
+ }
149
+
150
+ /**
151
+ * Register a browser provider. Throws {@link BrowserError}
152
+ * `BROWSER_DUPLICATE_PROVIDER` if its id is already registered. Returns a
153
+ * disposer; disposed with the calling fiber.
154
+ * @param provider - the provider; its `id` is the registry key.
155
+ * @returns the disposer that unregisters the provider.
156
+ */
157
+ registerBrowserProvider(provider: BrowserProvider): () => void {
158
+ if (this.providers.has(provider.id)) {
159
+ throw new BrowserError(`a browser provider with id "${provider.id}" is already registered`, 'BROWSER_DUPLICATE_PROVIDER')
160
+ }
161
+ // Bind the generator to this instance instead of aliasing `this` to a
162
+ // local (no-this-alias): the effect body mutates the instance registry.
163
+ const dispose = this.ctx.effect(function* (this: BrowserRuntime) {
164
+ this.providers.set(provider.id, provider)
165
+ yield () => this.providers.delete(provider.id)
166
+ }.bind(this), 'browser.registerProvider()')
167
+ // ctx.effect's disposer returns Promise<void>; our disposer API is
168
+ // synchronous fire-and-forget — discard the (always-resolved) promise.
169
+ return () => void dispose()
170
+ }
171
+
172
+ /** Resolve the selected provider or throw the matching {@link BrowserError}. */
173
+ private resolveProvider(): BrowserProvider {
174
+ const { providerId, providers } = this
175
+ if (providerId !== undefined) {
176
+ const provider = providers.get(providerId)
177
+ if (!provider) {
178
+ throw new BrowserError(`configured browser provider "${providerId}" is not registered`, 'BROWSER_PROVIDER_CONFIGURED_MISSING')
179
+ }
180
+ if (!provider.available()) {
181
+ throw new BrowserError(`configured browser provider "${providerId}" is registered but unavailable`, 'BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE')
182
+ }
183
+ return provider
184
+ }
185
+ const usable = [...providers.values()].filter(provider => provider.available())
186
+ const [single] = usable
187
+ if (single === undefined) {
188
+ throw new BrowserError('no usable browser provider is registered', 'BROWSER_PROVIDER_UNAVAILABLE')
189
+ }
190
+ if (usable.length > 1) {
191
+ const ids = usable.map(provider => provider.id).join(', ')
192
+ throw new BrowserError(`multiple usable browser providers are registered (${ids}); configure one explicitly`, 'BROWSER_PROVIDER_AMBIGUOUS')
193
+ }
194
+ return single
195
+ }
196
+
197
+ /** Open a new browser session through the selected provider. */
198
+ async open(options?: BrowserOpenOptions): Promise<BrowserSessionId> {
199
+ return this.resolveProvider().open(options)
200
+ }
201
+
202
+ /** Open a URL through the selected provider, optionally in a new tab. */
203
+ async openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void> {
204
+ return this.resolveProvider().openUrl(session, request, signal)
205
+ }
206
+
207
+ /** List the session's tabs through the selected provider. */
208
+ async listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]> {
209
+ return this.resolveProvider().listTabs(session)
210
+ }
211
+
212
+ /** Switch to a tab through the selected provider. */
213
+ async switchTab(session: BrowserSessionId, tabId: string): Promise<void> {
214
+ return this.resolveProvider().switchTab(session, tabId)
215
+ }
216
+
217
+ /** Close one tab through the selected provider. */
218
+ async closeTab(session: BrowserSessionId, tabId: string): Promise<boolean> {
219
+ return this.resolveProvider().closeTab(session, tabId)
220
+ }
221
+
222
+ /** Close every tab and reset the session through the selected provider. */
223
+ async reset(session: BrowserSessionId): Promise<void> {
224
+ return this.resolveProvider().reset(session)
225
+ }
226
+
227
+ /** Navigate the session's page through the selected provider. */
228
+ async navigate(session: BrowserSessionId, request: BrowserNavigateRequest, signal?: AbortSignal): Promise<void> {
229
+ return this.resolveProvider().navigate(session, request, signal)
230
+ }
231
+
232
+ /** Navigate to the previous history entry through the selected provider. */
233
+ async back(session: BrowserSessionId, signal?: AbortSignal): Promise<boolean> {
234
+ return this.resolveProvider().back(session, signal)
235
+ }
236
+
237
+ /** Navigate to the next history entry through the selected provider. */
238
+ async forward(session: BrowserSessionId, signal?: AbortSignal): Promise<boolean> {
239
+ return this.resolveProvider().forward(session, signal)
240
+ }
241
+
242
+ /** Reload the active page through the selected provider. */
243
+ async reload(session: BrowserSessionId, signal?: AbortSignal): Promise<void> {
244
+ return this.resolveProvider().reload(session, signal)
245
+ }
246
+
247
+ /** Stop loading the active page through the selected provider. */
248
+ async stopLoading(session: BrowserSessionId, signal?: AbortSignal): Promise<void> {
249
+ return this.resolveProvider().stopLoading(session, signal)
250
+ }
251
+
252
+ /** Execute JS in the session's page context through the selected provider. */
253
+ async execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult> {
254
+ return this.resolveProvider().execute(session, request, signal)
255
+ }
256
+
257
+ /** Produce an AI-friendly snapshot of the session's page. */
258
+ async snapshot(session: BrowserSessionId, options: { query?: string; limit?: number } = {}, signal?: AbortSignal): Promise<BrowserSnapshotResult> {
259
+ return this.resolveProvider().snapshot(session, options, signal)
260
+ }
261
+
262
+ /** Apply device/viewport/media emulation to the session's active tab. */
263
+ async emulate(session: BrowserSessionId, options: { width?: number; height?: number; deviceScaleFactor?: number; mobile?: boolean; userAgent?: string; colorScheme?: 'light' | 'dark' | 'no-preference'; clear?: boolean } = {}): Promise<{ applied: string[] }> {
264
+ return this.resolveProvider().emulate(session, options)
265
+ }
266
+
267
+ /** Console messages the host captured for the session's active tab. */
268
+ async consoleMessages(session: BrowserSessionId, options: { limit?: number; level?: string; clear?: boolean } = {}): Promise<{ messages: Array<{ level: string; text: string; at: string }> }> {
269
+ return this.resolveProvider().consoleMessages(session, options)
270
+ }
271
+
272
+ /** Network requests the host captured for the session's active tab. */
273
+ async networkRequests(session: BrowserSessionId, options: { limit?: number; failedOnly?: boolean; urlContains?: string; clear?: boolean } = {}): Promise<{ requests: Array<{ method: string; url: string; status?: number; mime?: string; kind?: string; failed?: string; ms?: number; at: string }> }> {
274
+ return this.resolveProvider().networkRequests(session, options)
275
+ }
276
+
277
+ /** Set how the host answers the next JS dialog, and report the state. */
278
+ async setDialogPolicy(session: BrowserSessionId, policy: { behavior: 'accept' | 'dismiss'; promptText?: string }): Promise<{ dialog: unknown; policy: { behavior: 'accept' | 'dismiss'; promptText?: string } }> {
279
+ return this.resolveProvider().setDialogPolicy(session, policy)
280
+ }
281
+
282
+ /** Drain any pending dialog, then report the last one and the current policy. */
283
+ async inspectDialog(session: BrowserSessionId): Promise<{ dialog: unknown; policy: { behavior: 'accept' | 'dismiss'; promptText?: string } }> {
284
+ return this.resolveProvider().inspectDialog(session)
285
+ }
286
+
287
+ /** The last JS dialog the host reported, plus the current policy. */
288
+ dialogState(session: BrowserSessionId): { dialog: unknown; policy: { behavior: 'accept' | 'dismiss'; promptText?: string } } {
289
+ return this.resolveProvider().dialogState(session)
290
+ }
291
+
292
+ /** Click one element referenced by an exact snapshot. */
293
+ async clickRef(session: BrowserSessionId, request: BrowserRefRequest, signal?: AbortSignal): Promise<void> {
294
+ return this.resolveProvider().clickRef(session, request, signal)
295
+ }
296
+
297
+ /** Scroll one element referenced by an exact snapshot into view. */
298
+ async scrollIntoView(session: BrowserSessionId, request: BrowserScrollIntoViewRequest, signal?: AbortSignal): Promise<BrowserScrollResult> {
299
+ return this.resolveProvider().scrollIntoView(session, request, signal)
300
+ }
301
+
302
+ /** Fetch page content in a requested format. */
303
+ async content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult> {
304
+ return this.resolveProvider().content(session, request, signal)
305
+ }
306
+
307
+ /** Click at viewport coordinates through the selected provider. */
308
+ async click(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult> {
309
+ return this.resolveProvider().click(session, request, signal)
310
+ }
311
+
312
+ /** Double-click at viewport coordinates through the selected provider. */
313
+ async doubleClick(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult> {
314
+ return this.resolveProvider().doubleClick(session, request, signal)
315
+ }
316
+
317
+ /** Press on one target, move to another, release, through the selected provider. */
318
+ async drag(session: BrowserSessionId, request: BrowserDragRequest, signal?: AbortSignal): Promise<BrowserDragResult> {
319
+ return this.resolveProvider().drag(session, request, signal)
320
+ }
321
+
322
+ async hover(session: BrowserSessionId, request: BrowserPointerTarget, signal?: AbortSignal): Promise<BrowserPointerResult> {
323
+ return this.resolveProvider().hover(session, request, signal)
324
+ }
325
+
326
+ /** Scroll the active page through the selected provider. */
327
+ async scroll(session: BrowserSessionId, request: BrowserScrollRequest, signal?: AbortSignal): Promise<BrowserScrollResult> {
328
+ return this.resolveProvider().scroll(session, request, signal)
329
+ }
330
+
331
+ /** Attach a local file to a file input through the selected provider. */
332
+ async uploadFile(session: BrowserSessionId, request: BrowserUploadFileRequest, signal?: AbortSignal): Promise<BrowserUploadFileResult> {
333
+ return this.resolveProvider().uploadFile(session, request, signal)
334
+ }
335
+
336
+ /** Wait for an element through the selected provider (bounded polling). */
337
+ async waitForElement(session: BrowserSessionId, request: BrowserWaitForRequest, signal?: AbortSignal): Promise<BrowserWaitForResult> {
338
+ return this.resolveProvider().waitForElement(session, request, signal)
339
+ }
340
+
341
+ /** Type into the focused element through the selected provider. */
342
+ async type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void> {
343
+ return this.resolveProvider().type(session, request, signal)
344
+ }
345
+
346
+ /** Press a key into the session's page through the selected provider. */
347
+ async pressKey(session: BrowserSessionId, request: BrowserPressKeyRequest, signal?: AbortSignal): Promise<void> {
348
+ return this.resolveProvider().pressKey(session, request, signal)
349
+ }
350
+
351
+ /** Fill a form's fields in one batch through the selected provider. */
352
+ async fillForm(session: BrowserSessionId, request: BrowserFillRequest, signal?: AbortSignal): Promise<BrowserFillResult> {
353
+ return this.resolveProvider().fillForm(session, request, signal)
354
+ }
355
+
356
+ /** Capture the current page through the selected provider. */
357
+ async screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult> {
358
+ return this.resolveProvider().screenshot(session, request, signal)
359
+ }
360
+
361
+ /** Check for a human-verification challenge on the active tab. */
362
+ async detectChallenge(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserChallenge> {
363
+ return this.resolveProvider().detectChallenge(session, signal)
364
+ }
365
+
366
+ /** Return the session's chronological operation log through the provider. */
367
+ async history(session: BrowserSessionId): Promise<readonly BrowserHistoryEntry[]> {
368
+ return this.resolveProvider().history(session)
369
+ }
370
+
371
+ /** Replay one recorded operation by sequence number through the provider. */
372
+ async replay(session: BrowserSessionId, seq: number): Promise<void> {
373
+ return this.resolveProvider().replay(session, seq)
374
+ }
375
+
376
+ /** Download a URL to a local file through the provider. */
377
+ async download(session: BrowserSessionId, request: BrowserDownloadRequest, signal?: AbortSignal): Promise<{ readonly path: string }> {
378
+ return this.resolveProvider().download(session, request, signal)
379
+ }
380
+
381
+ /** Export the session's cookies through the provider. */
382
+ async flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]> {
383
+ return this.resolveProvider().flushAuth(session)
384
+ }
385
+
386
+ /** Import cookies into the session through the provider. */
387
+ async restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number> {
388
+ return this.resolveProvider().restoreAuth(session, cookies)
389
+ }
390
+
391
+ /** Import cookies from a JSON export on disk through the provider. */
392
+ async importAuth(session: BrowserSessionId, path: string): Promise<{ restored: number; failed: number }> {
393
+ return this.resolveProvider().importAuth(session, path)
394
+ }
395
+
396
+ /** Start a background scrape batch through the provider. */
397
+ async startScrape(session: BrowserSessionId, request: BrowserScrapeRequest): Promise<BrowserScrapeStatus> {
398
+ return this.resolveProvider().startScrape(session, request)
399
+ }
400
+
401
+ /** Progress of one scrape batch through the provider. */
402
+ async scrapeStatus(id: string): Promise<BrowserScrapeStatus> {
403
+ return this.resolveProvider().scrapeStatus(id)
404
+ }
405
+
406
+ /** Ask a running scrape batch to stop through the provider. */
407
+ async stopScrape(id: string): Promise<BrowserScrapeStatus> {
408
+ return this.resolveProvider().stopScrape(id)
409
+ }
410
+
411
+ /** Every scrape batch the provider knows about. */
412
+ async listScrapes(): Promise<readonly BrowserScrapeStatus[]> {
413
+ return this.resolveProvider().listScrapes()
414
+ }
415
+
416
+ /** Remove cookies for one site scope through the provider. */
417
+ async clearAuth(session: BrowserSessionId, request: BrowserClearAuthRequest): Promise<BrowserClearAuthResult> {
418
+ return this.resolveProvider().clearAuth(session, request)
419
+ }
420
+
421
+ /** Set the session's browser task label through the selected provider. */
422
+ async setSpace(session: BrowserSessionId, label: string): Promise<void> {
423
+ return this.resolveProvider().setSpace(session, label)
424
+ }
425
+
426
+ /** List browser tasks (legacy spaces) with labels through the selected provider. */
427
+ async listSpaces(): Promise<readonly BrowserSpaceInfo[]> {
428
+ return this.resolveProvider().listSpaces()
429
+ }
430
+
431
+ /** List browser tasks with live collaboration status through the provider. */
432
+ async listTasks(): Promise<readonly BrowserTaskInfo[]> {
433
+ return this.resolveProvider().listTasks()
434
+ }
435
+
436
+ /** Read one session's task collaboration state through the provider. */
437
+ async getTask(session: BrowserSessionId): Promise<BrowserTaskInfo> {
438
+ return this.resolveProvider().getTask(session)
439
+ }
440
+
441
+ /** Update one session's visible task state through the provider. */
442
+ async updateTask(session: BrowserSessionId, update: BrowserTaskUpdate): Promise<BrowserTaskInfo> {
443
+ return this.resolveProvider().updateTask(session, update)
444
+ }
445
+
446
+ /** Mark one session as waiting for the user or returned to Agent control. */
447
+ async setHandoff(session: BrowserSessionId, state: BrowserHandoffState): Promise<BrowserTaskInfo> {
448
+ return this.resolveProvider().setHandoff(session, state)
449
+ }
450
+
451
+ /** Close the session through the selected provider. Idempotent; a missing
452
+ * provider is treated as already-closed so teardown paths stay no-ops. */
453
+ async close(session: BrowserSessionId): Promise<void> {
454
+ try {
455
+ await this.resolveProvider().close(session)
456
+ } catch (error) {
457
+ const code = error instanceof BrowserError ? (error as { code?: string }).code : undefined
458
+ if (code === 'BROWSER_PROVIDER_UNAVAILABLE'
459
+ || code === 'BROWSER_PROVIDER_CONFIGURED_MISSING'
460
+ || code === 'BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE'
461
+ || code === 'BROWSER_PROVIDER_AMBIGUOUS') {
462
+ return // provider gone; nothing to close
463
+ }
464
+ throw error
465
+ }
466
+ }
467
+ }
468
+
469
+ export default BrowserRuntime
470
+