dsh-builtin-browser 0.4.6 → 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.
- package/CHANGELOG.md +76 -0
- package/README.en.md +62 -491
- package/README.md +44 -470
- package/client.js +10 -10
- package/cordis.patch.yml +4 -4
- package/desktop-bridge/plugin-browser-bridge.js +1 -1
- package/docs/README.md +8 -4
- package/docs/architecture.md +82 -11
- package/docs/development.md +38 -0
- package/docs/limitations.md +49 -0
- package/docs/tool-reference.md +42 -42
- package/docs/user-guide.md +93 -37
- package/docs/why-browser.md +3 -3
- package/lib/browser/types.d.ts +2 -2
- package/lib/browser-electron/bridge-connection.d.ts +7 -1
- package/lib/browser-electron/bridge-connection.js +58 -3
- package/lib/browser-electron/desktop-bridge-host.d.ts +11 -0
- package/lib/browser-electron/desktop-bridge-host.js +21 -1
- package/lib/browser-electron/entry.d.ts +2 -2
- package/lib/browser-electron/entry.js +186 -103
- package/lib/browser-electron/history-store.d.ts +1 -1
- package/lib/browser-electron/history-store.js +1 -1
- package/lib/browser-electron/host-main.js +3 -3
- package/lib/browser-electron/provider.d.ts +32 -8
- package/lib/browser-electron/provider.js +97 -16
- package/lib/browser-electron/remote-host.js +1 -1
- package/lib/browser-electron/settings-store.d.ts +14 -6
- package/lib/browser-electron/settings-store.js +29 -9
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tool-browser/element-fields.d.ts +4 -4
- package/lib/tool-browser/element-fields.js +6 -6
- package/lib/tool-browser/index.d.ts +6 -6
- package/lib/tool-browser/index.js +196 -139
- package/package.json +2 -2
- package/scripts/verify-host-compat.mjs +6 -6
- package/src/browser/types.ts +2 -2
- package/src/browser-electron/bridge-connection.ts +61 -3
- package/src/browser-electron/desktop-bridge-host.ts +22 -1
- package/src/browser-electron/entry.ts +101 -9
- package/src/browser-electron/history-store.ts +1 -1
- package/src/browser-electron/host-main.ts +3 -3
- package/src/browser-electron/provider.ts +135 -31
- package/src/browser-electron/remote-host.ts +1 -1
- package/src/browser-electron/settings-store.ts +32 -13
- package/src/index.ts +1 -1
- package/src/tool-browser/element-fields.ts +6 -6
- package/src/tool-browser/index.ts +197 -140
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model-facing browser tools over `ctx.browser`: `
|
|
3
|
-
* `
|
|
4
|
-
* `
|
|
5
|
-
* `
|
|
2
|
+
* Model-facing browser tools over `ctx.browser`: `dsh_browser_open`,
|
|
3
|
+
* `dsh_browser_snapshot`, `dsh_browser_execute`, `dsh_browser_content`,
|
|
4
|
+
* `dsh_browser_screenshot`, and tab management (`dsh_browser_list_tabs`,
|
|
5
|
+
* `dsh_browser_switch_tab`, `dsh_browser_close_tab`, `dsh_browser_reset`).
|
|
6
6
|
*
|
|
7
7
|
* The tool layer owns only the model-facing schema, argument validation, and
|
|
8
8
|
* result formatting — never provider selection or page driving, which belong
|
|
9
9
|
* to the seam. Session lifecycle is owned here at the plugin level: each
|
|
10
10
|
* calling task (a DSH session) gets its own browser session — the first
|
|
11
|
-
* `
|
|
11
|
+
* `dsh_browser_open` (or any tool when no session exists) opens it, and later
|
|
12
12
|
* tools in the same task reuse it. Concurrent tasks therefore never fight
|
|
13
13
|
* over tabs, history, or navigation state.
|
|
14
14
|
* @module dsh-browser/tool-browser
|
|
@@ -72,7 +72,7 @@ interface ToolBrowserState {
|
|
|
72
72
|
*
|
|
73
73
|
* Two different things share this policy. A restriction from `cordis.patch.yml` is an
|
|
74
74
|
* operator decision about the whole plugin, so it applies to everyone and lives in
|
|
75
|
-
* `configRestrictedTo`. One set by `
|
|
75
|
+
* `configRestrictedTo`. One set by `dsh_browser_restrict` is a decision by one task about
|
|
76
76
|
* its own work, so it is keyed by task — a single shared value let one task lock the
|
|
77
77
|
* browser tools of every other task, which is not what any caller asks for. Sessions
|
|
78
78
|
* beside it were already keyed this way; this was not.
|
|
@@ -98,33 +98,33 @@ const liveStates = new Set<ToolBrowserState>()
|
|
|
98
98
|
* Tools that only observe, plus the ones that undo a restriction.
|
|
99
99
|
*
|
|
100
100
|
* Never subject to an allow-list: restricting what the agent may *do* must not also
|
|
101
|
-
* blind it or trap it, and the tool descriptions promise as much.
|
|
101
|
+
* blind it or trap it, and the tool descriptions promise as much. dsh_browser_restrict
|
|
102
102
|
* is in here so a task that restricted everything can still lift it.
|
|
103
103
|
*/
|
|
104
104
|
export const READ_ONLY_TOOLS: ReadonlySet<string> = new Set([
|
|
105
105
|
// Tools that only observe.
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
//
|
|
106
|
+
'dsh_browser_snapshot', 'dsh_browser_a11y', 'dsh_browser_content', 'dsh_browser_scrape',
|
|
107
|
+
'dsh_browser_screenshot', 'dsh_browser_get_value', 'dsh_browser_wait', 'dsh_browser_challenge',
|
|
108
|
+
'dsh_browser_list_tabs', 'dsh_browser_session', 'dsh_browser_history', 'dsh_browser_visited',
|
|
109
|
+
// dsh_browser_auth reads cookies out of the browser: an observation, and one the
|
|
110
110
|
// credentials switch gates on its own (see credentials.allowRead in the settings).
|
|
111
|
-
//
|
|
111
|
+
// dsh_browser_auth is deliberately NOT here. It reads cookies on "flush" but WRITES them on
|
|
112
112
|
// "restore", to arbitrary domains, which is an action like any other — exempting the tool
|
|
113
113
|
// would let a task that restricted its own actions rewrite the shared browser's logins.
|
|
114
114
|
//
|
|
115
115
|
// Getting out of a bad state, or lifting the restriction itself. Without these a task
|
|
116
116
|
// that restricted everything could neither recover nor be released.
|
|
117
|
-
'
|
|
117
|
+
'dsh_browser_restrict', 'dsh_browser_reset_session', 'dsh_browser_reset',
|
|
118
118
|
])
|
|
119
119
|
function assertAllowed(state: ToolBrowserState, toolName: string, task: string): void {
|
|
120
|
-
//
|
|
120
|
+
// dsh_browser_restrict must stay usable, or a task that restricted everything
|
|
121
121
|
// could never lift it again.
|
|
122
122
|
// An allow-list is a statement about ACTIONS, so it must not take away the ability to
|
|
123
123
|
// look, or to get out of a bad state. The tool description and both READMEs promise
|
|
124
124
|
// that read-only tools are never blocked; before this they were, because only
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
if (READ_ONLY_TOOLS.has(toolName) || toolName === '
|
|
125
|
+
// dsh_browser_restrict itself was exempt — a narrow list then locked dsh_browser_history,
|
|
126
|
+
// dsh_browser_reset_session and dsh_browser_auth away with no way back.
|
|
127
|
+
if (READ_ONLY_TOOLS.has(toolName) || toolName === 'dsh_browser_restrict') return
|
|
128
128
|
const config = state.configRestrictedTo
|
|
129
129
|
if (config !== undefined && !config.includes(toolName)) {
|
|
130
130
|
throw new Error(`browser action "${toolName}" is restricted by the plugin configuration (allow-list: ${config.join(', ')})`)
|
|
@@ -146,6 +146,38 @@ function agentOf(exec: unknown): ExecAgent | undefined {
|
|
|
146
146
|
return (exec as { agent?: ExecAgent } | undefined)?.agent
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Refuse the bare DSH web root, which answers 401 and looks like a real page.
|
|
151
|
+
*
|
|
152
|
+
* The address comes from `DSH_WEB_URL`, which the harness exports to its children — NOT from a
|
|
153
|
+
* hardcoded port. This machine's GUI has run on 3080 and on 19387, so a number would stop matching
|
|
154
|
+
* exactly when the guard is needed, and the trap is not a port anyway: it is the SHAPE. A loopback
|
|
155
|
+
* address at its root with no token renders as an unauthenticated 401 page, which an agent reads
|
|
156
|
+
* as the site it meant to open.
|
|
157
|
+
* @param rawUrl - the address a caller asked to open.
|
|
158
|
+
*/
|
|
159
|
+
function assertNotBareDshWebUrl(rawUrl: string): void {
|
|
160
|
+
const advertised = process.env.DSH_WEB_URL
|
|
161
|
+
if (typeof advertised !== 'string' || advertised === '') return
|
|
162
|
+
let target: URL
|
|
163
|
+
let dsh: URL
|
|
164
|
+
try {
|
|
165
|
+
target = new URL(rawUrl)
|
|
166
|
+
dsh = new URL(advertised)
|
|
167
|
+
} catch {
|
|
168
|
+
return // not a URL this guard can judge; the provider's own admission is the next gate
|
|
169
|
+
}
|
|
170
|
+
if (target.origin !== dsh.origin) return
|
|
171
|
+
if (target.pathname !== '/') return
|
|
172
|
+
// The authenticated URL printed by `dsh web` carries a token; the bare root does not.
|
|
173
|
+
if (target.searchParams.has('token')) return
|
|
174
|
+
throw new Error(
|
|
175
|
+
'dsh_browser_open: refusing the bare DSH web root (' + dsh.origin + '/) because it carries no '
|
|
176
|
+
+ 'authentication token, so it answers with a 401 page that reads like a real site. Open the '
|
|
177
|
+
+ 'target site instead; to open DSH itself, use the authenticated URL printed by `dsh web`.',
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
149
181
|
/**
|
|
150
182
|
* Read a target's `index`, refusing the values that cannot mean anything.
|
|
151
183
|
*
|
|
@@ -282,17 +314,17 @@ function formatSnapshot(snapshot: {
|
|
|
282
314
|
export function apply(ctx: Context, config: Config = {}): void {
|
|
283
315
|
const timeoutMs = config.timeoutMs ?? 60_000
|
|
284
316
|
/**
|
|
285
|
-
* Longest wait `
|
|
317
|
+
* Longest wait `dsh_browser_wait` may poll for, leaving room for the tool call itself to
|
|
286
318
|
* finish and report.
|
|
287
319
|
*
|
|
288
|
-
* `
|
|
320
|
+
* `dsh_browser_wait` promises a VERDICT — `{ready:false, reason}` — and the enclosing tool
|
|
289
321
|
* call aborts at `timeoutMs`. A caller-supplied budget longer than that aborted the call
|
|
290
322
|
* instead, so the documented verdict never arrived. Clamping to just inside the budget is
|
|
291
323
|
* what keeps the promise: the polling stops, the reason is returned.
|
|
292
324
|
*/
|
|
293
325
|
const maxWaitMs = Math.max(250, timeoutMs - 5_000)
|
|
294
326
|
/**
|
|
295
|
-
* Most fields `
|
|
327
|
+
* Most fields `dsh_browser_fill` accepts in one batch.
|
|
296
328
|
*
|
|
297
329
|
* The batch runs as ONE page evaluation, so hundreds of fields do not produce hundreds of
|
|
298
330
|
* per-field verdicts — they produce a single evaluate that exceeds the tool budget and
|
|
@@ -314,12 +346,36 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
314
346
|
// Tool guidance band is 100-199; 150 keeps clear of the common 110/120
|
|
315
347
|
// tool sections so ordering does not depend on plugin load sequence.
|
|
316
348
|
order: 150,
|
|
317
|
-
text: 'Use the
|
|
349
|
+
text: 'Use the dsh_browser_* tools to operate the built-in browser. Each task gets its own browser session AND its own window (with a real toolbar: address bar, back/forward/reload, tab strip) — the human can see it, use it like any browser, and take over at any time; your tabs and history are isolated from other tasks, so do not assume another task\'s navigation state is visible to you. Understand a page with dsh_browser_a11y (semantic roles/names/states — the best structure map) or dsh_browser_snapshot (numbered interactive elements), then drive it: dsh_browser_click/dsh_browser_type accept a target {by: css|text|xpath, value} for semantic locating, or coordinates from dsh_browser_screenshot for visual targeting. For form filling prefer dsh_browser_fill (batch) or dsh_browser_set_value/dsh_browser_check/dsh_browser_select/dsh_browser_clear (single control, target-based); verify with dsh_browser_get_value. Use dsh_browser_scrape for structured extraction from list pages instead of hand-written dsh_browser_execute. After navigating on slow sites, dsh_browser_wait for the page. Keep the human informed of what you are doing on the page. If a snapshot or dsh_browser_challenge reports a human-verification challenge (CAPTCHA), stop retrying and ask the human to complete it in the browser window, then re-check.',
|
|
318
350
|
})
|
|
319
351
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Register one tool, turning a name collision into a warning instead of a dead apply.
|
|
354
|
+
*
|
|
355
|
+
* Registering the tools in sequence without this is how ONE taken name becomes thirty-two
|
|
356
|
+
* missing ones: the throw aborts the rest of the apply, and what the user sees is a plugin that
|
|
357
|
+
* half-exists. That is precisely the shape of the failure reported against a sibling plugin,
|
|
358
|
+
* where the first spec collided and all six vanished behind a `console.warn` a desktop user
|
|
359
|
+
* never sees.
|
|
360
|
+
*
|
|
361
|
+
* A name this plugin cannot have is worth a sentence in the log, not the toolset.
|
|
362
|
+
* @param definition - the tool to register.
|
|
363
|
+
*/
|
|
364
|
+
const registerSafely = (definition: ReturnType<typeof defineTool>): void => {
|
|
365
|
+
try {
|
|
366
|
+
ctx.tools.register(definition)
|
|
367
|
+
} catch (error) {
|
|
368
|
+
ctx.logger?.warn?.(
|
|
369
|
+
`dsh-builtin-browser: could not register ${definition.name} (${String(error)}). Another `
|
|
370
|
+
+ 'plugin has probably taken that name — browser tool names are a shared namespace. The '
|
|
371
|
+
+ 'rest of this plugin\'s tools are unaffected.',
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
registerSafely(defineTool({
|
|
377
|
+
name: 'dsh_browser_open',
|
|
378
|
+
description: 'Open a URL in the shared browser window. Opens this task\'s browser session on first use; optionally opens in a new tab. Returns the resulting page snapshot. A fresh session starts on a blank, uncommitted tab: call this with the REAL target URL before reading, because reading that tab returns an empty page instead of waiting. Never treat the bare DSH host (e.g. http://127.0.0.1:3080/) as the target website — it requires the authenticated URL printed by `dsh web`, and the bare root is refused outright.',
|
|
323
379
|
parameters: {
|
|
324
380
|
url: { type: 'string', required: true, description: 'The URL to open (HTTP/HTTPS).' },
|
|
325
381
|
newTab: { type: 'boolean', description: 'Open in a new tab instead of the active one.' },
|
|
@@ -335,7 +391,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
335
391
|
elements: {
|
|
336
392
|
type: 'array',
|
|
337
393
|
required: true,
|
|
338
|
-
// Same shared item schema as
|
|
394
|
+
// Same shared item schema as dsh_browser_open's, so the two cannot drift apart.
|
|
339
395
|
items: ELEMENT_ITEM_SCHEMA,
|
|
340
396
|
},
|
|
341
397
|
challenge: {
|
|
@@ -349,15 +405,16 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
349
405
|
},
|
|
350
406
|
},
|
|
351
407
|
},
|
|
352
|
-
//
|
|
353
|
-
// `coords` parameter, so coordinates stay off here (
|
|
408
|
+
// dsh_browser_open is an overview of what just loaded, not a locator: it has no
|
|
409
|
+
// `coords` parameter, so coordinates stay off here (dsh_browser_snapshot owns
|
|
354
410
|
// that switch for callers that actually target pixels).
|
|
355
411
|
render: (args, value) => [{ type: 'text', text: formatSnapshot(value, {}) }],
|
|
356
412
|
},
|
|
357
413
|
timeoutMs,
|
|
358
414
|
isConcurrencySafe: () => false, // opens tabs / navigates; exclusive within a task
|
|
359
415
|
async execute(args, exec) {
|
|
360
|
-
assertAllowed(state, '
|
|
416
|
+
assertAllowed(state, 'dsh_browser_open', taskKey(exec))
|
|
417
|
+
assertNotBareDshWebUrl(args.url)
|
|
361
418
|
const browser = ctx.get('browser')
|
|
362
419
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
363
420
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -376,9 +433,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
376
433
|
},
|
|
377
434
|
}))
|
|
378
435
|
|
|
379
|
-
|
|
380
|
-
name: '
|
|
381
|
-
description: 'Return an AI-friendly snapshot of the current shared-browser page: numbered interactive elements (inputs, buttons, links) the model can cite. This is the primary way to understand a page, and it needs no image input — every listed element can afterwards be addressed by a semantic target (
|
|
436
|
+
registerSafely(defineTool({
|
|
437
|
+
name: 'dsh_browser_snapshot',
|
|
438
|
+
description: 'Return an AI-friendly snapshot of the current shared-browser page: numbered interactive elements (inputs, buttons, links) the model can cite. This is the primary way to understand a page, and it needs no image input — every listed element can afterwards be addressed by a semantic target (dsh_browser_click, dsh_browser_type, dsh_browser_check, …) rather than by coordinates.',
|
|
382
439
|
parameters: {
|
|
383
440
|
coords: { type: 'boolean', description: 'Also print each element\'s viewport coordinates. Off by default; only useful when a vision pass is going to click a pixel position.' },
|
|
384
441
|
},
|
|
@@ -442,9 +499,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
442
499
|
},
|
|
443
500
|
}))
|
|
444
501
|
|
|
445
|
-
|
|
446
|
-
name: '
|
|
447
|
-
description: 'Read the page\'s accessibility tree: every interactive node with its semantic role (button/link/textbox/checkbox/…), accessible name, current value, and states (enabled/disabled/checked/expanded/…), (coordinates only when coords: true). Prefer this over
|
|
502
|
+
registerSafely(defineTool({
|
|
503
|
+
name: 'dsh_browser_a11y',
|
|
504
|
+
description: 'Read the page\'s accessibility tree: every interactive node with its semantic role (button/link/textbox/checkbox/…), accessible name, current value, and states (enabled/disabled/checked/expanded/…), (coordinates only when coords: true). Prefer this over dsh_browser_snapshot to understand a page\'s structure and find the right element: roles and names tell you WHAT each node is, and the name is what dsh_browser_click matches when you target by text. Penetrates same-origin iframes and shadow roots.',
|
|
448
505
|
parameters: {
|
|
449
506
|
includeHidden: { type: 'boolean', description: 'Include hidden elements (default false).' },
|
|
450
507
|
maxNodes: { type: 'number', description: 'Maximum nodes (default 150, range 10-5000).' },
|
|
@@ -527,9 +584,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
527
584
|
},
|
|
528
585
|
}))
|
|
529
586
|
|
|
530
|
-
|
|
531
|
-
name: '
|
|
532
|
-
description: 'Check whether a human-verification challenge (CAPTCHA / bot detection: Cloudflare "Just a moment", reCAPTCHA, hCaptcha, Turnstile) is blocking the current page. When blocked, do NOT keep retrying automated steps — ask the human to complete the verification in the shared browser window, then re-check with
|
|
587
|
+
registerSafely(defineTool({
|
|
588
|
+
name: 'dsh_browser_challenge',
|
|
589
|
+
description: 'Check whether a human-verification challenge (CAPTCHA / bot detection: Cloudflare "Just a moment", reCAPTCHA, hCaptcha, Turnstile) is blocking the current page. When blocked, do NOT keep retrying automated steps — ask the human to complete the verification in the shared browser window, then re-check with dsh_browser_snapshot.',
|
|
533
590
|
parameters: {},
|
|
534
591
|
output: {
|
|
535
592
|
schema: {
|
|
@@ -561,15 +618,15 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
561
618
|
...challenge.kind !== undefined ? { kind: challenge.kind } : {},
|
|
562
619
|
...challenge.reason !== undefined ? { reason: challenge.reason } : {},
|
|
563
620
|
hint: challenge.blocked
|
|
564
|
-
? 'Ask the human to complete the verification in the shared browser window (the page is visible to them), then re-check with
|
|
621
|
+
? 'Ask the human to complete the verification in the shared browser window (the page is visible to them), then re-check with dsh_browser_snapshot.'
|
|
565
622
|
: '',
|
|
566
623
|
}
|
|
567
624
|
},
|
|
568
625
|
}))
|
|
569
626
|
|
|
570
|
-
|
|
571
|
-
name: '
|
|
572
|
-
description: 'Wait until the shared-browser page is ready: load complete, and optionally the expected URL and/or a CSS selector present (top document or same-origin iframes). Use after
|
|
627
|
+
registerSafely(defineTool({
|
|
628
|
+
name: 'dsh_browser_wait',
|
|
629
|
+
description: 'Wait until the shared-browser page is ready: load complete, and optionally the expected URL and/or a CSS selector present (top document or same-origin iframes). Use after dsh_browser_open on slow sites instead of snapshotting a white page — wait for the URL you navigated to first. Returns ready=true/false with a short reason; a miss is not an error.',
|
|
573
630
|
parameters: {
|
|
574
631
|
timeoutMs: { type: 'number', description: 'Maximum wait in ms (default 30000).' },
|
|
575
632
|
url: { type: 'string', description: 'Expected page URL (exact or prefix), e.g. the URL you opened.' },
|
|
@@ -603,8 +660,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
603
660
|
},
|
|
604
661
|
}))
|
|
605
662
|
|
|
606
|
-
|
|
607
|
-
name: '
|
|
663
|
+
registerSafely(defineTool({
|
|
664
|
+
name: 'dsh_browser_execute',
|
|
608
665
|
description: 'Execute JavaScript in the shared-browser page context. This is the primary way to interact with page elements: focus, fill inputs (use the native value setter for framework-controlled inputs, then dispatch an input event), click buttons (element.click() or a constructed MouseEvent). Returns the evaluation result by value, or the exception text.',
|
|
609
666
|
parameters: {
|
|
610
667
|
script: { type: 'string', required: true, description: 'The JavaScript expression to evaluate in the page context.' },
|
|
@@ -628,7 +685,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
628
685
|
const rendered = String(value.value ?? '')
|
|
629
686
|
const cap = 50_000
|
|
630
687
|
const text = rendered.length > cap
|
|
631
|
-
? `Result: ${rendered.slice(0, cap)}\n(result truncated at ${cap} of ${rendered.length} characters — return a smaller value, or write it with
|
|
688
|
+
? `Result: ${rendered.slice(0, cap)}\n(result truncated at ${cap} of ${rendered.length} characters — return a smaller value, or write it with dsh_browser_execute + a variable)`
|
|
632
689
|
: `Result: ${rendered}`
|
|
633
690
|
return [{ type: 'text', text }]
|
|
634
691
|
},
|
|
@@ -636,7 +693,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
636
693
|
timeoutMs,
|
|
637
694
|
isConcurrencySafe: () => false, // page JS can be stateful
|
|
638
695
|
async execute(args, exec) {
|
|
639
|
-
assertAllowed(state, '
|
|
696
|
+
assertAllowed(state, 'dsh_browser_execute', taskKey(exec))
|
|
640
697
|
const browser = ctx.get('browser')
|
|
641
698
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
642
699
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -653,8 +710,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
653
710
|
},
|
|
654
711
|
}))
|
|
655
712
|
|
|
656
|
-
|
|
657
|
-
name: '
|
|
713
|
+
registerSafely(defineTool({
|
|
714
|
+
name: 'dsh_browser_content',
|
|
658
715
|
description: 'Fetch the current shared-browser page content in a chosen format: html (raw DOM), markdown (structured reading), txt (plain text), or json. Optionally scope to a CSS selector and cap the length. Use this to read page content, not to interact.',
|
|
659
716
|
parameters: {
|
|
660
717
|
format: { type: 'string', required: true, enum: ['html', 'markdown', 'txt', 'json'], description: 'Output format.' },
|
|
@@ -689,9 +746,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
689
746
|
},
|
|
690
747
|
}))
|
|
691
748
|
|
|
692
|
-
|
|
693
|
-
name: '
|
|
694
|
-
description: 'Extract structured data from a page: give the container CSS selector (item) and a field map (name -> selector, optionally selector@attr to take an attribute; a@href yields the absolute URL). Returns one object per item. Static CSS queries only — no arbitrary code runs — so it is safe on any page. Use for list pages (search results, cards, tables) instead of hand-writing
|
|
749
|
+
registerSafely(defineTool({
|
|
750
|
+
name: 'dsh_browser_scrape',
|
|
751
|
+
description: 'Extract structured data from a page: give the container CSS selector (item) and a field map (name -> selector, optionally selector@attr to take an attribute; a@href yields the absolute URL). Returns one object per item. Static CSS queries only — no arbitrary code runs — so it is safe on any page. Use for list pages (search results, cards, tables) instead of hand-writing dsh_browser_execute.',
|
|
695
752
|
parameters: {
|
|
696
753
|
item: { type: 'string', required: true, description: 'CSS selector of each result container (e.g. "div.card").' },
|
|
697
754
|
fields: {
|
|
@@ -726,7 +783,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
726
783
|
timeoutMs,
|
|
727
784
|
isConcurrencySafe: () => true,
|
|
728
785
|
async execute(args, exec) {
|
|
729
|
-
assertAllowed(state, '
|
|
786
|
+
assertAllowed(state, 'dsh_browser_scrape', taskKey(exec))
|
|
730
787
|
const browser = ctx.get('browser')
|
|
731
788
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
732
789
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -740,9 +797,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
740
797
|
},
|
|
741
798
|
}))
|
|
742
799
|
|
|
743
|
-
|
|
744
|
-
name: '
|
|
745
|
-
description: 'Click in the shared browser. Prefer the semantic target: pass target {by: css|text|xpath, value, index?} and the element is located from the DOM, scrolled into view and clicked at its centre — this needs no image input and is the only form that works without vision. The alternative is viewport coordinates (x/y), which are meaningful only after a vision model has located the element on a
|
|
800
|
+
registerSafely(defineTool({
|
|
801
|
+
name: 'dsh_browser_click',
|
|
802
|
+
description: 'Click in the shared browser. Prefer the semantic target: pass target {by: css|text|xpath, value, index?} and the element is located from the DOM, scrolled into view and clicked at its centre — this needs no image input and is the only form that works without vision. The alternative is viewport coordinates (x/y), which are meaningful only after a vision model has located the element on a dsh_browser_screenshot; coordinate clicks are refused outright under the non-visual strategy. Provide exactly one of target or x/y.',
|
|
746
803
|
parameters: {
|
|
747
804
|
target: {
|
|
748
805
|
type: 'object',
|
|
@@ -754,7 +811,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
754
811
|
},
|
|
755
812
|
description: 'Locate the element semantically and click it (css/text/xpath).',
|
|
756
813
|
},
|
|
757
|
-
x: { type: 'number', description: 'Viewport x coordinate (CSS px), taken from a
|
|
814
|
+
x: { type: 'number', description: 'Viewport x coordinate (CSS px), taken from a dsh_browser_screenshot read by a vision model. Prefer a semantic target — it works without image input.' },
|
|
758
815
|
y: { type: 'number', description: 'Viewport y coordinate (CSS px), taken from the same screenshot as x.' },
|
|
759
816
|
},
|
|
760
817
|
output: {
|
|
@@ -764,7 +821,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
764
821
|
timeoutMs,
|
|
765
822
|
isConcurrencySafe: () => false,
|
|
766
823
|
async execute(args, exec) {
|
|
767
|
-
assertAllowed(state, '
|
|
824
|
+
assertAllowed(state, 'dsh_browser_click', taskKey(exec))
|
|
768
825
|
const browser = ctx.get('browser')
|
|
769
826
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
770
827
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -786,7 +843,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
786
843
|
}, exec.signal)
|
|
787
844
|
} else {
|
|
788
845
|
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
|
|
789
|
-
throw new Error('
|
|
846
|
+
throw new Error('dsh_browser_click: provide a target (css/text/xpath) or x/y coordinates')
|
|
790
847
|
}
|
|
791
848
|
await browser.click(session, { x: args.x, y: args.y }, exec.signal)
|
|
792
849
|
}
|
|
@@ -794,9 +851,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
794
851
|
},
|
|
795
852
|
}))
|
|
796
853
|
|
|
797
|
-
|
|
798
|
-
name: '
|
|
799
|
-
description: 'Type text into the focused element of the shared browser, or into a located element (pass target {by: css|text|xpath, value, index?} to focus it first). Text is inserted at the focus via CDP Input.insertText. For setting whole field values (and React-controlled inputs) prefer
|
|
854
|
+
registerSafely(defineTool({
|
|
855
|
+
name: 'dsh_browser_type',
|
|
856
|
+
description: 'Type text into the focused element of the shared browser, or into a located element (pass target {by: css|text|xpath, value, index?} to focus it first). Text is inserted at the focus via CDP Input.insertText. For setting whole field values (and React-controlled inputs) prefer dsh_browser_set_value; use dsh_browser_key for Enter/Tab/arrows.',
|
|
800
857
|
parameters: {
|
|
801
858
|
text: { type: 'string', required: true, description: 'The text to insert.' },
|
|
802
859
|
target: {
|
|
@@ -817,7 +874,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
817
874
|
timeoutMs,
|
|
818
875
|
isConcurrencySafe: () => false,
|
|
819
876
|
async execute(args, exec) {
|
|
820
|
-
assertAllowed(state, '
|
|
877
|
+
assertAllowed(state, 'dsh_browser_type', taskKey(exec))
|
|
821
878
|
const browser = ctx.get('browser')
|
|
822
879
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
823
880
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -845,8 +902,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
845
902
|
},
|
|
846
903
|
}))
|
|
847
904
|
|
|
848
|
-
|
|
849
|
-
name: '
|
|
905
|
+
registerSafely(defineTool({
|
|
906
|
+
name: 'dsh_browser_scroll',
|
|
850
907
|
description: 'Scroll the shared-browser page: by pixel deltas (deltaX/deltaY), to a CSS selector\'s element, or to the top/bottom. Use to reveal below-the-fold content before snapshotting or clicking.',
|
|
851
908
|
parameters: {
|
|
852
909
|
deltaX: { type: 'number', description: 'Horizontal scroll delta in CSS pixels.' },
|
|
@@ -862,7 +919,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
862
919
|
timeoutMs,
|
|
863
920
|
isConcurrencySafe: () => false, // mutates page scroll state; exclusive within a task
|
|
864
921
|
async execute(args, exec) {
|
|
865
|
-
assertAllowed(state, '
|
|
922
|
+
assertAllowed(state, 'dsh_browser_scroll', taskKey(exec))
|
|
866
923
|
const browser = ctx.get('browser')
|
|
867
924
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
868
925
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -877,8 +934,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
877
934
|
},
|
|
878
935
|
}))
|
|
879
936
|
|
|
880
|
-
|
|
881
|
-
name: '
|
|
937
|
+
registerSafely(defineTool({
|
|
938
|
+
name: 'dsh_browser_back',
|
|
882
939
|
description: 'Go back one step in the shared-browser page history. A no-op when there is no previous entry.',
|
|
883
940
|
parameters: {},
|
|
884
941
|
output: {
|
|
@@ -888,7 +945,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
888
945
|
timeoutMs,
|
|
889
946
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
890
947
|
async execute(_args, exec) {
|
|
891
|
-
assertAllowed(state, '
|
|
948
|
+
assertAllowed(state, 'dsh_browser_back', taskKey(exec))
|
|
892
949
|
const browser = ctx.get('browser')
|
|
893
950
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
894
951
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -897,8 +954,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
897
954
|
},
|
|
898
955
|
}))
|
|
899
956
|
|
|
900
|
-
|
|
901
|
-
name: '
|
|
957
|
+
registerSafely(defineTool({
|
|
958
|
+
name: 'dsh_browser_forward',
|
|
902
959
|
description: 'Go forward one step in the shared-browser page history. A no-op when there is no next entry.',
|
|
903
960
|
parameters: {},
|
|
904
961
|
output: {
|
|
@@ -908,7 +965,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
908
965
|
timeoutMs,
|
|
909
966
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
910
967
|
async execute(_args, exec) {
|
|
911
|
-
assertAllowed(state, '
|
|
968
|
+
assertAllowed(state, 'dsh_browser_forward', taskKey(exec))
|
|
912
969
|
const browser = ctx.get('browser')
|
|
913
970
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
914
971
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -917,8 +974,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
917
974
|
},
|
|
918
975
|
}))
|
|
919
976
|
|
|
920
|
-
|
|
921
|
-
name: '
|
|
977
|
+
registerSafely(defineTool({
|
|
978
|
+
name: 'dsh_browser_refresh',
|
|
922
979
|
description: 'Reload the current page in the shared browser (like a browser\'s refresh button). Use after a page got stuck, to apply script changes, or to re-fetch a page.',
|
|
923
980
|
parameters: {},
|
|
924
981
|
output: {
|
|
@@ -928,7 +985,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
928
985
|
timeoutMs,
|
|
929
986
|
isConcurrencySafe: () => false, // reloads the page; exclusive within a task
|
|
930
987
|
async execute(_args, exec) {
|
|
931
|
-
assertAllowed(state, '
|
|
988
|
+
assertAllowed(state, 'dsh_browser_refresh', taskKey(exec))
|
|
932
989
|
const browser = ctx.get('browser')
|
|
933
990
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
934
991
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -937,8 +994,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
937
994
|
},
|
|
938
995
|
}))
|
|
939
996
|
|
|
940
|
-
|
|
941
|
-
name: '
|
|
997
|
+
registerSafely(defineTool({
|
|
998
|
+
name: 'dsh_browser_key',
|
|
942
999
|
description: 'Press one named key in the shared-browser page (Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right, Home, End, PageUp, PageDown, Space). Use after focusing an input to submit a chat box (Enter), move focus (Tab), or navigate a list (arrows).',
|
|
943
1000
|
parameters: {
|
|
944
1001
|
key: { type: 'string', required: true, enum: ['Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Space'], description: 'The key to press.' },
|
|
@@ -950,7 +1007,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
950
1007
|
timeoutMs,
|
|
951
1008
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
952
1009
|
async execute(args, exec) {
|
|
953
|
-
assertAllowed(state, '
|
|
1010
|
+
assertAllowed(state, 'dsh_browser_key', taskKey(exec))
|
|
954
1011
|
const browser = ctx.get('browser')
|
|
955
1012
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
956
1013
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -959,9 +1016,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
959
1016
|
},
|
|
960
1017
|
}))
|
|
961
1018
|
|
|
962
|
-
|
|
963
|
-
name: '
|
|
964
|
-
description: 'Fill a form in one batch: pass fields with a CSS selector or name/label/placeholder text and the value to set (string, number, or boolean for checkbox/radio; for selects or radio groups pass the option value or visible text). Values are applied with the native setter plus input/change events, so React/Vue controlled inputs update correctly. Optionally submit the containing form. Prefer this over hand-written
|
|
1019
|
+
registerSafely(defineTool({
|
|
1020
|
+
name: 'dsh_browser_fill',
|
|
1021
|
+
description: 'Fill a form in one batch: pass fields with a CSS selector or name/label/placeholder text and the value to set (string, number, or boolean for checkbox/radio; for selects or radio groups pass the option value or visible text). Values are applied with the native setter plus input/change events, so React/Vue controlled inputs update correctly. Optionally submit the containing form. Prefer this over hand-written dsh_browser_execute for form filling; per-field failures are reported instead of throwing.',
|
|
965
1022
|
parameters: {
|
|
966
1023
|
fields: {
|
|
967
1024
|
type: 'array',
|
|
@@ -1019,13 +1076,13 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1019
1076
|
timeoutMs,
|
|
1020
1077
|
isConcurrencySafe: () => false,
|
|
1021
1078
|
async execute(args, exec) {
|
|
1022
|
-
assertAllowed(state, '
|
|
1079
|
+
assertAllowed(state, 'dsh_browser_fill', taskKey(exec))
|
|
1023
1080
|
const browser = ctx.get('browser')
|
|
1024
1081
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1025
1082
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
1026
1083
|
const requested = args.fields ?? []
|
|
1027
1084
|
if (requested.length > maxFillFields) {
|
|
1028
|
-
throw new Error(`
|
|
1085
|
+
throw new Error(`dsh_browser_fill: ${requested.length} fields in one call exceeds the ${maxFillFields}-field limit; split it into several calls so each one can report per-field results instead of timing out as a batch`)
|
|
1029
1086
|
}
|
|
1030
1087
|
const fields = requested.map((f: { selector?: string; name?: string; label?: string; placeholder?: string; kind?: string; value?: string }) => {
|
|
1031
1088
|
// A missing value is a caller mistake, not a request to clear the field. Turning
|
|
@@ -1034,7 +1091,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1034
1091
|
// a call can still arrive without it.
|
|
1035
1092
|
if (f.value === undefined) {
|
|
1036
1093
|
const matcher = f.selector ?? f.name ?? f.label ?? f.placeholder ?? '(no matcher)'
|
|
1037
|
-
throw new Error(`
|
|
1094
|
+
throw new Error(`dsh_browser_fill: field "${matcher}" has no "value"; pass one (use "false" to uncheck, or "" to clear deliberately)`)
|
|
1038
1095
|
}
|
|
1039
1096
|
return {
|
|
1040
1097
|
...f.selector !== undefined ? { selector: f.selector } : {},
|
|
@@ -1071,9 +1128,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1071
1128
|
description: 'Locate the element (css/text/xpath).',
|
|
1072
1129
|
}
|
|
1073
1130
|
|
|
1074
|
-
|
|
1075
|
-
name: '
|
|
1076
|
-
description: 'Set the value of ONE input/textarea/select/contenteditable, located by css/text/xpath. Uses the native setter plus input/change events, so React/Vue controlled inputs update correctly. For selects pass the option value or visible text. For a whole form at once prefer
|
|
1131
|
+
registerSafely(defineTool({
|
|
1132
|
+
name: 'dsh_browser_set_value',
|
|
1133
|
+
description: 'Set the value of ONE input/textarea/select/contenteditable, located by css/text/xpath. Uses the native setter plus input/change events, so React/Vue controlled inputs update correctly. For selects pass the option value or visible text. For a whole form at once prefer dsh_browser_fill.',
|
|
1077
1134
|
parameters: {
|
|
1078
1135
|
target: targetParam,
|
|
1079
1136
|
value: { type: 'string', required: true, description: 'The value to set (string form; numbers/booleans accepted).' },
|
|
@@ -1092,7 +1149,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1092
1149
|
timeoutMs,
|
|
1093
1150
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1094
1151
|
async execute(args, exec) {
|
|
1095
|
-
assertAllowed(state, '
|
|
1152
|
+
assertAllowed(state, 'dsh_browser_set_value', taskKey(exec))
|
|
1096
1153
|
const browser = ctx.get('browser')
|
|
1097
1154
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1098
1155
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1109,8 +1166,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1109
1166
|
},
|
|
1110
1167
|
}))
|
|
1111
1168
|
|
|
1112
|
-
|
|
1113
|
-
name: '
|
|
1169
|
+
registerSafely(defineTool({
|
|
1170
|
+
name: 'dsh_browser_check',
|
|
1114
1171
|
description: 'Check (or uncheck, with checked=false) a checkbox or radio button, located by css/text/xpath.',
|
|
1115
1172
|
parameters: {
|
|
1116
1173
|
target: targetParam,
|
|
@@ -1123,7 +1180,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1123
1180
|
timeoutMs,
|
|
1124
1181
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1125
1182
|
async execute(args, exec) {
|
|
1126
|
-
assertAllowed(state, '
|
|
1183
|
+
assertAllowed(state, 'dsh_browser_check', taskKey(exec))
|
|
1127
1184
|
const browser = ctx.get('browser')
|
|
1128
1185
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1129
1186
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1140,8 +1197,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1140
1197
|
},
|
|
1141
1198
|
}))
|
|
1142
1199
|
|
|
1143
|
-
|
|
1144
|
-
name: '
|
|
1200
|
+
registerSafely(defineTool({
|
|
1201
|
+
name: 'dsh_browser_select',
|
|
1145
1202
|
description: 'Select one option of a <select>, located by css/text/xpath — by option value, visible text, or 0-based index (provide exactly one).',
|
|
1146
1203
|
parameters: {
|
|
1147
1204
|
target: targetParam,
|
|
@@ -1163,7 +1220,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1163
1220
|
timeoutMs,
|
|
1164
1221
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1165
1222
|
async execute(args, exec) {
|
|
1166
|
-
assertAllowed(state, '
|
|
1223
|
+
assertAllowed(state, 'dsh_browser_select', taskKey(exec))
|
|
1167
1224
|
const browser = ctx.get('browser')
|
|
1168
1225
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1169
1226
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1182,8 +1239,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1182
1239
|
},
|
|
1183
1240
|
}))
|
|
1184
1241
|
|
|
1185
|
-
|
|
1186
|
-
name: '
|
|
1242
|
+
registerSafely(defineTool({
|
|
1243
|
+
name: 'dsh_browser_clear',
|
|
1187
1244
|
description: 'Clear an input/textarea/contenteditable, or uncheck a checkbox/radio, located by css/text/xpath.',
|
|
1188
1245
|
parameters: {
|
|
1189
1246
|
target: targetParam,
|
|
@@ -1195,7 +1252,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1195
1252
|
timeoutMs,
|
|
1196
1253
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1197
1254
|
async execute(args, exec) {
|
|
1198
|
-
assertAllowed(state, '
|
|
1255
|
+
assertAllowed(state, 'dsh_browser_clear', taskKey(exec))
|
|
1199
1256
|
const browser = ctx.get('browser')
|
|
1200
1257
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1201
1258
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1211,8 +1268,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1211
1268
|
},
|
|
1212
1269
|
}))
|
|
1213
1270
|
|
|
1214
|
-
|
|
1215
|
-
name: '
|
|
1271
|
+
registerSafely(defineTool({
|
|
1272
|
+
name: 'dsh_browser_get_value',
|
|
1216
1273
|
description: 'Read the current value of one input/textarea/select/contenteditable, located by css/text/xpath. Use to VERIFY that a fill worked (e.g. before submitting).',
|
|
1217
1274
|
parameters: {
|
|
1218
1275
|
target: targetParam,
|
|
@@ -1236,7 +1293,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1236
1293
|
timeoutMs,
|
|
1237
1294
|
isConcurrencySafe: () => true,
|
|
1238
1295
|
async execute(args, exec) {
|
|
1239
|
-
assertAllowed(state, '
|
|
1296
|
+
assertAllowed(state, 'dsh_browser_get_value', taskKey(exec))
|
|
1240
1297
|
const browser = ctx.get('browser')
|
|
1241
1298
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1242
1299
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1256,9 +1313,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1256
1313
|
},
|
|
1257
1314
|
}))
|
|
1258
1315
|
|
|
1259
|
-
|
|
1260
|
-
name: '
|
|
1261
|
-
description: 'Capture the current shared-browser page as a screenshot (PNG default, JPEG optional). This is for models that can read images: layout checks, charts, designs, CAPTCHAs, or locating an element by eye before clicking its coordinates. A model without image input gains nothing from it —
|
|
1316
|
+
registerSafely(defineTool({
|
|
1317
|
+
name: 'dsh_browser_screenshot',
|
|
1318
|
+
description: 'Capture the current shared-browser page as a screenshot (PNG default, JPEG optional). This is for models that can read images: layout checks, charts, designs, CAPTCHAs, or locating an element by eye before clicking its coordinates. A model without image input gains nothing from it — dsh_browser_snapshot, dsh_browser_a11y and dsh_browser_content carry the same page as text, and dsh_browser_scrape extracts structured data. Supports full-page capture, save-to-file, JPEG encoding, and downscaling (maxWidth/maxHeight) to cut vision-tool token cost. JPEG needs a browser whose CDP encoder works — the self-hosted browser and an installed Chrome/Edge; some carriers (the desktop sidebar) return PNG instead. Downscaling works on every carrier.',
|
|
1262
1319
|
parameters: {
|
|
1263
1320
|
fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
|
|
1264
1321
|
savePath: { type: 'string', description: 'Absolute file path to also save the image to (e.g. for read_image vision location). Must resolve inside the configured downloadDir (default: the system Downloads folder, localized names such as ~/下载 included); an existing file is never overwritten.' },
|
|
@@ -1308,8 +1365,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1308
1365
|
}))
|
|
1309
1366
|
|
|
1310
1367
|
if (config.tabTools !== false) {
|
|
1311
|
-
|
|
1312
|
-
name: '
|
|
1368
|
+
registerSafely(defineTool({
|
|
1369
|
+
name: 'dsh_browser_list_tabs',
|
|
1313
1370
|
description: 'List the shared-browser session\'s tabs with their URLs and which is active.',
|
|
1314
1371
|
parameters: {},
|
|
1315
1372
|
output: {
|
|
@@ -1349,9 +1406,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1349
1406
|
},
|
|
1350
1407
|
}))
|
|
1351
1408
|
|
|
1352
|
-
|
|
1353
|
-
name: '
|
|
1354
|
-
description: 'Switch the shared browser to a tab by id (from
|
|
1409
|
+
registerSafely(defineTool({
|
|
1410
|
+
name: 'dsh_browser_switch_tab',
|
|
1411
|
+
description: 'Switch the shared browser to a tab by id (from dsh_browser_list_tabs).',
|
|
1355
1412
|
parameters: {
|
|
1356
1413
|
tabId: { type: 'string', required: true, description: 'The tab id to switch to.' },
|
|
1357
1414
|
},
|
|
@@ -1362,7 +1419,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1362
1419
|
timeoutMs,
|
|
1363
1420
|
isConcurrencySafe: () => false, // mutates the active tab; exclusive within a task
|
|
1364
1421
|
async execute(args, exec) {
|
|
1365
|
-
assertAllowed(state, '
|
|
1422
|
+
assertAllowed(state, 'dsh_browser_switch_tab', taskKey(exec))
|
|
1366
1423
|
const browser = ctx.get('browser')
|
|
1367
1424
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1368
1425
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1371,8 +1428,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1371
1428
|
},
|
|
1372
1429
|
}))
|
|
1373
1430
|
|
|
1374
|
-
|
|
1375
|
-
name: '
|
|
1431
|
+
registerSafely(defineTool({
|
|
1432
|
+
name: 'dsh_browser_close_tab',
|
|
1376
1433
|
description: 'Close a tab in the shared browser by id. Closing the active tab activates the next.',
|
|
1377
1434
|
parameters: {
|
|
1378
1435
|
tabId: { type: 'string', required: true, description: 'The tab id to close.' },
|
|
@@ -1384,7 +1441,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1384
1441
|
timeoutMs,
|
|
1385
1442
|
isConcurrencySafe: () => false, // mutates the tab list; exclusive within a task
|
|
1386
1443
|
async execute(args, exec) {
|
|
1387
|
-
assertAllowed(state, '
|
|
1444
|
+
assertAllowed(state, 'dsh_browser_close_tab', taskKey(exec))
|
|
1388
1445
|
const browser = ctx.get('browser')
|
|
1389
1446
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1390
1447
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1393,8 +1450,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1393
1450
|
},
|
|
1394
1451
|
}))
|
|
1395
1452
|
|
|
1396
|
-
|
|
1397
|
-
name: '
|
|
1453
|
+
registerSafely(defineTool({
|
|
1454
|
+
name: 'dsh_browser_reset',
|
|
1398
1455
|
description: 'Close every tab in the shared browser and start fresh with one blank tab.',
|
|
1399
1456
|
parameters: {},
|
|
1400
1457
|
output: {
|
|
@@ -1404,7 +1461,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1404
1461
|
timeoutMs,
|
|
1405
1462
|
isConcurrencySafe: () => false, // closes every tab; exclusive within a task
|
|
1406
1463
|
async execute(_args, exec) {
|
|
1407
|
-
assertAllowed(state, '
|
|
1464
|
+
assertAllowed(state, 'dsh_browser_reset', taskKey(exec))
|
|
1408
1465
|
const browser = ctx.get('browser')
|
|
1409
1466
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1410
1467
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1414,8 +1471,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1414
1471
|
}))
|
|
1415
1472
|
}
|
|
1416
1473
|
|
|
1417
|
-
|
|
1418
|
-
name: '
|
|
1474
|
+
registerSafely(defineTool({
|
|
1475
|
+
name: 'dsh_browser_history',
|
|
1419
1476
|
description: 'List the shared browser session\'s recorded operation history (navigate/execute/click/type), newest last, with per-step success/error. Use to understand what the agent did and to pick a step to replay.',
|
|
1420
1477
|
parameters: {
|
|
1421
1478
|
verbose: { type: 'boolean', description: "Include each operation's parameters (default false). They are what you just sent, so they are omitted unless asked for." },
|
|
@@ -1511,9 +1568,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1511
1568
|
},
|
|
1512
1569
|
}))
|
|
1513
1570
|
|
|
1514
|
-
|
|
1515
|
-
name: '
|
|
1516
|
-
description: 'List pages the shared browser has visited, newest first, from the PERSISTENT browsing history: it survives closing the browser and restarting DSH, so it is how you (or the human) find a page again afterwards. Reopen one by passing its url to
|
|
1571
|
+
registerSafely(defineTool({
|
|
1572
|
+
name: 'dsh_browser_visited',
|
|
1573
|
+
description: 'List pages the shared browser has visited, newest first, from the PERSISTENT browsing history: it survives closing the browser and restarting DSH, so it is how you (or the human) find a page again afterwards. Reopen one by passing its url to dsh_browser_open. Not to be confused with dsh_browser_history, which is only the current session\'s operation log (navigate/click/type) and disappears with the session.',
|
|
1517
1574
|
parameters: {
|
|
1518
1575
|
limit: { type: 'number', description: 'Maximum entries to return (default 30, capped at 200).' },
|
|
1519
1576
|
domain: { type: 'string', description: 'Only visits whose hostname contains this text (case-insensitive).' },
|
|
@@ -1590,9 +1647,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1590
1647
|
},
|
|
1591
1648
|
}))
|
|
1592
1649
|
|
|
1593
|
-
|
|
1594
|
-
name: '
|
|
1595
|
-
description: 'Replay one recorded browser operation by its history sequence number (from
|
|
1650
|
+
registerSafely(defineTool({
|
|
1651
|
+
name: 'dsh_browser_replay',
|
|
1652
|
+
description: 'Replay one recorded browser operation by its history sequence number (from dsh_browser_history). Navigate/click/type are re-issued against the current page; execute re-runs its script. The replayed step is appended to history as a new entry.',
|
|
1596
1653
|
parameters: {
|
|
1597
1654
|
seq: { type: 'number', required: true, description: 'The history entry sequence number to replay.' },
|
|
1598
1655
|
},
|
|
@@ -1603,7 +1660,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1603
1660
|
timeoutMs,
|
|
1604
1661
|
isConcurrencySafe: () => false,
|
|
1605
1662
|
async execute(args, exec) {
|
|
1606
|
-
assertAllowed(state, '
|
|
1663
|
+
assertAllowed(state, 'dsh_browser_replay', taskKey(exec))
|
|
1607
1664
|
const browser = ctx.get('browser')
|
|
1608
1665
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1609
1666
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1612,8 +1669,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1612
1669
|
},
|
|
1613
1670
|
}))
|
|
1614
1671
|
|
|
1615
|
-
|
|
1616
|
-
name: '
|
|
1672
|
+
registerSafely(defineTool({
|
|
1673
|
+
name: 'dsh_browser_download',
|
|
1617
1674
|
description: 'Download a URL to a local file, keeping the browser session\'s cookies and login state. Use for fetching files behind authentication or from the current page context. Available on the self-hosted browser; the desktop shell delegates downloads to the real browser UI.',
|
|
1618
1675
|
parameters: {
|
|
1619
1676
|
url: { type: 'string', required: true, description: 'The URL to download.' },
|
|
@@ -1626,7 +1683,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1626
1683
|
timeoutMs,
|
|
1627
1684
|
isConcurrencySafe: () => false,
|
|
1628
1685
|
async execute(args, exec) {
|
|
1629
|
-
assertAllowed(state, '
|
|
1686
|
+
assertAllowed(state, 'dsh_browser_download', taskKey(exec))
|
|
1630
1687
|
const browser = ctx.get('browser')
|
|
1631
1688
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1632
1689
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1635,8 +1692,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1635
1692
|
},
|
|
1636
1693
|
}))
|
|
1637
1694
|
|
|
1638
|
-
|
|
1639
|
-
name: '
|
|
1695
|
+
registerSafely(defineTool({
|
|
1696
|
+
name: 'dsh_browser_session',
|
|
1640
1697
|
description: 'Show THIS task\'s browser session: its id and open tabs. Each task (DSH session) has its own browser session, so this reflects what your task drives. The window is shared with the human and other tasks, but tab sets and history are isolated per task.',
|
|
1641
1698
|
parameters: {},
|
|
1642
1699
|
output: {
|
|
@@ -1676,9 +1733,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1676
1733
|
},
|
|
1677
1734
|
}))
|
|
1678
1735
|
|
|
1679
|
-
|
|
1680
|
-
name: '
|
|
1681
|
-
description: 'Reset THIS task\'s browser session: close it entirely so the next
|
|
1736
|
+
registerSafely(defineTool({
|
|
1737
|
+
name: 'dsh_browser_reset_session',
|
|
1738
|
+
description: 'Reset THIS task\'s browser session: close it entirely so the next dsh_browser_* call starts a fresh session with one blank tab. Other tasks\' sessions are untouched. Use when a session is in a bad state or you want a clean slate.',
|
|
1682
1739
|
parameters: {},
|
|
1683
1740
|
output: {
|
|
1684
1741
|
schema: { type: 'object', additionalProperties: false, properties: { reset: { type: 'boolean', required: true } } },
|
|
@@ -1687,7 +1744,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1687
1744
|
timeoutMs,
|
|
1688
1745
|
isConcurrencySafe: () => false, // closes the whole session; exclusive within a task
|
|
1689
1746
|
async execute(_args, exec) {
|
|
1690
|
-
assertAllowed(state, '
|
|
1747
|
+
assertAllowed(state, 'dsh_browser_reset_session', taskKey(exec))
|
|
1691
1748
|
const browser = ctx.get('browser')
|
|
1692
1749
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1693
1750
|
const key = taskKey(exec)
|
|
@@ -1705,9 +1762,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1705
1762
|
},
|
|
1706
1763
|
}))
|
|
1707
1764
|
|
|
1708
|
-
|
|
1709
|
-
name: '
|
|
1710
|
-
description: 'Restrict which browser actions are allowed, to prevent stray clicks/navigation. Pass a list of browser tool names (e.g. ["
|
|
1765
|
+
registerSafely(defineTool({
|
|
1766
|
+
name: 'dsh_browser_restrict',
|
|
1767
|
+
description: 'Restrict which browser actions are allowed, to prevent stray clicks/navigation. Pass a list of browser tool names (e.g. ["dsh_browser_snapshot","dsh_browser_content","dsh_browser_click"]) — any other dsh_browser_* call is refused. Pass an empty list or omit to lift the restriction. Never blocked, whatever the list says: the observing tools (snapshot/a11y/content/scrape/screenshot/get_value/wait/challenge/list_tabs/session/history/visited) and the three that lift a restriction or recover from a bad state (restrict/reset/reset_session) — so a restriction can always be read around and undone. IMPORTANT: this is a SOFT guardrail against accidental actions, NOT a security boundary — you (the model) can lift it yourself with an empty list.',
|
|
1711
1768
|
parameters: {
|
|
1712
1769
|
allowed: {
|
|
1713
1770
|
type: 'array',
|
|
@@ -1724,9 +1781,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1724
1781
|
async execute(args, exec) {
|
|
1725
1782
|
// Always allowed so the guard can be lifted.
|
|
1726
1783
|
const allowed = args.allowed ?? []
|
|
1727
|
-
const unknown = allowed.filter((t: string) => !t.startsWith('
|
|
1784
|
+
const unknown = allowed.filter((t: string) => !t.startsWith('dsh_browser_'))
|
|
1728
1785
|
if (unknown.length > 0) {
|
|
1729
|
-
throw new Error(`
|
|
1786
|
+
throw new Error(`dsh_browser_restrict: unknown tool name(s) ${unknown.map(t => `"${t}"`).join(', ')} (must start with "dsh_browser_")`)
|
|
1730
1787
|
}
|
|
1731
1788
|
// Empty list (or omitted) lifts this task's restriction; a non-empty list is its
|
|
1732
1789
|
// new allow-list. Per task, not per plugin: this state object is shared by every
|
|
@@ -1739,8 +1796,8 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1739
1796
|
},
|
|
1740
1797
|
}))
|
|
1741
1798
|
|
|
1742
|
-
|
|
1743
|
-
name: '
|
|
1799
|
+
registerSafely(defineTool({
|
|
1800
|
+
name: 'dsh_browser_auth',
|
|
1744
1801
|
description: 'Export or restore the browser session\'s cookies (login state). Use "flush" to get a JSON cookie list (save it to a private file to persist logins), or "restore" with that list to put logins back (e.g. after the browser host restarted). Available on all three carriers: the self-hosted browser reads its own session, while the desktop sidebar and an installed Chrome/Edge go through CDP. Refused entirely when the credential switch is off. Exported cookies are LIVE CREDENTIALS: treat them as secrets — do not echo them into the conversation, keep them out of logs, and store the list in a private file.',
|
|
1745
1802
|
parameters: {
|
|
1746
1803
|
action: { type: 'string', required: true, enum: ['flush', 'restore'], description: 'flush = export cookies; restore = import cookies.' },
|
|
@@ -1760,7 +1817,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1760
1817
|
timeoutMs,
|
|
1761
1818
|
isConcurrencySafe: () => false,
|
|
1762
1819
|
async execute(args, exec) {
|
|
1763
|
-
assertAllowed(state, '
|
|
1820
|
+
assertAllowed(state, 'dsh_browser_auth', taskKey(exec))
|
|
1764
1821
|
const browser = ctx.get('browser')
|
|
1765
1822
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1766
1823
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1772,7 +1829,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1772
1829
|
// empty call returned {restored: 0} and rendered 'Restored 0 cookies.', which
|
|
1773
1830
|
// reads as success and would leave the caller believing the login was restored.
|
|
1774
1831
|
if (!Array.isArray(args.cookies) || args.cookies.length === 0) {
|
|
1775
|
-
throw new Error('
|
|
1832
|
+
throw new Error('dsh_browser_auth: action "restore" needs a non-empty "cookies" array (get one from action "flush" on the same profile)')
|
|
1776
1833
|
}
|
|
1777
1834
|
const list = args.cookies as unknown[]
|
|
1778
1835
|
const restored = await browser.restoreAuth(session, list as never)
|