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
|
|
@@ -57,33 +57,33 @@ const liveStates = new Set();
|
|
|
57
57
|
* Tools that only observe, plus the ones that undo a restriction.
|
|
58
58
|
*
|
|
59
59
|
* Never subject to an allow-list: restricting what the agent may *do* must not also
|
|
60
|
-
* blind it or trap it, and the tool descriptions promise as much.
|
|
60
|
+
* blind it or trap it, and the tool descriptions promise as much. dsh_browser_restrict
|
|
61
61
|
* is in here so a task that restricted everything can still lift it.
|
|
62
62
|
*/
|
|
63
63
|
export const READ_ONLY_TOOLS = new Set([
|
|
64
64
|
// Tools that only observe.
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
//
|
|
65
|
+
'dsh_browser_snapshot', 'dsh_browser_a11y', 'dsh_browser_content', 'dsh_browser_scrape',
|
|
66
|
+
'dsh_browser_screenshot', 'dsh_browser_get_value', 'dsh_browser_wait', 'dsh_browser_challenge',
|
|
67
|
+
'dsh_browser_list_tabs', 'dsh_browser_session', 'dsh_browser_history', 'dsh_browser_visited',
|
|
68
|
+
// dsh_browser_auth reads cookies out of the browser: an observation, and one the
|
|
69
69
|
// credentials switch gates on its own (see credentials.allowRead in the settings).
|
|
70
|
-
//
|
|
70
|
+
// dsh_browser_auth is deliberately NOT here. It reads cookies on "flush" but WRITES them on
|
|
71
71
|
// "restore", to arbitrary domains, which is an action like any other — exempting the tool
|
|
72
72
|
// would let a task that restricted its own actions rewrite the shared browser's logins.
|
|
73
73
|
//
|
|
74
74
|
// Getting out of a bad state, or lifting the restriction itself. Without these a task
|
|
75
75
|
// that restricted everything could neither recover nor be released.
|
|
76
|
-
'
|
|
76
|
+
'dsh_browser_restrict', 'dsh_browser_reset_session', 'dsh_browser_reset',
|
|
77
77
|
]);
|
|
78
78
|
function assertAllowed(state, toolName, task) {
|
|
79
|
-
//
|
|
79
|
+
// dsh_browser_restrict must stay usable, or a task that restricted everything
|
|
80
80
|
// could never lift it again.
|
|
81
81
|
// An allow-list is a statement about ACTIONS, so it must not take away the ability to
|
|
82
82
|
// look, or to get out of a bad state. The tool description and both READMEs promise
|
|
83
83
|
// that read-only tools are never blocked; before this they were, because only
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
if (READ_ONLY_TOOLS.has(toolName) || toolName === '
|
|
84
|
+
// dsh_browser_restrict itself was exempt — a narrow list then locked dsh_browser_history,
|
|
85
|
+
// dsh_browser_reset_session and dsh_browser_auth away with no way back.
|
|
86
|
+
if (READ_ONLY_TOOLS.has(toolName) || toolName === 'dsh_browser_restrict')
|
|
87
87
|
return;
|
|
88
88
|
const config = state.configRestrictedTo;
|
|
89
89
|
if (config !== undefined && !config.includes(toolName)) {
|
|
@@ -98,6 +98,40 @@ function assertAllowed(state, toolName, task) {
|
|
|
98
98
|
function agentOf(exec) {
|
|
99
99
|
return exec?.agent;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Refuse the bare DSH web root, which answers 401 and looks like a real page.
|
|
103
|
+
*
|
|
104
|
+
* The address comes from `DSH_WEB_URL`, which the harness exports to its children — NOT from a
|
|
105
|
+
* hardcoded port. This machine's GUI has run on 3080 and on 19387, so a number would stop matching
|
|
106
|
+
* exactly when the guard is needed, and the trap is not a port anyway: it is the SHAPE. A loopback
|
|
107
|
+
* address at its root with no token renders as an unauthenticated 401 page, which an agent reads
|
|
108
|
+
* as the site it meant to open.
|
|
109
|
+
* @param rawUrl - the address a caller asked to open.
|
|
110
|
+
*/
|
|
111
|
+
function assertNotBareDshWebUrl(rawUrl) {
|
|
112
|
+
const advertised = process.env.DSH_WEB_URL;
|
|
113
|
+
if (typeof advertised !== 'string' || advertised === '')
|
|
114
|
+
return;
|
|
115
|
+
let target;
|
|
116
|
+
let dsh;
|
|
117
|
+
try {
|
|
118
|
+
target = new URL(rawUrl);
|
|
119
|
+
dsh = new URL(advertised);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return; // not a URL this guard can judge; the provider's own admission is the next gate
|
|
123
|
+
}
|
|
124
|
+
if (target.origin !== dsh.origin)
|
|
125
|
+
return;
|
|
126
|
+
if (target.pathname !== '/')
|
|
127
|
+
return;
|
|
128
|
+
// The authenticated URL printed by `dsh web` carries a token; the bare root does not.
|
|
129
|
+
if (target.searchParams.has('token'))
|
|
130
|
+
return;
|
|
131
|
+
throw new Error('dsh_browser_open: refusing the bare DSH web root (' + dsh.origin + '/) because it carries no '
|
|
132
|
+
+ 'authentication token, so it answers with a 401 page that reads like a real site. Open the '
|
|
133
|
+
+ 'target site instead; to open DSH itself, use the authenticated URL printed by `dsh web`.');
|
|
134
|
+
}
|
|
101
135
|
/**
|
|
102
136
|
* Read a target's `index`, refusing the values that cannot mean anything.
|
|
103
137
|
*
|
|
@@ -228,17 +262,17 @@ function formatSnapshot(snapshot, options = {}) {
|
|
|
228
262
|
export function apply(ctx, config = {}) {
|
|
229
263
|
const timeoutMs = config.timeoutMs ?? 60_000;
|
|
230
264
|
/**
|
|
231
|
-
* Longest wait `
|
|
265
|
+
* Longest wait `dsh_browser_wait` may poll for, leaving room for the tool call itself to
|
|
232
266
|
* finish and report.
|
|
233
267
|
*
|
|
234
|
-
* `
|
|
268
|
+
* `dsh_browser_wait` promises a VERDICT — `{ready:false, reason}` — and the enclosing tool
|
|
235
269
|
* call aborts at `timeoutMs`. A caller-supplied budget longer than that aborted the call
|
|
236
270
|
* instead, so the documented verdict never arrived. Clamping to just inside the budget is
|
|
237
271
|
* what keeps the promise: the polling stops, the reason is returned.
|
|
238
272
|
*/
|
|
239
273
|
const maxWaitMs = Math.max(250, timeoutMs - 5_000);
|
|
240
274
|
/**
|
|
241
|
-
* Most fields `
|
|
275
|
+
* Most fields `dsh_browser_fill` accepts in one batch.
|
|
242
276
|
*
|
|
243
277
|
* The batch runs as ONE page evaluation, so hundreds of fields do not produce hundreds of
|
|
244
278
|
* per-field verdicts — they produce a single evaluate that exceeds the tool budget and
|
|
@@ -259,11 +293,33 @@ export function apply(ctx, config = {}) {
|
|
|
259
293
|
// Tool guidance band is 100-199; 150 keeps clear of the common 110/120
|
|
260
294
|
// tool sections so ordering does not depend on plugin load sequence.
|
|
261
295
|
order: 150,
|
|
262
|
-
text: 'Use the
|
|
296
|
+
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.',
|
|
263
297
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Register one tool, turning a name collision into a warning instead of a dead apply.
|
|
300
|
+
*
|
|
301
|
+
* Registering the tools in sequence without this is how ONE taken name becomes thirty-two
|
|
302
|
+
* missing ones: the throw aborts the rest of the apply, and what the user sees is a plugin that
|
|
303
|
+
* half-exists. That is precisely the shape of the failure reported against a sibling plugin,
|
|
304
|
+
* where the first spec collided and all six vanished behind a `console.warn` a desktop user
|
|
305
|
+
* never sees.
|
|
306
|
+
*
|
|
307
|
+
* A name this plugin cannot have is worth a sentence in the log, not the toolset.
|
|
308
|
+
* @param definition - the tool to register.
|
|
309
|
+
*/
|
|
310
|
+
const registerSafely = (definition) => {
|
|
311
|
+
try {
|
|
312
|
+
ctx.tools.register(definition);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
ctx.logger?.warn?.(`dsh-builtin-browser: could not register ${definition.name} (${String(error)}). Another `
|
|
316
|
+
+ 'plugin has probably taken that name — browser tool names are a shared namespace. The '
|
|
317
|
+
+ 'rest of this plugin\'s tools are unaffected.');
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
registerSafely(defineTool({
|
|
321
|
+
name: 'dsh_browser_open',
|
|
322
|
+
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.',
|
|
267
323
|
parameters: {
|
|
268
324
|
url: { type: 'string', required: true, description: 'The URL to open (HTTP/HTTPS).' },
|
|
269
325
|
newTab: { type: 'boolean', description: 'Open in a new tab instead of the active one.' },
|
|
@@ -279,7 +335,7 @@ export function apply(ctx, config = {}) {
|
|
|
279
335
|
elements: {
|
|
280
336
|
type: 'array',
|
|
281
337
|
required: true,
|
|
282
|
-
// Same shared item schema as
|
|
338
|
+
// Same shared item schema as dsh_browser_open's, so the two cannot drift apart.
|
|
283
339
|
items: ELEMENT_ITEM_SCHEMA,
|
|
284
340
|
},
|
|
285
341
|
challenge: {
|
|
@@ -293,15 +349,16 @@ export function apply(ctx, config = {}) {
|
|
|
293
349
|
},
|
|
294
350
|
},
|
|
295
351
|
},
|
|
296
|
-
//
|
|
297
|
-
// `coords` parameter, so coordinates stay off here (
|
|
352
|
+
// dsh_browser_open is an overview of what just loaded, not a locator: it has no
|
|
353
|
+
// `coords` parameter, so coordinates stay off here (dsh_browser_snapshot owns
|
|
298
354
|
// that switch for callers that actually target pixels).
|
|
299
355
|
render: (args, value) => [{ type: 'text', text: formatSnapshot(value, {}) }],
|
|
300
356
|
},
|
|
301
357
|
timeoutMs,
|
|
302
358
|
isConcurrencySafe: () => false, // opens tabs / navigates; exclusive within a task
|
|
303
359
|
async execute(args, exec) {
|
|
304
|
-
assertAllowed(state, '
|
|
360
|
+
assertAllowed(state, 'dsh_browser_open', taskKey(exec));
|
|
361
|
+
assertNotBareDshWebUrl(args.url);
|
|
305
362
|
const browser = ctx.get('browser');
|
|
306
363
|
if (browser === undefined)
|
|
307
364
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -320,9 +377,9 @@ export function apply(ctx, config = {}) {
|
|
|
320
377
|
};
|
|
321
378
|
},
|
|
322
379
|
}));
|
|
323
|
-
|
|
324
|
-
name: '
|
|
325
|
-
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 (
|
|
380
|
+
registerSafely(defineTool({
|
|
381
|
+
name: 'dsh_browser_snapshot',
|
|
382
|
+
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.',
|
|
326
383
|
parameters: {
|
|
327
384
|
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.' },
|
|
328
385
|
},
|
|
@@ -386,9 +443,9 @@ export function apply(ctx, config = {}) {
|
|
|
386
443
|
};
|
|
387
444
|
},
|
|
388
445
|
}));
|
|
389
|
-
|
|
390
|
-
name: '
|
|
391
|
-
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
|
|
446
|
+
registerSafely(defineTool({
|
|
447
|
+
name: 'dsh_browser_a11y',
|
|
448
|
+
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.',
|
|
392
449
|
parameters: {
|
|
393
450
|
includeHidden: { type: 'boolean', description: 'Include hidden elements (default false).' },
|
|
394
451
|
maxNodes: { type: 'number', description: 'Maximum nodes (default 150, range 10-5000).' },
|
|
@@ -471,9 +528,9 @@ export function apply(ctx, config = {}) {
|
|
|
471
528
|
};
|
|
472
529
|
},
|
|
473
530
|
}));
|
|
474
|
-
|
|
475
|
-
name: '
|
|
476
|
-
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
|
|
531
|
+
registerSafely(defineTool({
|
|
532
|
+
name: 'dsh_browser_challenge',
|
|
533
|
+
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.',
|
|
477
534
|
parameters: {},
|
|
478
535
|
output: {
|
|
479
536
|
schema: {
|
|
@@ -506,14 +563,14 @@ export function apply(ctx, config = {}) {
|
|
|
506
563
|
...challenge.kind !== undefined ? { kind: challenge.kind } : {},
|
|
507
564
|
...challenge.reason !== undefined ? { reason: challenge.reason } : {},
|
|
508
565
|
hint: challenge.blocked
|
|
509
|
-
? 'Ask the human to complete the verification in the shared browser window (the page is visible to them), then re-check with
|
|
566
|
+
? '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.'
|
|
510
567
|
: '',
|
|
511
568
|
};
|
|
512
569
|
},
|
|
513
570
|
}));
|
|
514
|
-
|
|
515
|
-
name: '
|
|
516
|
-
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
|
|
571
|
+
registerSafely(defineTool({
|
|
572
|
+
name: 'dsh_browser_wait',
|
|
573
|
+
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.',
|
|
517
574
|
parameters: {
|
|
518
575
|
timeoutMs: { type: 'number', description: 'Maximum wait in ms (default 30000).' },
|
|
519
576
|
url: { type: 'string', description: 'Expected page URL (exact or prefix), e.g. the URL you opened.' },
|
|
@@ -547,8 +604,8 @@ export function apply(ctx, config = {}) {
|
|
|
547
604
|
return { ready: result.ready, reason: result.reason };
|
|
548
605
|
},
|
|
549
606
|
}));
|
|
550
|
-
|
|
551
|
-
name: '
|
|
607
|
+
registerSafely(defineTool({
|
|
608
|
+
name: 'dsh_browser_execute',
|
|
552
609
|
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.',
|
|
553
610
|
parameters: {
|
|
554
611
|
script: { type: 'string', required: true, description: 'The JavaScript expression to evaluate in the page context.' },
|
|
@@ -573,7 +630,7 @@ export function apply(ctx, config = {}) {
|
|
|
573
630
|
const rendered = String(value.value ?? '');
|
|
574
631
|
const cap = 50_000;
|
|
575
632
|
const text = rendered.length > cap
|
|
576
|
-
? `Result: ${rendered.slice(0, cap)}\n(result truncated at ${cap} of ${rendered.length} characters — return a smaller value, or write it with
|
|
633
|
+
? `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)`
|
|
577
634
|
: `Result: ${rendered}`;
|
|
578
635
|
return [{ type: 'text', text }];
|
|
579
636
|
},
|
|
@@ -581,7 +638,7 @@ export function apply(ctx, config = {}) {
|
|
|
581
638
|
timeoutMs,
|
|
582
639
|
isConcurrencySafe: () => false, // page JS can be stateful
|
|
583
640
|
async execute(args, exec) {
|
|
584
|
-
assertAllowed(state, '
|
|
641
|
+
assertAllowed(state, 'dsh_browser_execute', taskKey(exec));
|
|
585
642
|
const browser = ctx.get('browser');
|
|
586
643
|
if (browser === undefined)
|
|
587
644
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -598,8 +655,8 @@ export function apply(ctx, config = {}) {
|
|
|
598
655
|
return { ok: false, exception: result.exception };
|
|
599
656
|
},
|
|
600
657
|
}));
|
|
601
|
-
|
|
602
|
-
name: '
|
|
658
|
+
registerSafely(defineTool({
|
|
659
|
+
name: 'dsh_browser_content',
|
|
603
660
|
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.',
|
|
604
661
|
parameters: {
|
|
605
662
|
format: { type: 'string', required: true, enum: ['html', 'markdown', 'txt', 'json'], description: 'Output format.' },
|
|
@@ -634,9 +691,9 @@ export function apply(ctx, config = {}) {
|
|
|
634
691
|
return { content: result.content, truncated: result.truncated };
|
|
635
692
|
},
|
|
636
693
|
}));
|
|
637
|
-
|
|
638
|
-
name: '
|
|
639
|
-
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
|
|
694
|
+
registerSafely(defineTool({
|
|
695
|
+
name: 'dsh_browser_scrape',
|
|
696
|
+
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.',
|
|
640
697
|
parameters: {
|
|
641
698
|
item: { type: 'string', required: true, description: 'CSS selector of each result container (e.g. "div.card").' },
|
|
642
699
|
fields: {
|
|
@@ -671,7 +728,7 @@ export function apply(ctx, config = {}) {
|
|
|
671
728
|
timeoutMs,
|
|
672
729
|
isConcurrencySafe: () => true,
|
|
673
730
|
async execute(args, exec) {
|
|
674
|
-
assertAllowed(state, '
|
|
731
|
+
assertAllowed(state, 'dsh_browser_scrape', taskKey(exec));
|
|
675
732
|
const browser = ctx.get('browser');
|
|
676
733
|
if (browser === undefined)
|
|
677
734
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -685,9 +742,9 @@ export function apply(ctx, config = {}) {
|
|
|
685
742
|
return { count: result.count, items: result.items.map(it => ({ ...it })) };
|
|
686
743
|
},
|
|
687
744
|
}));
|
|
688
|
-
|
|
689
|
-
name: '
|
|
690
|
-
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
|
|
745
|
+
registerSafely(defineTool({
|
|
746
|
+
name: 'dsh_browser_click',
|
|
747
|
+
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.',
|
|
691
748
|
parameters: {
|
|
692
749
|
target: {
|
|
693
750
|
type: 'object',
|
|
@@ -699,7 +756,7 @@ export function apply(ctx, config = {}) {
|
|
|
699
756
|
},
|
|
700
757
|
description: 'Locate the element semantically and click it (css/text/xpath).',
|
|
701
758
|
},
|
|
702
|
-
x: { type: 'number', description: 'Viewport x coordinate (CSS px), taken from a
|
|
759
|
+
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.' },
|
|
703
760
|
y: { type: 'number', description: 'Viewport y coordinate (CSS px), taken from the same screenshot as x.' },
|
|
704
761
|
},
|
|
705
762
|
output: {
|
|
@@ -709,7 +766,7 @@ export function apply(ctx, config = {}) {
|
|
|
709
766
|
timeoutMs,
|
|
710
767
|
isConcurrencySafe: () => false,
|
|
711
768
|
async execute(args, exec) {
|
|
712
|
-
assertAllowed(state, '
|
|
769
|
+
assertAllowed(state, 'dsh_browser_click', taskKey(exec));
|
|
713
770
|
const browser = ctx.get('browser');
|
|
714
771
|
if (browser === undefined)
|
|
715
772
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -733,16 +790,16 @@ export function apply(ctx, config = {}) {
|
|
|
733
790
|
}
|
|
734
791
|
else {
|
|
735
792
|
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
|
|
736
|
-
throw new Error('
|
|
793
|
+
throw new Error('dsh_browser_click: provide a target (css/text/xpath) or x/y coordinates');
|
|
737
794
|
}
|
|
738
795
|
await browser.click(session, { x: args.x, y: args.y }, exec.signal);
|
|
739
796
|
}
|
|
740
797
|
return { clicked: true };
|
|
741
798
|
},
|
|
742
799
|
}));
|
|
743
|
-
|
|
744
|
-
name: '
|
|
745
|
-
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
|
|
800
|
+
registerSafely(defineTool({
|
|
801
|
+
name: 'dsh_browser_type',
|
|
802
|
+
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.',
|
|
746
803
|
parameters: {
|
|
747
804
|
text: { type: 'string', required: true, description: 'The text to insert.' },
|
|
748
805
|
target: {
|
|
@@ -763,7 +820,7 @@ export function apply(ctx, config = {}) {
|
|
|
763
820
|
timeoutMs,
|
|
764
821
|
isConcurrencySafe: () => false,
|
|
765
822
|
async execute(args, exec) {
|
|
766
|
-
assertAllowed(state, '
|
|
823
|
+
assertAllowed(state, 'dsh_browser_type', taskKey(exec));
|
|
767
824
|
const browser = ctx.get('browser');
|
|
768
825
|
if (browser === undefined)
|
|
769
826
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -792,8 +849,8 @@ export function apply(ctx, config = {}) {
|
|
|
792
849
|
return { typed: true };
|
|
793
850
|
},
|
|
794
851
|
}));
|
|
795
|
-
|
|
796
|
-
name: '
|
|
852
|
+
registerSafely(defineTool({
|
|
853
|
+
name: 'dsh_browser_scroll',
|
|
797
854
|
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.',
|
|
798
855
|
parameters: {
|
|
799
856
|
deltaX: { type: 'number', description: 'Horizontal scroll delta in CSS pixels.' },
|
|
@@ -809,7 +866,7 @@ export function apply(ctx, config = {}) {
|
|
|
809
866
|
timeoutMs,
|
|
810
867
|
isConcurrencySafe: () => false, // mutates page scroll state; exclusive within a task
|
|
811
868
|
async execute(args, exec) {
|
|
812
|
-
assertAllowed(state, '
|
|
869
|
+
assertAllowed(state, 'dsh_browser_scroll', taskKey(exec));
|
|
813
870
|
const browser = ctx.get('browser');
|
|
814
871
|
if (browser === undefined)
|
|
815
872
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -824,8 +881,8 @@ export function apply(ctx, config = {}) {
|
|
|
824
881
|
return { scrolled: true };
|
|
825
882
|
},
|
|
826
883
|
}));
|
|
827
|
-
|
|
828
|
-
name: '
|
|
884
|
+
registerSafely(defineTool({
|
|
885
|
+
name: 'dsh_browser_back',
|
|
829
886
|
description: 'Go back one step in the shared-browser page history. A no-op when there is no previous entry.',
|
|
830
887
|
parameters: {},
|
|
831
888
|
output: {
|
|
@@ -835,7 +892,7 @@ export function apply(ctx, config = {}) {
|
|
|
835
892
|
timeoutMs,
|
|
836
893
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
837
894
|
async execute(_args, exec) {
|
|
838
|
-
assertAllowed(state, '
|
|
895
|
+
assertAllowed(state, 'dsh_browser_back', taskKey(exec));
|
|
839
896
|
const browser = ctx.get('browser');
|
|
840
897
|
if (browser === undefined)
|
|
841
898
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -844,8 +901,8 @@ export function apply(ctx, config = {}) {
|
|
|
844
901
|
return { back: true };
|
|
845
902
|
},
|
|
846
903
|
}));
|
|
847
|
-
|
|
848
|
-
name: '
|
|
904
|
+
registerSafely(defineTool({
|
|
905
|
+
name: 'dsh_browser_forward',
|
|
849
906
|
description: 'Go forward one step in the shared-browser page history. A no-op when there is no next entry.',
|
|
850
907
|
parameters: {},
|
|
851
908
|
output: {
|
|
@@ -855,7 +912,7 @@ export function apply(ctx, config = {}) {
|
|
|
855
912
|
timeoutMs,
|
|
856
913
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
857
914
|
async execute(_args, exec) {
|
|
858
|
-
assertAllowed(state, '
|
|
915
|
+
assertAllowed(state, 'dsh_browser_forward', taskKey(exec));
|
|
859
916
|
const browser = ctx.get('browser');
|
|
860
917
|
if (browser === undefined)
|
|
861
918
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -864,8 +921,8 @@ export function apply(ctx, config = {}) {
|
|
|
864
921
|
return { forward: true };
|
|
865
922
|
},
|
|
866
923
|
}));
|
|
867
|
-
|
|
868
|
-
name: '
|
|
924
|
+
registerSafely(defineTool({
|
|
925
|
+
name: 'dsh_browser_refresh',
|
|
869
926
|
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.',
|
|
870
927
|
parameters: {},
|
|
871
928
|
output: {
|
|
@@ -875,7 +932,7 @@ export function apply(ctx, config = {}) {
|
|
|
875
932
|
timeoutMs,
|
|
876
933
|
isConcurrencySafe: () => false, // reloads the page; exclusive within a task
|
|
877
934
|
async execute(_args, exec) {
|
|
878
|
-
assertAllowed(state, '
|
|
935
|
+
assertAllowed(state, 'dsh_browser_refresh', taskKey(exec));
|
|
879
936
|
const browser = ctx.get('browser');
|
|
880
937
|
if (browser === undefined)
|
|
881
938
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -884,8 +941,8 @@ export function apply(ctx, config = {}) {
|
|
|
884
941
|
return { refreshed: true };
|
|
885
942
|
},
|
|
886
943
|
}));
|
|
887
|
-
|
|
888
|
-
name: '
|
|
944
|
+
registerSafely(defineTool({
|
|
945
|
+
name: 'dsh_browser_key',
|
|
889
946
|
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).',
|
|
890
947
|
parameters: {
|
|
891
948
|
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.' },
|
|
@@ -897,7 +954,7 @@ export function apply(ctx, config = {}) {
|
|
|
897
954
|
timeoutMs,
|
|
898
955
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
899
956
|
async execute(args, exec) {
|
|
900
|
-
assertAllowed(state, '
|
|
957
|
+
assertAllowed(state, 'dsh_browser_key', taskKey(exec));
|
|
901
958
|
const browser = ctx.get('browser');
|
|
902
959
|
if (browser === undefined)
|
|
903
960
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -906,9 +963,9 @@ export function apply(ctx, config = {}) {
|
|
|
906
963
|
return { pressed: true };
|
|
907
964
|
},
|
|
908
965
|
}));
|
|
909
|
-
|
|
910
|
-
name: '
|
|
911
|
-
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
|
|
966
|
+
registerSafely(defineTool({
|
|
967
|
+
name: 'dsh_browser_fill',
|
|
968
|
+
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.',
|
|
912
969
|
parameters: {
|
|
913
970
|
fields: {
|
|
914
971
|
type: 'array',
|
|
@@ -966,14 +1023,14 @@ export function apply(ctx, config = {}) {
|
|
|
966
1023
|
timeoutMs,
|
|
967
1024
|
isConcurrencySafe: () => false,
|
|
968
1025
|
async execute(args, exec) {
|
|
969
|
-
assertAllowed(state, '
|
|
1026
|
+
assertAllowed(state, 'dsh_browser_fill', taskKey(exec));
|
|
970
1027
|
const browser = ctx.get('browser');
|
|
971
1028
|
if (browser === undefined)
|
|
972
1029
|
throw new Error('tool-browser: browser service unavailable');
|
|
973
1030
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec));
|
|
974
1031
|
const requested = args.fields ?? [];
|
|
975
1032
|
if (requested.length > maxFillFields) {
|
|
976
|
-
throw new Error(`
|
|
1033
|
+
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`);
|
|
977
1034
|
}
|
|
978
1035
|
const fields = requested.map((f) => {
|
|
979
1036
|
// A missing value is a caller mistake, not a request to clear the field. Turning
|
|
@@ -982,7 +1039,7 @@ export function apply(ctx, config = {}) {
|
|
|
982
1039
|
// a call can still arrive without it.
|
|
983
1040
|
if (f.value === undefined) {
|
|
984
1041
|
const matcher = f.selector ?? f.name ?? f.label ?? f.placeholder ?? '(no matcher)';
|
|
985
|
-
throw new Error(`
|
|
1042
|
+
throw new Error(`dsh_browser_fill: field "${matcher}" has no "value"; pass one (use "false" to uncheck, or "" to clear deliberately)`);
|
|
986
1043
|
}
|
|
987
1044
|
return {
|
|
988
1045
|
...f.selector !== undefined ? { selector: f.selector } : {},
|
|
@@ -1017,9 +1074,9 @@ export function apply(ctx, config = {}) {
|
|
|
1017
1074
|
},
|
|
1018
1075
|
description: 'Locate the element (css/text/xpath).',
|
|
1019
1076
|
};
|
|
1020
|
-
|
|
1021
|
-
name: '
|
|
1022
|
-
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
|
|
1077
|
+
registerSafely(defineTool({
|
|
1078
|
+
name: 'dsh_browser_set_value',
|
|
1079
|
+
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.',
|
|
1023
1080
|
parameters: {
|
|
1024
1081
|
target: targetParam,
|
|
1025
1082
|
value: { type: 'string', required: true, description: 'The value to set (string form; numbers/booleans accepted).' },
|
|
@@ -1038,7 +1095,7 @@ export function apply(ctx, config = {}) {
|
|
|
1038
1095
|
timeoutMs,
|
|
1039
1096
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1040
1097
|
async execute(args, exec) {
|
|
1041
|
-
assertAllowed(state, '
|
|
1098
|
+
assertAllowed(state, 'dsh_browser_set_value', taskKey(exec));
|
|
1042
1099
|
const browser = ctx.get('browser');
|
|
1043
1100
|
if (browser === undefined)
|
|
1044
1101
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1055,8 +1112,8 @@ export function apply(ctx, config = {}) {
|
|
|
1055
1112
|
return { method: result.method, value: result.value };
|
|
1056
1113
|
},
|
|
1057
1114
|
}));
|
|
1058
|
-
|
|
1059
|
-
name: '
|
|
1115
|
+
registerSafely(defineTool({
|
|
1116
|
+
name: 'dsh_browser_check',
|
|
1060
1117
|
description: 'Check (or uncheck, with checked=false) a checkbox or radio button, located by css/text/xpath.',
|
|
1061
1118
|
parameters: {
|
|
1062
1119
|
target: targetParam,
|
|
@@ -1069,7 +1126,7 @@ export function apply(ctx, config = {}) {
|
|
|
1069
1126
|
timeoutMs,
|
|
1070
1127
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1071
1128
|
async execute(args, exec) {
|
|
1072
|
-
assertAllowed(state, '
|
|
1129
|
+
assertAllowed(state, 'dsh_browser_check', taskKey(exec));
|
|
1073
1130
|
const browser = ctx.get('browser');
|
|
1074
1131
|
if (browser === undefined)
|
|
1075
1132
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1086,8 +1143,8 @@ export function apply(ctx, config = {}) {
|
|
|
1086
1143
|
return { checked: result.checked };
|
|
1087
1144
|
},
|
|
1088
1145
|
}));
|
|
1089
|
-
|
|
1090
|
-
name: '
|
|
1146
|
+
registerSafely(defineTool({
|
|
1147
|
+
name: 'dsh_browser_select',
|
|
1091
1148
|
description: 'Select one option of a <select>, located by css/text/xpath — by option value, visible text, or 0-based index (provide exactly one).',
|
|
1092
1149
|
parameters: {
|
|
1093
1150
|
target: targetParam,
|
|
@@ -1109,7 +1166,7 @@ export function apply(ctx, config = {}) {
|
|
|
1109
1166
|
timeoutMs,
|
|
1110
1167
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1111
1168
|
async execute(args, exec) {
|
|
1112
|
-
assertAllowed(state, '
|
|
1169
|
+
assertAllowed(state, 'dsh_browser_select', taskKey(exec));
|
|
1113
1170
|
const browser = ctx.get('browser');
|
|
1114
1171
|
if (browser === undefined)
|
|
1115
1172
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1128,8 +1185,8 @@ export function apply(ctx, config = {}) {
|
|
|
1128
1185
|
return { value: result.value, text: result.text };
|
|
1129
1186
|
},
|
|
1130
1187
|
}));
|
|
1131
|
-
|
|
1132
|
-
name: '
|
|
1188
|
+
registerSafely(defineTool({
|
|
1189
|
+
name: 'dsh_browser_clear',
|
|
1133
1190
|
description: 'Clear an input/textarea/contenteditable, or uncheck a checkbox/radio, located by css/text/xpath.',
|
|
1134
1191
|
parameters: {
|
|
1135
1192
|
target: targetParam,
|
|
@@ -1141,7 +1198,7 @@ export function apply(ctx, config = {}) {
|
|
|
1141
1198
|
timeoutMs,
|
|
1142
1199
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1143
1200
|
async execute(args, exec) {
|
|
1144
|
-
assertAllowed(state, '
|
|
1201
|
+
assertAllowed(state, 'dsh_browser_clear', taskKey(exec));
|
|
1145
1202
|
const browser = ctx.get('browser');
|
|
1146
1203
|
if (browser === undefined)
|
|
1147
1204
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1157,8 +1214,8 @@ export function apply(ctx, config = {}) {
|
|
|
1157
1214
|
return { cleared: true };
|
|
1158
1215
|
},
|
|
1159
1216
|
}));
|
|
1160
|
-
|
|
1161
|
-
name: '
|
|
1217
|
+
registerSafely(defineTool({
|
|
1218
|
+
name: 'dsh_browser_get_value',
|
|
1162
1219
|
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).',
|
|
1163
1220
|
parameters: {
|
|
1164
1221
|
target: targetParam,
|
|
@@ -1184,7 +1241,7 @@ export function apply(ctx, config = {}) {
|
|
|
1184
1241
|
timeoutMs,
|
|
1185
1242
|
isConcurrencySafe: () => true,
|
|
1186
1243
|
async execute(args, exec) {
|
|
1187
|
-
assertAllowed(state, '
|
|
1244
|
+
assertAllowed(state, 'dsh_browser_get_value', taskKey(exec));
|
|
1188
1245
|
const browser = ctx.get('browser');
|
|
1189
1246
|
if (browser === undefined)
|
|
1190
1247
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1204,9 +1261,9 @@ export function apply(ctx, config = {}) {
|
|
|
1204
1261
|
};
|
|
1205
1262
|
},
|
|
1206
1263
|
}));
|
|
1207
|
-
|
|
1208
|
-
name: '
|
|
1209
|
-
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 —
|
|
1264
|
+
registerSafely(defineTool({
|
|
1265
|
+
name: 'dsh_browser_screenshot',
|
|
1266
|
+
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.',
|
|
1210
1267
|
parameters: {
|
|
1211
1268
|
fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
|
|
1212
1269
|
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.' },
|
|
@@ -1256,8 +1313,8 @@ export function apply(ctx, config = {}) {
|
|
|
1256
1313
|
},
|
|
1257
1314
|
}));
|
|
1258
1315
|
if (config.tabTools !== false) {
|
|
1259
|
-
|
|
1260
|
-
name: '
|
|
1316
|
+
registerSafely(defineTool({
|
|
1317
|
+
name: 'dsh_browser_list_tabs',
|
|
1261
1318
|
description: 'List the shared-browser session\'s tabs with their URLs and which is active.',
|
|
1262
1319
|
parameters: {},
|
|
1263
1320
|
output: {
|
|
@@ -1297,9 +1354,9 @@ export function apply(ctx, config = {}) {
|
|
|
1297
1354
|
return { tabs: tabs.map(t => ({ id: t.id, url: t.url, active: t.active })) };
|
|
1298
1355
|
},
|
|
1299
1356
|
}));
|
|
1300
|
-
|
|
1301
|
-
name: '
|
|
1302
|
-
description: 'Switch the shared browser to a tab by id (from
|
|
1357
|
+
registerSafely(defineTool({
|
|
1358
|
+
name: 'dsh_browser_switch_tab',
|
|
1359
|
+
description: 'Switch the shared browser to a tab by id (from dsh_browser_list_tabs).',
|
|
1303
1360
|
parameters: {
|
|
1304
1361
|
tabId: { type: 'string', required: true, description: 'The tab id to switch to.' },
|
|
1305
1362
|
},
|
|
@@ -1310,7 +1367,7 @@ export function apply(ctx, config = {}) {
|
|
|
1310
1367
|
timeoutMs,
|
|
1311
1368
|
isConcurrencySafe: () => false, // mutates the active tab; exclusive within a task
|
|
1312
1369
|
async execute(args, exec) {
|
|
1313
|
-
assertAllowed(state, '
|
|
1370
|
+
assertAllowed(state, 'dsh_browser_switch_tab', taskKey(exec));
|
|
1314
1371
|
const browser = ctx.get('browser');
|
|
1315
1372
|
if (browser === undefined)
|
|
1316
1373
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1319,8 +1376,8 @@ export function apply(ctx, config = {}) {
|
|
|
1319
1376
|
return { switched: true };
|
|
1320
1377
|
},
|
|
1321
1378
|
}));
|
|
1322
|
-
|
|
1323
|
-
name: '
|
|
1379
|
+
registerSafely(defineTool({
|
|
1380
|
+
name: 'dsh_browser_close_tab',
|
|
1324
1381
|
description: 'Close a tab in the shared browser by id. Closing the active tab activates the next.',
|
|
1325
1382
|
parameters: {
|
|
1326
1383
|
tabId: { type: 'string', required: true, description: 'The tab id to close.' },
|
|
@@ -1332,7 +1389,7 @@ export function apply(ctx, config = {}) {
|
|
|
1332
1389
|
timeoutMs,
|
|
1333
1390
|
isConcurrencySafe: () => false, // mutates the tab list; exclusive within a task
|
|
1334
1391
|
async execute(args, exec) {
|
|
1335
|
-
assertAllowed(state, '
|
|
1392
|
+
assertAllowed(state, 'dsh_browser_close_tab', taskKey(exec));
|
|
1336
1393
|
const browser = ctx.get('browser');
|
|
1337
1394
|
if (browser === undefined)
|
|
1338
1395
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1341,8 +1398,8 @@ export function apply(ctx, config = {}) {
|
|
|
1341
1398
|
return { closed: true };
|
|
1342
1399
|
},
|
|
1343
1400
|
}));
|
|
1344
|
-
|
|
1345
|
-
name: '
|
|
1401
|
+
registerSafely(defineTool({
|
|
1402
|
+
name: 'dsh_browser_reset',
|
|
1346
1403
|
description: 'Close every tab in the shared browser and start fresh with one blank tab.',
|
|
1347
1404
|
parameters: {},
|
|
1348
1405
|
output: {
|
|
@@ -1352,7 +1409,7 @@ export function apply(ctx, config = {}) {
|
|
|
1352
1409
|
timeoutMs,
|
|
1353
1410
|
isConcurrencySafe: () => false, // closes every tab; exclusive within a task
|
|
1354
1411
|
async execute(_args, exec) {
|
|
1355
|
-
assertAllowed(state, '
|
|
1412
|
+
assertAllowed(state, 'dsh_browser_reset', taskKey(exec));
|
|
1356
1413
|
const browser = ctx.get('browser');
|
|
1357
1414
|
if (browser === undefined)
|
|
1358
1415
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1362,8 +1419,8 @@ export function apply(ctx, config = {}) {
|
|
|
1362
1419
|
},
|
|
1363
1420
|
}));
|
|
1364
1421
|
}
|
|
1365
|
-
|
|
1366
|
-
name: '
|
|
1422
|
+
registerSafely(defineTool({
|
|
1423
|
+
name: 'dsh_browser_history',
|
|
1367
1424
|
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.',
|
|
1368
1425
|
parameters: {
|
|
1369
1426
|
verbose: { type: 'boolean', description: "Include each operation's parameters (default false). They are what you just sent, so they are omitted unless asked for." },
|
|
@@ -1462,9 +1519,9 @@ export function apply(ctx, config = {}) {
|
|
|
1462
1519
|
return { entries: rendered };
|
|
1463
1520
|
},
|
|
1464
1521
|
}));
|
|
1465
|
-
|
|
1466
|
-
name: '
|
|
1467
|
-
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
|
|
1522
|
+
registerSafely(defineTool({
|
|
1523
|
+
name: 'dsh_browser_visited',
|
|
1524
|
+
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.',
|
|
1468
1525
|
parameters: {
|
|
1469
1526
|
limit: { type: 'number', description: 'Maximum entries to return (default 30, capped at 200).' },
|
|
1470
1527
|
domain: { type: 'string', description: 'Only visits whose hostname contains this text (case-insensitive).' },
|
|
@@ -1541,9 +1598,9 @@ export function apply(ctx, config = {}) {
|
|
|
1541
1598
|
};
|
|
1542
1599
|
},
|
|
1543
1600
|
}));
|
|
1544
|
-
|
|
1545
|
-
name: '
|
|
1546
|
-
description: 'Replay one recorded browser operation by its history sequence number (from
|
|
1601
|
+
registerSafely(defineTool({
|
|
1602
|
+
name: 'dsh_browser_replay',
|
|
1603
|
+
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.',
|
|
1547
1604
|
parameters: {
|
|
1548
1605
|
seq: { type: 'number', required: true, description: 'The history entry sequence number to replay.' },
|
|
1549
1606
|
},
|
|
@@ -1554,7 +1611,7 @@ export function apply(ctx, config = {}) {
|
|
|
1554
1611
|
timeoutMs,
|
|
1555
1612
|
isConcurrencySafe: () => false,
|
|
1556
1613
|
async execute(args, exec) {
|
|
1557
|
-
assertAllowed(state, '
|
|
1614
|
+
assertAllowed(state, 'dsh_browser_replay', taskKey(exec));
|
|
1558
1615
|
const browser = ctx.get('browser');
|
|
1559
1616
|
if (browser === undefined)
|
|
1560
1617
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1563,8 +1620,8 @@ export function apply(ctx, config = {}) {
|
|
|
1563
1620
|
return { replayed: true };
|
|
1564
1621
|
},
|
|
1565
1622
|
}));
|
|
1566
|
-
|
|
1567
|
-
name: '
|
|
1623
|
+
registerSafely(defineTool({
|
|
1624
|
+
name: 'dsh_browser_download',
|
|
1568
1625
|
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.',
|
|
1569
1626
|
parameters: {
|
|
1570
1627
|
url: { type: 'string', required: true, description: 'The URL to download.' },
|
|
@@ -1577,7 +1634,7 @@ export function apply(ctx, config = {}) {
|
|
|
1577
1634
|
timeoutMs,
|
|
1578
1635
|
isConcurrencySafe: () => false,
|
|
1579
1636
|
async execute(args, exec) {
|
|
1580
|
-
assertAllowed(state, '
|
|
1637
|
+
assertAllowed(state, 'dsh_browser_download', taskKey(exec));
|
|
1581
1638
|
const browser = ctx.get('browser');
|
|
1582
1639
|
if (browser === undefined)
|
|
1583
1640
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1586,8 +1643,8 @@ export function apply(ctx, config = {}) {
|
|
|
1586
1643
|
return { path: result.path };
|
|
1587
1644
|
},
|
|
1588
1645
|
}));
|
|
1589
|
-
|
|
1590
|
-
name: '
|
|
1646
|
+
registerSafely(defineTool({
|
|
1647
|
+
name: 'dsh_browser_session',
|
|
1591
1648
|
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.',
|
|
1592
1649
|
parameters: {},
|
|
1593
1650
|
output: {
|
|
@@ -1627,9 +1684,9 @@ export function apply(ctx, config = {}) {
|
|
|
1627
1684
|
return { session, tabs: tabs.map(t => ({ id: t.id, url: t.url, active: t.active })) };
|
|
1628
1685
|
},
|
|
1629
1686
|
}));
|
|
1630
|
-
|
|
1631
|
-
name: '
|
|
1632
|
-
description: 'Reset THIS task\'s browser session: close it entirely so the next
|
|
1687
|
+
registerSafely(defineTool({
|
|
1688
|
+
name: 'dsh_browser_reset_session',
|
|
1689
|
+
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.',
|
|
1633
1690
|
parameters: {},
|
|
1634
1691
|
output: {
|
|
1635
1692
|
schema: { type: 'object', additionalProperties: false, properties: { reset: { type: 'boolean', required: true } } },
|
|
@@ -1638,7 +1695,7 @@ export function apply(ctx, config = {}) {
|
|
|
1638
1695
|
timeoutMs,
|
|
1639
1696
|
isConcurrencySafe: () => false, // closes the whole session; exclusive within a task
|
|
1640
1697
|
async execute(_args, exec) {
|
|
1641
|
-
assertAllowed(state, '
|
|
1698
|
+
assertAllowed(state, 'dsh_browser_reset_session', taskKey(exec));
|
|
1642
1699
|
const browser = ctx.get('browser');
|
|
1643
1700
|
if (browser === undefined)
|
|
1644
1701
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1657,9 +1714,9 @@ export function apply(ctx, config = {}) {
|
|
|
1657
1714
|
return { reset: true };
|
|
1658
1715
|
},
|
|
1659
1716
|
}));
|
|
1660
|
-
|
|
1661
|
-
name: '
|
|
1662
|
-
description: 'Restrict which browser actions are allowed, to prevent stray clicks/navigation. Pass a list of browser tool names (e.g. ["
|
|
1717
|
+
registerSafely(defineTool({
|
|
1718
|
+
name: 'dsh_browser_restrict',
|
|
1719
|
+
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.',
|
|
1663
1720
|
parameters: {
|
|
1664
1721
|
allowed: {
|
|
1665
1722
|
type: 'array',
|
|
@@ -1676,9 +1733,9 @@ export function apply(ctx, config = {}) {
|
|
|
1676
1733
|
async execute(args, exec) {
|
|
1677
1734
|
// Always allowed so the guard can be lifted.
|
|
1678
1735
|
const allowed = args.allowed ?? [];
|
|
1679
|
-
const unknown = allowed.filter((t) => !t.startsWith('
|
|
1736
|
+
const unknown = allowed.filter((t) => !t.startsWith('dsh_browser_'));
|
|
1680
1737
|
if (unknown.length > 0) {
|
|
1681
|
-
throw new Error(`
|
|
1738
|
+
throw new Error(`dsh_browser_restrict: unknown tool name(s) ${unknown.map(t => `"${t}"`).join(', ')} (must start with "dsh_browser_")`);
|
|
1682
1739
|
}
|
|
1683
1740
|
// Empty list (or omitted) lifts this task's restriction; a non-empty list is its
|
|
1684
1741
|
// new allow-list. Per task, not per plugin: this state object is shared by every
|
|
@@ -1692,8 +1749,8 @@ export function apply(ctx, config = {}) {
|
|
|
1692
1749
|
return { restrictedTo: current === undefined ? [] : [...current] };
|
|
1693
1750
|
},
|
|
1694
1751
|
}));
|
|
1695
|
-
|
|
1696
|
-
name: '
|
|
1752
|
+
registerSafely(defineTool({
|
|
1753
|
+
name: 'dsh_browser_auth',
|
|
1697
1754
|
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.',
|
|
1698
1755
|
parameters: {
|
|
1699
1756
|
action: { type: 'string', required: true, enum: ['flush', 'restore'], description: 'flush = export cookies; restore = import cookies.' },
|
|
@@ -1713,7 +1770,7 @@ export function apply(ctx, config = {}) {
|
|
|
1713
1770
|
timeoutMs,
|
|
1714
1771
|
isConcurrencySafe: () => false,
|
|
1715
1772
|
async execute(args, exec) {
|
|
1716
|
-
assertAllowed(state, '
|
|
1773
|
+
assertAllowed(state, 'dsh_browser_auth', taskKey(exec));
|
|
1717
1774
|
const browser = ctx.get('browser');
|
|
1718
1775
|
if (browser === undefined)
|
|
1719
1776
|
throw new Error('tool-browser: browser service unavailable');
|
|
@@ -1726,7 +1783,7 @@ export function apply(ctx, config = {}) {
|
|
|
1726
1783
|
// empty call returned {restored: 0} and rendered 'Restored 0 cookies.', which
|
|
1727
1784
|
// reads as success and would leave the caller believing the login was restored.
|
|
1728
1785
|
if (!Array.isArray(args.cookies) || args.cookies.length === 0) {
|
|
1729
|
-
throw new Error('
|
|
1786
|
+
throw new Error('dsh_browser_auth: action "restore" needs a non-empty "cookies" array (get one from action "flush" on the same profile)');
|
|
1730
1787
|
}
|
|
1731
1788
|
const list = args.cookies;
|
|
1732
1789
|
const restored = await browser.restoreAuth(session, list);
|