dsh-builtin-browser 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.en.md +62 -491
  3. package/README.md +44 -470
  4. package/client.js +10 -10
  5. package/cordis.patch.yml +4 -4
  6. package/desktop-bridge/plugin-browser-bridge.js +1 -1
  7. package/docs/README.md +8 -4
  8. package/docs/architecture.md +82 -11
  9. package/docs/development.md +38 -0
  10. package/docs/limitations.md +49 -0
  11. package/docs/tool-reference.md +42 -42
  12. package/docs/user-guide.md +93 -37
  13. package/docs/why-browser.md +3 -3
  14. package/lib/browser/types.d.ts +2 -2
  15. package/lib/browser-electron/entry.d.ts +2 -2
  16. package/lib/browser-electron/entry.js +1 -1
  17. package/lib/browser-electron/history-store.d.ts +1 -1
  18. package/lib/browser-electron/history-store.js +1 -1
  19. package/lib/browser-electron/host-main.js +3 -3
  20. package/lib/browser-electron/provider.d.ts +9 -9
  21. package/lib/browser-electron/provider.js +14 -14
  22. package/lib/browser-electron/remote-host.js +1 -1
  23. package/lib/browser-electron/settings-store.d.ts +4 -4
  24. package/lib/index.d.ts +1 -1
  25. package/lib/index.js +1 -1
  26. package/lib/tool-browser/element-fields.d.ts +4 -4
  27. package/lib/tool-browser/element-fields.js +6 -6
  28. package/lib/tool-browser/index.d.ts +6 -6
  29. package/lib/tool-browser/index.js +161 -139
  30. package/package.json +2 -2
  31. package/scripts/verify-host-compat.mjs +6 -6
  32. package/src/browser/types.ts +2 -2
  33. package/src/browser-electron/entry.ts +2 -2
  34. package/src/browser-electron/history-store.ts +1 -1
  35. package/src/browser-electron/host-main.ts +3 -3
  36. package/src/browser-electron/provider.ts +15 -15
  37. package/src/browser-electron/remote-host.ts +1 -1
  38. package/src/browser-electron/settings-store.ts +4 -4
  39. package/src/index.ts +1 -1
  40. package/src/tool-browser/element-fields.ts +6 -6
  41. package/src/tool-browser/index.ts +164 -140
@@ -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)) {
@@ -128,7 +128,7 @@ function assertNotBareDshWebUrl(rawUrl) {
128
128
  // The authenticated URL printed by `dsh web` carries a token; the bare root does not.
129
129
  if (target.searchParams.has('token'))
130
130
  return;
131
- throw new Error('browser_open: refusing the bare DSH web root (' + dsh.origin + '/) because it carries no '
131
+ throw new Error('dsh_browser_open: refusing the bare DSH web root (' + dsh.origin + '/) because it carries no '
132
132
  + 'authentication token, so it answers with a 401 page that reads like a real site. Open the '
133
133
  + 'target site instead; to open DSH itself, use the authenticated URL printed by `dsh web`.');
134
134
  }
@@ -262,17 +262,17 @@ function formatSnapshot(snapshot, options = {}) {
262
262
  export function apply(ctx, config = {}) {
263
263
  const timeoutMs = config.timeoutMs ?? 60_000;
264
264
  /**
265
- * 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
266
266
  * finish and report.
267
267
  *
268
- * `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
269
269
  * call aborts at `timeoutMs`. A caller-supplied budget longer than that aborted the call
270
270
  * instead, so the documented verdict never arrived. Clamping to just inside the budget is
271
271
  * what keeps the promise: the polling stops, the reason is returned.
272
272
  */
273
273
  const maxWaitMs = Math.max(250, timeoutMs - 5_000);
274
274
  /**
275
- * Most fields `browser_fill` accepts in one batch.
275
+ * Most fields `dsh_browser_fill` accepts in one batch.
276
276
  *
277
277
  * The batch runs as ONE page evaluation, so hundreds of fields do not produce hundreds of
278
278
  * per-field verdicts — they produce a single evaluate that exceeds the tool budget and
@@ -293,10 +293,32 @@ export function apply(ctx, config = {}) {
293
293
  // Tool guidance band is 100-199; 150 keeps clear of the common 110/120
294
294
  // tool sections so ordering does not depend on plugin load sequence.
295
295
  order: 150,
296
- 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.',
297
297
  });
298
- ctx.tools.register(defineTool({
299
- name: 'browser_open',
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',
300
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.',
301
323
  parameters: {
302
324
  url: { type: 'string', required: true, description: 'The URL to open (HTTP/HTTPS).' },
@@ -313,7 +335,7 @@ export function apply(ctx, config = {}) {
313
335
  elements: {
314
336
  type: 'array',
315
337
  required: true,
316
- // 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.
317
339
  items: ELEMENT_ITEM_SCHEMA,
318
340
  },
319
341
  challenge: {
@@ -327,15 +349,15 @@ export function apply(ctx, config = {}) {
327
349
  },
328
350
  },
329
351
  },
330
- // browser_open is an overview of what just loaded, not a locator: it has no
331
- // `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
332
354
  // that switch for callers that actually target pixels).
333
355
  render: (args, value) => [{ type: 'text', text: formatSnapshot(value, {}) }],
334
356
  },
335
357
  timeoutMs,
336
358
  isConcurrencySafe: () => false, // opens tabs / navigates; exclusive within a task
337
359
  async execute(args, exec) {
338
- assertAllowed(state, 'browser_open', taskKey(exec));
360
+ assertAllowed(state, 'dsh_browser_open', taskKey(exec));
339
361
  assertNotBareDshWebUrl(args.url);
340
362
  const browser = ctx.get('browser');
341
363
  if (browser === undefined)
@@ -355,9 +377,9 @@ export function apply(ctx, config = {}) {
355
377
  };
356
378
  },
357
379
  }));
358
- ctx.tools.register(defineTool({
359
- name: 'browser_snapshot',
360
- 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.',
361
383
  parameters: {
362
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.' },
363
385
  },
@@ -421,9 +443,9 @@ export function apply(ctx, config = {}) {
421
443
  };
422
444
  },
423
445
  }));
424
- ctx.tools.register(defineTool({
425
- name: 'browser_a11y',
426
- 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.',
427
449
  parameters: {
428
450
  includeHidden: { type: 'boolean', description: 'Include hidden elements (default false).' },
429
451
  maxNodes: { type: 'number', description: 'Maximum nodes (default 150, range 10-5000).' },
@@ -506,9 +528,9 @@ export function apply(ctx, config = {}) {
506
528
  };
507
529
  },
508
530
  }));
509
- ctx.tools.register(defineTool({
510
- name: 'browser_challenge',
511
- 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.',
512
534
  parameters: {},
513
535
  output: {
514
536
  schema: {
@@ -541,14 +563,14 @@ export function apply(ctx, config = {}) {
541
563
  ...challenge.kind !== undefined ? { kind: challenge.kind } : {},
542
564
  ...challenge.reason !== undefined ? { reason: challenge.reason } : {},
543
565
  hint: challenge.blocked
544
- ? '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.'
545
567
  : '',
546
568
  };
547
569
  },
548
570
  }));
549
- ctx.tools.register(defineTool({
550
- name: 'browser_wait',
551
- 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.',
552
574
  parameters: {
553
575
  timeoutMs: { type: 'number', description: 'Maximum wait in ms (default 30000).' },
554
576
  url: { type: 'string', description: 'Expected page URL (exact or prefix), e.g. the URL you opened.' },
@@ -582,8 +604,8 @@ export function apply(ctx, config = {}) {
582
604
  return { ready: result.ready, reason: result.reason };
583
605
  },
584
606
  }));
585
- ctx.tools.register(defineTool({
586
- name: 'browser_execute',
607
+ registerSafely(defineTool({
608
+ name: 'dsh_browser_execute',
587
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.',
588
610
  parameters: {
589
611
  script: { type: 'string', required: true, description: 'The JavaScript expression to evaluate in the page context.' },
@@ -608,7 +630,7 @@ export function apply(ctx, config = {}) {
608
630
  const rendered = String(value.value ?? '');
609
631
  const cap = 50_000;
610
632
  const text = rendered.length > cap
611
- ? `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)`
612
634
  : `Result: ${rendered}`;
613
635
  return [{ type: 'text', text }];
614
636
  },
@@ -616,7 +638,7 @@ export function apply(ctx, config = {}) {
616
638
  timeoutMs,
617
639
  isConcurrencySafe: () => false, // page JS can be stateful
618
640
  async execute(args, exec) {
619
- assertAllowed(state, 'browser_execute', taskKey(exec));
641
+ assertAllowed(state, 'dsh_browser_execute', taskKey(exec));
620
642
  const browser = ctx.get('browser');
621
643
  if (browser === undefined)
622
644
  throw new Error('tool-browser: browser service unavailable');
@@ -633,8 +655,8 @@ export function apply(ctx, config = {}) {
633
655
  return { ok: false, exception: result.exception };
634
656
  },
635
657
  }));
636
- ctx.tools.register(defineTool({
637
- name: 'browser_content',
658
+ registerSafely(defineTool({
659
+ name: 'dsh_browser_content',
638
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.',
639
661
  parameters: {
640
662
  format: { type: 'string', required: true, enum: ['html', 'markdown', 'txt', 'json'], description: 'Output format.' },
@@ -669,9 +691,9 @@ export function apply(ctx, config = {}) {
669
691
  return { content: result.content, truncated: result.truncated };
670
692
  },
671
693
  }));
672
- ctx.tools.register(defineTool({
673
- name: 'browser_scrape',
674
- 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.',
675
697
  parameters: {
676
698
  item: { type: 'string', required: true, description: 'CSS selector of each result container (e.g. "div.card").' },
677
699
  fields: {
@@ -706,7 +728,7 @@ export function apply(ctx, config = {}) {
706
728
  timeoutMs,
707
729
  isConcurrencySafe: () => true,
708
730
  async execute(args, exec) {
709
- assertAllowed(state, 'browser_scrape', taskKey(exec));
731
+ assertAllowed(state, 'dsh_browser_scrape', taskKey(exec));
710
732
  const browser = ctx.get('browser');
711
733
  if (browser === undefined)
712
734
  throw new Error('tool-browser: browser service unavailable');
@@ -720,9 +742,9 @@ export function apply(ctx, config = {}) {
720
742
  return { count: result.count, items: result.items.map(it => ({ ...it })) };
721
743
  },
722
744
  }));
723
- ctx.tools.register(defineTool({
724
- name: 'browser_click',
725
- 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.',
726
748
  parameters: {
727
749
  target: {
728
750
  type: 'object',
@@ -734,7 +756,7 @@ export function apply(ctx, config = {}) {
734
756
  },
735
757
  description: 'Locate the element semantically and click it (css/text/xpath).',
736
758
  },
737
- 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.' },
738
760
  y: { type: 'number', description: 'Viewport y coordinate (CSS px), taken from the same screenshot as x.' },
739
761
  },
740
762
  output: {
@@ -744,7 +766,7 @@ export function apply(ctx, config = {}) {
744
766
  timeoutMs,
745
767
  isConcurrencySafe: () => false,
746
768
  async execute(args, exec) {
747
- assertAllowed(state, 'browser_click', taskKey(exec));
769
+ assertAllowed(state, 'dsh_browser_click', taskKey(exec));
748
770
  const browser = ctx.get('browser');
749
771
  if (browser === undefined)
750
772
  throw new Error('tool-browser: browser service unavailable');
@@ -768,16 +790,16 @@ export function apply(ctx, config = {}) {
768
790
  }
769
791
  else {
770
792
  if (typeof args.x !== 'number' || typeof args.y !== 'number') {
771
- 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');
772
794
  }
773
795
  await browser.click(session, { x: args.x, y: args.y }, exec.signal);
774
796
  }
775
797
  return { clicked: true };
776
798
  },
777
799
  }));
778
- ctx.tools.register(defineTool({
779
- name: 'browser_type',
780
- 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.',
781
803
  parameters: {
782
804
  text: { type: 'string', required: true, description: 'The text to insert.' },
783
805
  target: {
@@ -798,7 +820,7 @@ export function apply(ctx, config = {}) {
798
820
  timeoutMs,
799
821
  isConcurrencySafe: () => false,
800
822
  async execute(args, exec) {
801
- assertAllowed(state, 'browser_type', taskKey(exec));
823
+ assertAllowed(state, 'dsh_browser_type', taskKey(exec));
802
824
  const browser = ctx.get('browser');
803
825
  if (browser === undefined)
804
826
  throw new Error('tool-browser: browser service unavailable');
@@ -827,8 +849,8 @@ export function apply(ctx, config = {}) {
827
849
  return { typed: true };
828
850
  },
829
851
  }));
830
- ctx.tools.register(defineTool({
831
- name: 'browser_scroll',
852
+ registerSafely(defineTool({
853
+ name: 'dsh_browser_scroll',
832
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.',
833
855
  parameters: {
834
856
  deltaX: { type: 'number', description: 'Horizontal scroll delta in CSS pixels.' },
@@ -844,7 +866,7 @@ export function apply(ctx, config = {}) {
844
866
  timeoutMs,
845
867
  isConcurrencySafe: () => false, // mutates page scroll state; exclusive within a task
846
868
  async execute(args, exec) {
847
- assertAllowed(state, 'browser_scroll', taskKey(exec));
869
+ assertAllowed(state, 'dsh_browser_scroll', taskKey(exec));
848
870
  const browser = ctx.get('browser');
849
871
  if (browser === undefined)
850
872
  throw new Error('tool-browser: browser service unavailable');
@@ -859,8 +881,8 @@ export function apply(ctx, config = {}) {
859
881
  return { scrolled: true };
860
882
  },
861
883
  }));
862
- ctx.tools.register(defineTool({
863
- name: 'browser_back',
884
+ registerSafely(defineTool({
885
+ name: 'dsh_browser_back',
864
886
  description: 'Go back one step in the shared-browser page history. A no-op when there is no previous entry.',
865
887
  parameters: {},
866
888
  output: {
@@ -870,7 +892,7 @@ export function apply(ctx, config = {}) {
870
892
  timeoutMs,
871
893
  isConcurrencySafe: () => false, // mutates page state; exclusive within a task
872
894
  async execute(_args, exec) {
873
- assertAllowed(state, 'browser_back', taskKey(exec));
895
+ assertAllowed(state, 'dsh_browser_back', taskKey(exec));
874
896
  const browser = ctx.get('browser');
875
897
  if (browser === undefined)
876
898
  throw new Error('tool-browser: browser service unavailable');
@@ -879,8 +901,8 @@ export function apply(ctx, config = {}) {
879
901
  return { back: true };
880
902
  },
881
903
  }));
882
- ctx.tools.register(defineTool({
883
- name: 'browser_forward',
904
+ registerSafely(defineTool({
905
+ name: 'dsh_browser_forward',
884
906
  description: 'Go forward one step in the shared-browser page history. A no-op when there is no next entry.',
885
907
  parameters: {},
886
908
  output: {
@@ -890,7 +912,7 @@ export function apply(ctx, config = {}) {
890
912
  timeoutMs,
891
913
  isConcurrencySafe: () => false, // mutates page state; exclusive within a task
892
914
  async execute(_args, exec) {
893
- assertAllowed(state, 'browser_forward', taskKey(exec));
915
+ assertAllowed(state, 'dsh_browser_forward', taskKey(exec));
894
916
  const browser = ctx.get('browser');
895
917
  if (browser === undefined)
896
918
  throw new Error('tool-browser: browser service unavailable');
@@ -899,8 +921,8 @@ export function apply(ctx, config = {}) {
899
921
  return { forward: true };
900
922
  },
901
923
  }));
902
- ctx.tools.register(defineTool({
903
- name: 'browser_refresh',
924
+ registerSafely(defineTool({
925
+ name: 'dsh_browser_refresh',
904
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.',
905
927
  parameters: {},
906
928
  output: {
@@ -910,7 +932,7 @@ export function apply(ctx, config = {}) {
910
932
  timeoutMs,
911
933
  isConcurrencySafe: () => false, // reloads the page; exclusive within a task
912
934
  async execute(_args, exec) {
913
- assertAllowed(state, 'browser_refresh', taskKey(exec));
935
+ assertAllowed(state, 'dsh_browser_refresh', taskKey(exec));
914
936
  const browser = ctx.get('browser');
915
937
  if (browser === undefined)
916
938
  throw new Error('tool-browser: browser service unavailable');
@@ -919,8 +941,8 @@ export function apply(ctx, config = {}) {
919
941
  return { refreshed: true };
920
942
  },
921
943
  }));
922
- ctx.tools.register(defineTool({
923
- name: 'browser_key',
944
+ registerSafely(defineTool({
945
+ name: 'dsh_browser_key',
924
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).',
925
947
  parameters: {
926
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.' },
@@ -932,7 +954,7 @@ export function apply(ctx, config = {}) {
932
954
  timeoutMs,
933
955
  isConcurrencySafe: () => false, // mutates page state; exclusive within a task
934
956
  async execute(args, exec) {
935
- assertAllowed(state, 'browser_key', taskKey(exec));
957
+ assertAllowed(state, 'dsh_browser_key', taskKey(exec));
936
958
  const browser = ctx.get('browser');
937
959
  if (browser === undefined)
938
960
  throw new Error('tool-browser: browser service unavailable');
@@ -941,9 +963,9 @@ export function apply(ctx, config = {}) {
941
963
  return { pressed: true };
942
964
  },
943
965
  }));
944
- ctx.tools.register(defineTool({
945
- name: 'browser_fill',
946
- 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.',
947
969
  parameters: {
948
970
  fields: {
949
971
  type: 'array',
@@ -1001,14 +1023,14 @@ export function apply(ctx, config = {}) {
1001
1023
  timeoutMs,
1002
1024
  isConcurrencySafe: () => false,
1003
1025
  async execute(args, exec) {
1004
- assertAllowed(state, 'browser_fill', taskKey(exec));
1026
+ assertAllowed(state, 'dsh_browser_fill', taskKey(exec));
1005
1027
  const browser = ctx.get('browser');
1006
1028
  if (browser === undefined)
1007
1029
  throw new Error('tool-browser: browser service unavailable');
1008
1030
  const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec));
1009
1031
  const requested = args.fields ?? [];
1010
1032
  if (requested.length > maxFillFields) {
1011
- 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`);
1012
1034
  }
1013
1035
  const fields = requested.map((f) => {
1014
1036
  // A missing value is a caller mistake, not a request to clear the field. Turning
@@ -1017,7 +1039,7 @@ export function apply(ctx, config = {}) {
1017
1039
  // a call can still arrive without it.
1018
1040
  if (f.value === undefined) {
1019
1041
  const matcher = f.selector ?? f.name ?? f.label ?? f.placeholder ?? '(no matcher)';
1020
- 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)`);
1021
1043
  }
1022
1044
  return {
1023
1045
  ...f.selector !== undefined ? { selector: f.selector } : {},
@@ -1052,9 +1074,9 @@ export function apply(ctx, config = {}) {
1052
1074
  },
1053
1075
  description: 'Locate the element (css/text/xpath).',
1054
1076
  };
1055
- ctx.tools.register(defineTool({
1056
- name: 'browser_set_value',
1057
- 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.',
1058
1080
  parameters: {
1059
1081
  target: targetParam,
1060
1082
  value: { type: 'string', required: true, description: 'The value to set (string form; numbers/booleans accepted).' },
@@ -1073,7 +1095,7 @@ export function apply(ctx, config = {}) {
1073
1095
  timeoutMs,
1074
1096
  isConcurrencySafe: () => false, // mutates a field; exclusive within a task
1075
1097
  async execute(args, exec) {
1076
- assertAllowed(state, 'browser_set_value', taskKey(exec));
1098
+ assertAllowed(state, 'dsh_browser_set_value', taskKey(exec));
1077
1099
  const browser = ctx.get('browser');
1078
1100
  if (browser === undefined)
1079
1101
  throw new Error('tool-browser: browser service unavailable');
@@ -1090,8 +1112,8 @@ export function apply(ctx, config = {}) {
1090
1112
  return { method: result.method, value: result.value };
1091
1113
  },
1092
1114
  }));
1093
- ctx.tools.register(defineTool({
1094
- name: 'browser_check',
1115
+ registerSafely(defineTool({
1116
+ name: 'dsh_browser_check',
1095
1117
  description: 'Check (or uncheck, with checked=false) a checkbox or radio button, located by css/text/xpath.',
1096
1118
  parameters: {
1097
1119
  target: targetParam,
@@ -1104,7 +1126,7 @@ export function apply(ctx, config = {}) {
1104
1126
  timeoutMs,
1105
1127
  isConcurrencySafe: () => false, // mutates a field; exclusive within a task
1106
1128
  async execute(args, exec) {
1107
- assertAllowed(state, 'browser_check', taskKey(exec));
1129
+ assertAllowed(state, 'dsh_browser_check', taskKey(exec));
1108
1130
  const browser = ctx.get('browser');
1109
1131
  if (browser === undefined)
1110
1132
  throw new Error('tool-browser: browser service unavailable');
@@ -1121,8 +1143,8 @@ export function apply(ctx, config = {}) {
1121
1143
  return { checked: result.checked };
1122
1144
  },
1123
1145
  }));
1124
- ctx.tools.register(defineTool({
1125
- name: 'browser_select',
1146
+ registerSafely(defineTool({
1147
+ name: 'dsh_browser_select',
1126
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).',
1127
1149
  parameters: {
1128
1150
  target: targetParam,
@@ -1144,7 +1166,7 @@ export function apply(ctx, config = {}) {
1144
1166
  timeoutMs,
1145
1167
  isConcurrencySafe: () => false, // mutates a field; exclusive within a task
1146
1168
  async execute(args, exec) {
1147
- assertAllowed(state, 'browser_select', taskKey(exec));
1169
+ assertAllowed(state, 'dsh_browser_select', taskKey(exec));
1148
1170
  const browser = ctx.get('browser');
1149
1171
  if (browser === undefined)
1150
1172
  throw new Error('tool-browser: browser service unavailable');
@@ -1163,8 +1185,8 @@ export function apply(ctx, config = {}) {
1163
1185
  return { value: result.value, text: result.text };
1164
1186
  },
1165
1187
  }));
1166
- ctx.tools.register(defineTool({
1167
- name: 'browser_clear',
1188
+ registerSafely(defineTool({
1189
+ name: 'dsh_browser_clear',
1168
1190
  description: 'Clear an input/textarea/contenteditable, or uncheck a checkbox/radio, located by css/text/xpath.',
1169
1191
  parameters: {
1170
1192
  target: targetParam,
@@ -1176,7 +1198,7 @@ export function apply(ctx, config = {}) {
1176
1198
  timeoutMs,
1177
1199
  isConcurrencySafe: () => false, // mutates a field; exclusive within a task
1178
1200
  async execute(args, exec) {
1179
- assertAllowed(state, 'browser_clear', taskKey(exec));
1201
+ assertAllowed(state, 'dsh_browser_clear', taskKey(exec));
1180
1202
  const browser = ctx.get('browser');
1181
1203
  if (browser === undefined)
1182
1204
  throw new Error('tool-browser: browser service unavailable');
@@ -1192,8 +1214,8 @@ export function apply(ctx, config = {}) {
1192
1214
  return { cleared: true };
1193
1215
  },
1194
1216
  }));
1195
- ctx.tools.register(defineTool({
1196
- name: 'browser_get_value',
1217
+ registerSafely(defineTool({
1218
+ name: 'dsh_browser_get_value',
1197
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).',
1198
1220
  parameters: {
1199
1221
  target: targetParam,
@@ -1219,7 +1241,7 @@ export function apply(ctx, config = {}) {
1219
1241
  timeoutMs,
1220
1242
  isConcurrencySafe: () => true,
1221
1243
  async execute(args, exec) {
1222
- assertAllowed(state, 'browser_get_value', taskKey(exec));
1244
+ assertAllowed(state, 'dsh_browser_get_value', taskKey(exec));
1223
1245
  const browser = ctx.get('browser');
1224
1246
  if (browser === undefined)
1225
1247
  throw new Error('tool-browser: browser service unavailable');
@@ -1239,9 +1261,9 @@ export function apply(ctx, config = {}) {
1239
1261
  };
1240
1262
  },
1241
1263
  }));
1242
- ctx.tools.register(defineTool({
1243
- name: 'browser_screenshot',
1244
- 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.',
1245
1267
  parameters: {
1246
1268
  fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
1247
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.' },
@@ -1291,8 +1313,8 @@ export function apply(ctx, config = {}) {
1291
1313
  },
1292
1314
  }));
1293
1315
  if (config.tabTools !== false) {
1294
- ctx.tools.register(defineTool({
1295
- name: 'browser_list_tabs',
1316
+ registerSafely(defineTool({
1317
+ name: 'dsh_browser_list_tabs',
1296
1318
  description: 'List the shared-browser session\'s tabs with their URLs and which is active.',
1297
1319
  parameters: {},
1298
1320
  output: {
@@ -1332,9 +1354,9 @@ export function apply(ctx, config = {}) {
1332
1354
  return { tabs: tabs.map(t => ({ id: t.id, url: t.url, active: t.active })) };
1333
1355
  },
1334
1356
  }));
1335
- ctx.tools.register(defineTool({
1336
- name: 'browser_switch_tab',
1337
- 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).',
1338
1360
  parameters: {
1339
1361
  tabId: { type: 'string', required: true, description: 'The tab id to switch to.' },
1340
1362
  },
@@ -1345,7 +1367,7 @@ export function apply(ctx, config = {}) {
1345
1367
  timeoutMs,
1346
1368
  isConcurrencySafe: () => false, // mutates the active tab; exclusive within a task
1347
1369
  async execute(args, exec) {
1348
- assertAllowed(state, 'browser_switch_tab', taskKey(exec));
1370
+ assertAllowed(state, 'dsh_browser_switch_tab', taskKey(exec));
1349
1371
  const browser = ctx.get('browser');
1350
1372
  if (browser === undefined)
1351
1373
  throw new Error('tool-browser: browser service unavailable');
@@ -1354,8 +1376,8 @@ export function apply(ctx, config = {}) {
1354
1376
  return { switched: true };
1355
1377
  },
1356
1378
  }));
1357
- ctx.tools.register(defineTool({
1358
- name: 'browser_close_tab',
1379
+ registerSafely(defineTool({
1380
+ name: 'dsh_browser_close_tab',
1359
1381
  description: 'Close a tab in the shared browser by id. Closing the active tab activates the next.',
1360
1382
  parameters: {
1361
1383
  tabId: { type: 'string', required: true, description: 'The tab id to close.' },
@@ -1367,7 +1389,7 @@ export function apply(ctx, config = {}) {
1367
1389
  timeoutMs,
1368
1390
  isConcurrencySafe: () => false, // mutates the tab list; exclusive within a task
1369
1391
  async execute(args, exec) {
1370
- assertAllowed(state, 'browser_close_tab', taskKey(exec));
1392
+ assertAllowed(state, 'dsh_browser_close_tab', taskKey(exec));
1371
1393
  const browser = ctx.get('browser');
1372
1394
  if (browser === undefined)
1373
1395
  throw new Error('tool-browser: browser service unavailable');
@@ -1376,8 +1398,8 @@ export function apply(ctx, config = {}) {
1376
1398
  return { closed: true };
1377
1399
  },
1378
1400
  }));
1379
- ctx.tools.register(defineTool({
1380
- name: 'browser_reset',
1401
+ registerSafely(defineTool({
1402
+ name: 'dsh_browser_reset',
1381
1403
  description: 'Close every tab in the shared browser and start fresh with one blank tab.',
1382
1404
  parameters: {},
1383
1405
  output: {
@@ -1387,7 +1409,7 @@ export function apply(ctx, config = {}) {
1387
1409
  timeoutMs,
1388
1410
  isConcurrencySafe: () => false, // closes every tab; exclusive within a task
1389
1411
  async execute(_args, exec) {
1390
- assertAllowed(state, 'browser_reset', taskKey(exec));
1412
+ assertAllowed(state, 'dsh_browser_reset', taskKey(exec));
1391
1413
  const browser = ctx.get('browser');
1392
1414
  if (browser === undefined)
1393
1415
  throw new Error('tool-browser: browser service unavailable');
@@ -1397,8 +1419,8 @@ export function apply(ctx, config = {}) {
1397
1419
  },
1398
1420
  }));
1399
1421
  }
1400
- ctx.tools.register(defineTool({
1401
- name: 'browser_history',
1422
+ registerSafely(defineTool({
1423
+ name: 'dsh_browser_history',
1402
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.',
1403
1425
  parameters: {
1404
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." },
@@ -1497,9 +1519,9 @@ export function apply(ctx, config = {}) {
1497
1519
  return { entries: rendered };
1498
1520
  },
1499
1521
  }));
1500
- ctx.tools.register(defineTool({
1501
- name: 'browser_visited',
1502
- 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.',
1503
1525
  parameters: {
1504
1526
  limit: { type: 'number', description: 'Maximum entries to return (default 30, capped at 200).' },
1505
1527
  domain: { type: 'string', description: 'Only visits whose hostname contains this text (case-insensitive).' },
@@ -1576,9 +1598,9 @@ export function apply(ctx, config = {}) {
1576
1598
  };
1577
1599
  },
1578
1600
  }));
1579
- ctx.tools.register(defineTool({
1580
- name: 'browser_replay',
1581
- 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.',
1582
1604
  parameters: {
1583
1605
  seq: { type: 'number', required: true, description: 'The history entry sequence number to replay.' },
1584
1606
  },
@@ -1589,7 +1611,7 @@ export function apply(ctx, config = {}) {
1589
1611
  timeoutMs,
1590
1612
  isConcurrencySafe: () => false,
1591
1613
  async execute(args, exec) {
1592
- assertAllowed(state, 'browser_replay', taskKey(exec));
1614
+ assertAllowed(state, 'dsh_browser_replay', taskKey(exec));
1593
1615
  const browser = ctx.get('browser');
1594
1616
  if (browser === undefined)
1595
1617
  throw new Error('tool-browser: browser service unavailable');
@@ -1598,8 +1620,8 @@ export function apply(ctx, config = {}) {
1598
1620
  return { replayed: true };
1599
1621
  },
1600
1622
  }));
1601
- ctx.tools.register(defineTool({
1602
- name: 'browser_download',
1623
+ registerSafely(defineTool({
1624
+ name: 'dsh_browser_download',
1603
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.',
1604
1626
  parameters: {
1605
1627
  url: { type: 'string', required: true, description: 'The URL to download.' },
@@ -1612,7 +1634,7 @@ export function apply(ctx, config = {}) {
1612
1634
  timeoutMs,
1613
1635
  isConcurrencySafe: () => false,
1614
1636
  async execute(args, exec) {
1615
- assertAllowed(state, 'browser_download', taskKey(exec));
1637
+ assertAllowed(state, 'dsh_browser_download', taskKey(exec));
1616
1638
  const browser = ctx.get('browser');
1617
1639
  if (browser === undefined)
1618
1640
  throw new Error('tool-browser: browser service unavailable');
@@ -1621,8 +1643,8 @@ export function apply(ctx, config = {}) {
1621
1643
  return { path: result.path };
1622
1644
  },
1623
1645
  }));
1624
- ctx.tools.register(defineTool({
1625
- name: 'browser_session',
1646
+ registerSafely(defineTool({
1647
+ name: 'dsh_browser_session',
1626
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.',
1627
1649
  parameters: {},
1628
1650
  output: {
@@ -1662,9 +1684,9 @@ export function apply(ctx, config = {}) {
1662
1684
  return { session, tabs: tabs.map(t => ({ id: t.id, url: t.url, active: t.active })) };
1663
1685
  },
1664
1686
  }));
1665
- ctx.tools.register(defineTool({
1666
- name: 'browser_reset_session',
1667
- 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.',
1668
1690
  parameters: {},
1669
1691
  output: {
1670
1692
  schema: { type: 'object', additionalProperties: false, properties: { reset: { type: 'boolean', required: true } } },
@@ -1673,7 +1695,7 @@ export function apply(ctx, config = {}) {
1673
1695
  timeoutMs,
1674
1696
  isConcurrencySafe: () => false, // closes the whole session; exclusive within a task
1675
1697
  async execute(_args, exec) {
1676
- assertAllowed(state, 'browser_reset_session', taskKey(exec));
1698
+ assertAllowed(state, 'dsh_browser_reset_session', taskKey(exec));
1677
1699
  const browser = ctx.get('browser');
1678
1700
  if (browser === undefined)
1679
1701
  throw new Error('tool-browser: browser service unavailable');
@@ -1692,9 +1714,9 @@ export function apply(ctx, config = {}) {
1692
1714
  return { reset: true };
1693
1715
  },
1694
1716
  }));
1695
- ctx.tools.register(defineTool({
1696
- name: 'browser_restrict',
1697
- 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.',
1698
1720
  parameters: {
1699
1721
  allowed: {
1700
1722
  type: 'array',
@@ -1711,9 +1733,9 @@ export function apply(ctx, config = {}) {
1711
1733
  async execute(args, exec) {
1712
1734
  // Always allowed so the guard can be lifted.
1713
1735
  const allowed = args.allowed ?? [];
1714
- const unknown = allowed.filter((t) => !t.startsWith('browser_'));
1736
+ const unknown = allowed.filter((t) => !t.startsWith('dsh_browser_'));
1715
1737
  if (unknown.length > 0) {
1716
- 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_")`);
1717
1739
  }
1718
1740
  // Empty list (or omitted) lifts this task's restriction; a non-empty list is its
1719
1741
  // new allow-list. Per task, not per plugin: this state object is shared by every
@@ -1727,8 +1749,8 @@ export function apply(ctx, config = {}) {
1727
1749
  return { restrictedTo: current === undefined ? [] : [...current] };
1728
1750
  },
1729
1751
  }));
1730
- ctx.tools.register(defineTool({
1731
- name: 'browser_auth',
1752
+ registerSafely(defineTool({
1753
+ name: 'dsh_browser_auth',
1732
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.',
1733
1755
  parameters: {
1734
1756
  action: { type: 'string', required: true, enum: ['flush', 'restore'], description: 'flush = export cookies; restore = import cookies.' },
@@ -1748,7 +1770,7 @@ export function apply(ctx, config = {}) {
1748
1770
  timeoutMs,
1749
1771
  isConcurrencySafe: () => false,
1750
1772
  async execute(args, exec) {
1751
- assertAllowed(state, 'browser_auth', taskKey(exec));
1773
+ assertAllowed(state, 'dsh_browser_auth', taskKey(exec));
1752
1774
  const browser = ctx.get('browser');
1753
1775
  if (browser === undefined)
1754
1776
  throw new Error('tool-browser: browser service unavailable');
@@ -1761,7 +1783,7 @@ export function apply(ctx, config = {}) {
1761
1783
  // empty call returned {restored: 0} and rendered 'Restored 0 cookies.', which
1762
1784
  // reads as success and would leave the caller believing the login was restored.
1763
1785
  if (!Array.isArray(args.cookies) || args.cookies.length === 0) {
1764
- 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)');
1765
1787
  }
1766
1788
  const list = args.cookies;
1767
1789
  const restored = await browser.restoreAuth(session, list);