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.
Files changed (48) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.en.md +62 -491
  3. package/README.md +44 -470
  4. package/client.js +10 -10
  5. package/cordis.patch.yml +4 -4
  6. package/desktop-bridge/plugin-browser-bridge.js +1 -1
  7. package/docs/README.md +8 -4
  8. package/docs/architecture.md +82 -11
  9. package/docs/development.md +38 -0
  10. package/docs/limitations.md +49 -0
  11. package/docs/tool-reference.md +42 -42
  12. package/docs/user-guide.md +93 -37
  13. package/docs/why-browser.md +3 -3
  14. package/lib/browser/types.d.ts +2 -2
  15. package/lib/browser-electron/bridge-connection.d.ts +7 -1
  16. package/lib/browser-electron/bridge-connection.js +58 -3
  17. package/lib/browser-electron/desktop-bridge-host.d.ts +11 -0
  18. package/lib/browser-electron/desktop-bridge-host.js +21 -1
  19. package/lib/browser-electron/entry.d.ts +2 -2
  20. package/lib/browser-electron/entry.js +186 -103
  21. package/lib/browser-electron/history-store.d.ts +1 -1
  22. package/lib/browser-electron/history-store.js +1 -1
  23. package/lib/browser-electron/host-main.js +3 -3
  24. package/lib/browser-electron/provider.d.ts +32 -8
  25. package/lib/browser-electron/provider.js +97 -16
  26. package/lib/browser-electron/remote-host.js +1 -1
  27. package/lib/browser-electron/settings-store.d.ts +14 -6
  28. package/lib/browser-electron/settings-store.js +29 -9
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/tool-browser/element-fields.d.ts +4 -4
  32. package/lib/tool-browser/element-fields.js +6 -6
  33. package/lib/tool-browser/index.d.ts +6 -6
  34. package/lib/tool-browser/index.js +196 -139
  35. package/package.json +2 -2
  36. package/scripts/verify-host-compat.mjs +6 -6
  37. package/src/browser/types.ts +2 -2
  38. package/src/browser-electron/bridge-connection.ts +61 -3
  39. package/src/browser-electron/desktop-bridge-host.ts +22 -1
  40. package/src/browser-electron/entry.ts +101 -9
  41. package/src/browser-electron/history-store.ts +1 -1
  42. package/src/browser-electron/host-main.ts +3 -3
  43. package/src/browser-electron/provider.ts +135 -31
  44. package/src/browser-electron/remote-host.ts +1 -1
  45. package/src/browser-electron/settings-store.ts +32 -13
  46. package/src/index.ts +1 -1
  47. package/src/tool-browser/element-fields.ts +6 -6
  48. package/src/tool-browser/index.ts +197 -140
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Model-facing browser tools over `ctx.browser`: `browser_open`,
3
- * `browser_snapshot`, `browser_execute`, `browser_content`,
4
- * `browser_screenshot`, and tab management (`browser_list_tabs`,
5
- * `browser_switch_tab`, `browser_close_tab`, `browser_reset`).
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
- * `browser_open` (or any tool when no session exists) opens it, and later
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. browser_restrict
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
- 'browser_snapshot', 'browser_a11y', 'browser_content', 'browser_scrape',
66
- 'browser_screenshot', 'browser_get_value', 'browser_wait', 'browser_challenge',
67
- 'browser_list_tabs', 'browser_session', 'browser_history', 'browser_visited',
68
- // browser_auth reads cookies out of the browser: an observation, and one the
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
- // browser_auth is deliberately NOT here. It reads cookies on "flush" but WRITES them on
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
- 'browser_restrict', 'browser_reset_session', 'browser_reset',
76
+ 'dsh_browser_restrict', 'dsh_browser_reset_session', 'dsh_browser_reset',
77
77
  ]);
78
78
  function assertAllowed(state, toolName, task) {
79
- // browser_restrict must stay usable, or a task that restricted everything
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
- // browser_restrict itself was exempt — a narrow list then locked browser_history,
85
- // browser_reset_session and browser_auth away with no way back.
86
- if (READ_ONLY_TOOLS.has(toolName) || toolName === 'browser_restrict')
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 `browser_wait` may poll for, leaving room for the tool call itself to
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
- * `browser_wait` promises a VERDICT — `{ready:false, reason}` — and the enclosing tool
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 `browser_fill` accepts in one batch.
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 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 browser_a11y (semantic roles/names/states — the best structure map) or browser_snapshot (numbered interactive elements), then drive it: browser_click/browser_type accept a target {by: css|text|xpath, value} for semantic locating, or coordinates from browser_screenshot for visual targeting. For form filling prefer browser_fill (batch) or browser_set_value/browser_check/browser_select/browser_clear (single control, target-based); verify with browser_get_value. Use browser_scrape for structured extraction from list pages instead of hand-written browser_execute. After navigating on slow sites, browser_wait for the page. Keep the human informed of what you are doing on the page. If a snapshot or browser_challenge reports a human-verification challenge (CAPTCHA), stop retrying and ask the human to complete it in the browser window, then re-check.',
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
- ctx.tools.register(defineTool({
265
- name: 'browser_open',
266
- 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.',
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 browser_open's, so the two cannot drift apart.
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
- // browser_open is an overview of what just loaded, not a locator: it has no
297
- // `coords` parameter, so coordinates stay off here (browser_snapshot owns
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, 'browser_open', taskKey(exec));
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
- ctx.tools.register(defineTool({
324
- name: 'browser_snapshot',
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 (browser_click, browser_type, browser_check, …) rather than by coordinates.',
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
- ctx.tools.register(defineTool({
390
- name: 'browser_a11y',
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 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 browser_click matches when you target by text. Penetrates same-origin iframes and shadow roots.',
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
- ctx.tools.register(defineTool({
475
- name: 'browser_challenge',
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 browser_snapshot.',
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 browser_snapshot.'
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
- ctx.tools.register(defineTool({
515
- name: 'browser_wait',
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 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.',
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
- ctx.tools.register(defineTool({
551
- name: 'browser_execute',
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 browser_execute + a variable)`
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, 'browser_execute', taskKey(exec));
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
- ctx.tools.register(defineTool({
602
- name: 'browser_content',
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
- ctx.tools.register(defineTool({
638
- name: 'browser_scrape',
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 browser_execute.',
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, 'browser_scrape', taskKey(exec));
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
- ctx.tools.register(defineTool({
689
- name: 'browser_click',
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 browser_screenshot; coordinate clicks are refused outright under the non-visual strategy. Provide exactly one of target or x/y.',
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 browser_screenshot read by a vision model. Prefer a semantic target — it works without image input.' },
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, 'browser_click', taskKey(exec));
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('browser_click: provide a target (css/text/xpath) or x/y coordinates');
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
- ctx.tools.register(defineTool({
744
- name: 'browser_type',
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 browser_set_value; use browser_key for Enter/Tab/arrows.',
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, 'browser_type', taskKey(exec));
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
- ctx.tools.register(defineTool({
796
- name: 'browser_scroll',
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, 'browser_scroll', taskKey(exec));
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
- ctx.tools.register(defineTool({
828
- name: 'browser_back',
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, 'browser_back', taskKey(exec));
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
- ctx.tools.register(defineTool({
848
- name: 'browser_forward',
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, 'browser_forward', taskKey(exec));
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
- ctx.tools.register(defineTool({
868
- name: 'browser_refresh',
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, 'browser_refresh', taskKey(exec));
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
- ctx.tools.register(defineTool({
888
- name: 'browser_key',
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, 'browser_key', taskKey(exec));
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
- ctx.tools.register(defineTool({
910
- name: 'browser_fill',
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 browser_execute for form filling; per-field failures are reported instead of throwing.',
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, 'browser_fill', taskKey(exec));
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(`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`);
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(`browser_fill: field "${matcher}" has no "value"; pass one (use "false" to uncheck, or "" to clear deliberately)`);
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
- ctx.tools.register(defineTool({
1021
- name: 'browser_set_value',
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 browser_fill.',
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, 'browser_set_value', taskKey(exec));
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
- ctx.tools.register(defineTool({
1059
- name: 'browser_check',
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, 'browser_check', taskKey(exec));
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
- ctx.tools.register(defineTool({
1090
- name: 'browser_select',
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, 'browser_select', taskKey(exec));
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
- ctx.tools.register(defineTool({
1132
- name: 'browser_clear',
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, 'browser_clear', taskKey(exec));
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
- ctx.tools.register(defineTool({
1161
- name: 'browser_get_value',
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, 'browser_get_value', taskKey(exec));
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
- ctx.tools.register(defineTool({
1208
- name: 'browser_screenshot',
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 — browser_snapshot, browser_a11y and browser_content carry the same page as text, and 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.',
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
- ctx.tools.register(defineTool({
1260
- name: 'browser_list_tabs',
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
- ctx.tools.register(defineTool({
1301
- name: 'browser_switch_tab',
1302
- description: 'Switch the shared browser to a tab by id (from browser_list_tabs).',
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, 'browser_switch_tab', taskKey(exec));
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
- ctx.tools.register(defineTool({
1323
- name: 'browser_close_tab',
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, 'browser_close_tab', taskKey(exec));
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
- ctx.tools.register(defineTool({
1345
- name: 'browser_reset',
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, 'browser_reset', taskKey(exec));
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
- ctx.tools.register(defineTool({
1366
- name: 'browser_history',
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
- ctx.tools.register(defineTool({
1466
- name: 'browser_visited',
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 browser_open. Not to be confused with browser_history, which is only the current session\'s operation log (navigate/click/type) and disappears with the session.',
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
- ctx.tools.register(defineTool({
1545
- name: 'browser_replay',
1546
- description: 'Replay one recorded browser operation by its history sequence number (from 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.',
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, 'browser_replay', taskKey(exec));
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
- ctx.tools.register(defineTool({
1567
- name: 'browser_download',
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, 'browser_download', taskKey(exec));
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
- ctx.tools.register(defineTool({
1590
- name: 'browser_session',
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
- ctx.tools.register(defineTool({
1631
- name: 'browser_reset_session',
1632
- description: 'Reset THIS task\'s browser session: close it entirely so the next 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.',
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, 'browser_reset_session', taskKey(exec));
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
- ctx.tools.register(defineTool({
1661
- name: 'browser_restrict',
1662
- description: 'Restrict which browser actions are allowed, to prevent stray clicks/navigation. Pass a list of browser tool names (e.g. ["browser_snapshot","browser_content","browser_click"]) — any other 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.',
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('browser_'));
1736
+ const unknown = allowed.filter((t) => !t.startsWith('dsh_browser_'));
1680
1737
  if (unknown.length > 0) {
1681
- throw new Error(`browser_restrict: unknown tool name(s) ${unknown.map(t => `"${t}"`).join(', ')} (must start with "browser_")`);
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
- ctx.tools.register(defineTool({
1696
- name: 'browser_auth',
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, 'browser_auth', taskKey(exec));
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('browser_auth: action "restore" needs a non-empty "cookies" array (get one from action "flush" on the same profile)');
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);