@browserbridge/bbx 1.7.8 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/PRIVACY.md +57 -12
  2. package/README.md +19 -8
  3. package/SECURITY.md +12 -0
  4. package/package.json +15 -6
  5. package/packages/agent-client/src/cli-args.js +59 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +272 -70
  9. package/packages/agent-client/src/client.js +296 -8
  10. package/packages/agent-client/src/command-registry.js +64 -55
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/content-extract.js +198 -0
  13. package/packages/agent-client/src/npm-self-update.js +305 -0
  14. package/packages/agent-client/src/remotes.js +4 -2
  15. package/packages/agent-client/src/runtime.js +864 -39
  16. package/packages/agent-client/src/types.ts +169 -6
  17. package/packages/mcp-server/src/guidance.js +1 -1
  18. package/packages/mcp-server/src/handlers-capture.js +69 -6
  19. package/packages/mcp-server/src/handlers-dom.js +61 -4
  20. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  21. package/packages/mcp-server/src/handlers-page.js +211 -54
  22. package/packages/mcp-server/src/handlers-utils.js +101 -10
  23. package/packages/mcp-server/src/handlers.js +14 -7
  24. package/packages/mcp-server/src/server.js +176 -11
  25. package/packages/native-host/src/artifact-store.js +353 -0
  26. package/packages/native-host/src/config.js +9 -1
  27. package/packages/native-host/src/daemon-logger.js +3 -1
  28. package/packages/native-host/src/daemon.js +701 -33
  29. package/packages/native-host/src/native-host.js +28 -3
  30. package/packages/protocol/src/capability-values.js +2 -0
  31. package/packages/protocol/src/defaults.js +31 -0
  32. package/packages/protocol/src/errors.js +102 -6
  33. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  34. package/packages/protocol/src/index.js +2 -0
  35. package/packages/protocol/src/protocol.js +612 -6
  36. package/packages/protocol/src/recovery-telemetry.js +302 -0
  37. package/packages/protocol/src/registry.js +206 -38
  38. package/packages/protocol/src/summary.js +203 -30
  39. package/packages/protocol/src/types.ts +721 -2
  40. package/skills/browser-bridge/SKILL.md +54 -19
  41. package/skills/browser-bridge/references/interaction.md +55 -11
  42. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  43. package/skills/browser-bridge/references/protocol.md +265 -86
  44. package/skills/browser-bridge/references/token-efficiency.md +29 -13
@@ -40,7 +40,12 @@ const METHOD_POLICIES = Object.freeze({
40
40
  capability: CAPABILITIES.NETWORK_INTERCEPT,
41
41
  debuggerBacked: true,
42
42
  }),
43
+ sensitiveRead: Object.freeze({ capability: CAPABILITIES.SENSITIVE_READ }),
43
44
  navigationControl: Object.freeze({ capability: CAPABILITIES.NAVIGATION_CONTROL }),
45
+ navigationControlDebugger: Object.freeze({
46
+ capability: CAPABILITIES.NAVIGATION_CONTROL,
47
+ debuggerBacked: true,
48
+ }),
44
49
  domRead: Object.freeze({ capability: CAPABILITIES.DOM_READ }),
45
50
  domReadDebugger: Object.freeze({
46
51
  capability: CAPABILITIES.DOM_READ,
@@ -102,10 +107,19 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
102
107
  'page.get_state': 'Get URL, title, origin, and ready-state for the active page.',
103
108
  'page.evaluate': 'Evaluate JavaScript in the page context.',
104
109
  'page.get_console': 'Read buffered console output from the page.',
105
- 'page.wait_for_load_state': 'Wait for the page to finish loading.',
106
- 'page.get_storage': 'Read local or session storage values.',
110
+ 'page.handle_dialog':
111
+ 'Inspect or explicitly act on the current JavaScript dialog. expectedDialogId is only a stale-decision check immediately before dispatch; Chrome cannot bind the CDP command atomically to that identifier.',
112
+ 'page.wait_for_load_state':
113
+ 'Wait for truthful tab-complete state and/or an event-aware URL condition.',
114
+ 'page.get_storage': 'Read local or session storage key metadata without values.',
115
+ 'sensitive.read': 'Deliberately read one exact local or session storage value.',
107
116
  'page.get_text': 'Read bounded visible text from the page.',
108
- 'page.get_network': 'Read buffered fetch and XHR network activity.',
117
+ 'page.extract_content':
118
+ 'Extract bounded semantic page content as text or Markdown without returning source HTML.',
119
+ 'page.get_network':
120
+ 'Read buffered fetch/XHR activity or explicitly manage bounded all-resource CDP capture.',
121
+ 'network.export_har':
122
+ 'Export bounded captured network evidence as a HAR 1.2 document, inline or as a daemon-owned artifact.',
109
123
  'network.intercept.add': 'Add a request interception rule (CDP Fetch domain).',
110
124
  'network.intercept.remove': 'Remove a request interception rule by ID.',
111
125
  'network.intercept.list': 'List active interception rules.',
@@ -115,6 +129,10 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
115
129
  'navigation.go_back': 'Navigate backward in tab history.',
116
130
  'navigation.go_forward': 'Navigate forward in tab history.',
117
131
  'dom.query': 'Query a DOM subtree and return compact node summaries.',
132
+ 'dom.baseline.create': 'Create a short-lived compact semantic DOM baseline for later comparison.',
133
+ 'dom.baseline.compare': 'Compare the current DOM with a retained semantic baseline.',
134
+ 'dom.baseline.describe': 'Describe a retained semantic DOM baseline and its scope.',
135
+ 'dom.baseline.release': 'Release a retained semantic DOM baseline.',
118
136
  'dom.describe': 'Describe one element by elementRef.',
119
137
  'dom.get_text': 'Read bounded text for one element.',
120
138
  'dom.get_attributes': 'Read selected attributes for one element.',
@@ -122,37 +140,45 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
122
140
  'dom.find_by_text': 'Find elements by visible text.',
123
141
  'dom.find_by_role': 'Find elements by ARIA role and optional name.',
124
142
  'dom.get_html': 'Read inner or outer HTML for one element.',
125
- 'dom.get_accessibility_tree': 'Read a pruned accessibility tree for the tab.',
143
+ 'dom.get_accessibility_tree':
144
+ 'Read a depth-limited accessibility tree with optional compact or interactive filtering.',
126
145
  'layout.get_box_model': 'Read the box model for one element.',
127
146
  'layout.hit_test': 'Resolve the topmost element at a viewport point.',
128
- 'styles.get_computed': 'Read computed style properties for one element.',
129
- 'styles.get_matched_rules': 'Read matched CSS rule context for one element.',
147
+ 'styles.get_computed':
148
+ 'Read requested computed styles; omission returns display, position, width, height, and color.',
149
+ 'styles.get_matched_rules':
150
+ 'Read element classes and inline style context (not stylesheet cascade data).',
130
151
  'viewport.scroll': 'Scroll the viewport or a scrollable element.',
131
152
  'viewport.resize': 'Resize or reset the tab viewport.',
132
- 'input.click': 'Click an element.',
133
- 'input.focus': 'Focus an element.',
134
- 'input.type': 'Type text into an element.',
135
- 'input.fill': 'Set value of an input/textarea element (React/Vue/Angular-safe).',
153
+ 'input.click': 'Actionability-check and click an element through DOM or optional CDP input.',
154
+ 'input.focus': 'Actionability-check and focus an element through DOM input.',
155
+ 'input.type': 'Actionability-check and type through DOM or optional CDP text input.',
156
+ 'input.fill':
157
+ 'Fill an editable target using a DOM strategy or optional CDP text input; verify afterward.',
136
158
  'input.press_key': 'Send a key press to the page or an element.',
137
159
  'input.set_checked': 'Set checkbox or radio checked state.',
138
160
  'input.select_option': 'Select options in a select element.',
139
- 'input.hover': 'Hover over an element.',
140
- 'input.drag': 'Drag from one element to another.',
161
+ 'input.hover': 'Actionability-check and hover through DOM or optional CDP pointer input.',
162
+ 'input.drag': 'Actionability-check and drag through DOM or optional CDP pointer input.',
141
163
  'input.scroll_into_view': 'Scroll an element into the visible viewport.',
142
164
  'screenshot.capture_region': 'Capture a screenshot of a viewport region.',
143
165
  'screenshot.capture_element': 'Capture a screenshot of one element.',
144
166
  'screenshot.capture_full_page': 'Capture a full-page screenshot beyond the viewport.',
167
+ 'artifact.read': 'Read one bounded chunk from a daemon-owned artifact.',
168
+ 'artifact.delete': 'Delete one daemon-owned artifact.',
145
169
  'patch.apply_styles': 'Apply a reversible inline style patch.',
146
170
  'patch.apply_dom': 'Apply a reversible DOM patch.',
147
171
  'patch.list': 'List active reversible patches.',
148
172
  'patch.rollback': 'Rollback one reversible patch.',
149
- 'patch.commit_session_baseline': 'Commit the current patch session baseline.',
173
+ 'patch.commit_session_baseline':
174
+ 'Keep current document mutations and discard their rollback history.',
150
175
  'cdp.get_document': 'Read the CDP DOM document tree.',
151
176
  'cdp.get_dom_snapshot': 'Read a CDP DOM snapshot.',
152
177
  'cdp.get_box_model': 'Read a CDP box model for a node.',
153
178
  'cdp.get_computed_styles_for_node': 'Read CDP computed styles for a node.',
154
179
  'cdp.dispatch_key_event': 'Dispatch a key press through Chrome DevTools Protocol input.',
155
- 'performance.get_metrics': 'Read browser performance metrics.',
180
+ 'performance.get_metrics':
181
+ 'Read a raw Chrome/CDP counter point sample; names and units vary, and BBX measures no navigation window, LCP, CLS, or INP.',
156
182
  'log.tail': 'Tail recent bridge log entries.',
157
183
  'health.ping': 'Check daemon, extension, and access-routing health.',
158
184
  'daemon.metrics': 'Daemon health and performance metrics.',
@@ -195,7 +221,7 @@ function createRegistryEntry(
195
221
  /** @type {Readonly<Record<import('./types.js').BridgeMethod, BridgeMethodRegistryEntry>>} */
196
222
  export const BRIDGE_METHOD_REGISTRY = Object.freeze({
197
223
  // system - trivial
198
- 'access.request': createRegistryEntry('access.request', 'system', false, [], 'trivial'),
224
+ 'access.request': createRegistryEntry('access.request', 'system', false, ['intent'], 'trivial'),
199
225
  'skill.get_runtime_context': createRegistryEntry(
200
226
  'skill.get_runtime_context',
201
227
  'system',
@@ -265,11 +291,19 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
265
291
  'low',
266
292
  METHOD_POLICIES.pageRead
267
293
  ),
294
+ 'page.handle_dialog': createRegistryEntry(
295
+ 'page.handle_dialog',
296
+ 'page',
297
+ true,
298
+ ['action', 'promptText', 'expectedDialogId'],
299
+ 'low',
300
+ METHOD_POLICIES.navigationControlDebugger
301
+ ),
268
302
  'page.wait_for_load_state': createRegistryEntry(
269
303
  'page.wait_for_load_state',
270
304
  'wait',
271
305
  true,
272
- ['timeoutMs'],
306
+ ['waitForLoad', 'timeoutMs', 'url', 'urlMatch'],
273
307
  'low',
274
308
  METHOD_POLICIES.pageRead
275
309
  ),
@@ -281,6 +315,14 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
281
315
  'low',
282
316
  METHOD_POLICIES.pageRead
283
317
  ),
318
+ 'sensitive.read': createRegistryEntry(
319
+ 'sensitive.read',
320
+ 'sensitive',
321
+ true,
322
+ ['source', 'key'],
323
+ 'low',
324
+ METHOD_POLICIES.sensitiveRead
325
+ ),
284
326
  'page.get_text': createRegistryEntry(
285
327
  'page.get_text',
286
328
  'page',
@@ -289,14 +331,30 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
289
331
  'low',
290
332
  METHOD_POLICIES.pageRead
291
333
  ),
334
+ 'page.extract_content': createRegistryEntry(
335
+ 'page.extract_content',
336
+ 'page',
337
+ true,
338
+ ['format', 'selector', 'includeMetadata', 'consistency', 'textBudget', 'settleTimeoutMs'],
339
+ 'moderate',
340
+ METHOD_POLICIES.pageRead
341
+ ),
292
342
  'page.get_network': createRegistryEntry(
293
343
  'page.get_network',
294
344
  'page',
295
345
  true,
296
- ['clear', 'limit', 'urlPattern'],
346
+ ['clear', 'limit', 'urlPattern', 'source', 'capture'],
297
347
  'low',
298
348
  METHOD_POLICIES.networkRead
299
349
  ),
350
+ 'network.export_har': createRegistryEntry(
351
+ 'network.export_har',
352
+ 'page',
353
+ true,
354
+ ['limit', 'urlPattern', 'delivery'],
355
+ 'moderate',
356
+ METHOD_POLICIES.networkRead
357
+ ),
300
358
  // network intercept - moderate (holds debugger session)
301
359
  'network.intercept.add': createRegistryEntry(
302
360
  'network.intercept.add',
@@ -380,11 +438,43 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
380
438
  'low',
381
439
  METHOD_POLICIES.domRead
382
440
  ),
441
+ 'dom.baseline.create': createRegistryEntry(
442
+ 'dom.baseline.create',
443
+ 'inspect',
444
+ true,
445
+ ['selector', 'maxNodes', 'maxDepth', 'textBudget', 'attributeAllowlist'],
446
+ 'low',
447
+ METHOD_POLICIES.domRead
448
+ ),
449
+ 'dom.baseline.compare': createRegistryEntry(
450
+ 'dom.baseline.compare',
451
+ 'inspect',
452
+ true,
453
+ ['baselineId', 'maxChanges'],
454
+ 'low',
455
+ METHOD_POLICIES.domRead
456
+ ),
457
+ 'dom.baseline.describe': createRegistryEntry(
458
+ 'dom.baseline.describe',
459
+ 'inspect',
460
+ true,
461
+ ['baselineId'],
462
+ 'low',
463
+ METHOD_POLICIES.domRead
464
+ ),
465
+ 'dom.baseline.release': createRegistryEntry(
466
+ 'dom.baseline.release',
467
+ 'inspect',
468
+ true,
469
+ ['baselineId'],
470
+ 'low',
471
+ METHOD_POLICIES.domRead
472
+ ),
383
473
  'dom.describe': createRegistryEntry(
384
474
  'dom.describe',
385
475
  'inspect',
386
476
  true,
387
- ['elementRef'],
477
+ ['elementRef', 'target'],
388
478
  'low',
389
479
  METHOD_POLICIES.domRead
390
480
  ),
@@ -392,7 +482,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
392
482
  'dom.get_text',
393
483
  'inspect',
394
484
  true,
395
- ['elementRef', 'textBudget'],
485
+ ['elementRef', 'target', 'textBudget'],
396
486
  'low',
397
487
  METHOD_POLICIES.domRead
398
488
  ),
@@ -400,7 +490,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
400
490
  'dom.get_attributes',
401
491
  'inspect',
402
492
  true,
403
- ['elementRef', 'attributes'],
493
+ ['elementRef', 'target', 'attributes'],
404
494
  'low',
405
495
  METHOD_POLICIES.domRead
406
496
  ),
@@ -432,7 +522,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
432
522
  'dom.get_html',
433
523
  'inspect',
434
524
  true,
435
- ['elementRef', 'outer', 'maxLength'],
525
+ ['elementRef', 'target', 'outer', 'maxLength'],
436
526
  'low',
437
527
  METHOD_POLICIES.domRead
438
528
  ),
@@ -440,7 +530,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
440
530
  'dom.get_accessibility_tree',
441
531
  'inspect',
442
532
  true,
443
- ['maxNodes', 'maxDepth'],
533
+ ['selector', 'maxNodes', 'maxDepth', 'compact', 'interactiveOnly'],
444
534
  'moderate',
445
535
  METHOD_POLICIES.domReadDebugger
446
536
  ),
@@ -449,7 +539,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
449
539
  'layout.get_box_model',
450
540
  'inspect',
451
541
  true,
452
- ['elementRef'],
542
+ ['elementRef', 'target'],
453
543
  'low',
454
544
  METHOD_POLICIES.layoutRead
455
545
  ),
@@ -466,7 +556,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
466
556
  'styles.get_computed',
467
557
  'inspect',
468
558
  true,
469
- ['elementRef', 'properties'],
559
+ ['elementRef', 'target', 'properties'],
470
560
  'low',
471
561
  METHOD_POLICIES.stylesRead
472
562
  ),
@@ -474,7 +564,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
474
564
  'styles.get_matched_rules',
475
565
  'inspect',
476
566
  true,
477
- ['elementRef'],
567
+ ['elementRef', 'target'],
478
568
  'moderate',
479
569
  METHOD_POLICIES.stylesRead
480
570
  ),
@@ -500,7 +590,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
500
590
  'input.click',
501
591
  'interact',
502
592
  true,
503
- ['target', 'button', 'clickCount', 'modifiers'],
593
+ ['target', 'button', 'clickCount', 'modifiers', 'executionMode', 'recoverStale'],
504
594
  'low',
505
595
  METHOD_POLICIES.automationInput
506
596
  ),
@@ -508,7 +598,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
508
598
  'input.focus',
509
599
  'interact',
510
600
  true,
511
- ['target'],
601
+ ['target', 'executionMode', 'recoverStale'],
512
602
  'low',
513
603
  METHOD_POLICIES.automationInput
514
604
  ),
@@ -516,7 +606,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
516
606
  'input.type',
517
607
  'interact',
518
608
  true,
519
- ['target', 'text', 'clear', 'submit', 'modifiers'],
609
+ ['target', 'text', 'clear', 'submit', 'modifiers', 'executionMode', 'recoverStale'],
520
610
  'low',
521
611
  METHOD_POLICIES.automationInput
522
612
  ),
@@ -524,7 +614,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
524
614
  'input.fill',
525
615
  'interact',
526
616
  true,
527
- ['target', 'value', 'mode'],
617
+ ['target', 'value', 'mode', 'executionMode', 'recoverStale'],
528
618
  'low',
529
619
  METHOD_POLICIES.automationInput
530
620
  ),
@@ -532,7 +622,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
532
622
  'input.press_key',
533
623
  'interact',
534
624
  true,
535
- ['target', 'key', 'modifiers'],
625
+ ['target', 'key', 'modifiers', 'executionMode', 'recoverStale'],
536
626
  'low',
537
627
  METHOD_POLICIES.automationInput
538
628
  ),
@@ -540,7 +630,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
540
630
  'input.set_checked',
541
631
  'interact',
542
632
  true,
543
- ['target', 'checked'],
633
+ ['target', 'checked', 'executionMode', 'recoverStale'],
544
634
  'low',
545
635
  METHOD_POLICIES.automationInput
546
636
  ),
@@ -548,7 +638,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
548
638
  'input.select_option',
549
639
  'interact',
550
640
  true,
551
- ['target', 'values', 'labels', 'indexes'],
641
+ ['target', 'values', 'labels', 'indexes', 'executionMode', 'recoverStale'],
552
642
  'low',
553
643
  METHOD_POLICIES.automationInput
554
644
  ),
@@ -556,7 +646,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
556
646
  'input.hover',
557
647
  'interact',
558
648
  true,
559
- ['target', 'duration', 'modifiers'],
649
+ ['target', 'duration', 'modifiers', 'executionMode', 'recoverStale'],
560
650
  'low',
561
651
  METHOD_POLICIES.automationInput
562
652
  ),
@@ -564,7 +654,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
564
654
  'input.drag',
565
655
  'interact',
566
656
  true,
567
- ['source', 'destination', 'offsetX', 'offsetY'],
657
+ ['source', 'destination', 'offsetX', 'offsetY', 'executionMode', 'recoverStale'],
568
658
  'moderate',
569
659
  METHOD_POLICIES.automationInput
570
660
  ),
@@ -581,7 +671,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
581
671
  'screenshot.capture_region',
582
672
  'capture',
583
673
  true,
584
- ['x', 'y', 'width', 'height'],
674
+ ['x', 'y', 'width', 'height', 'format', 'quality', 'delivery', 'scale'],
585
675
  'high',
586
676
  METHOD_POLICIES.screenshotPartialDebugger
587
677
  ),
@@ -589,7 +679,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
589
679
  'screenshot.capture_element',
590
680
  'capture',
591
681
  true,
592
- ['elementRef'],
682
+ ['elementRef', 'format', 'quality', 'delivery', 'scale'],
593
683
  'high',
594
684
  METHOD_POLICIES.screenshotPartialDebugger
595
685
  ),
@@ -597,10 +687,24 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
597
687
  'screenshot.capture_full_page',
598
688
  'capture',
599
689
  true,
600
- [],
690
+ ['format', 'quality', 'delivery', 'scale'],
601
691
  'high',
602
692
  METHOD_POLICIES.screenshotPartialDebugger
603
693
  ),
694
+ 'artifact.read': createRegistryEntry(
695
+ 'artifact.read',
696
+ 'artifact',
697
+ false,
698
+ ['artifactId', 'offset', 'maxBytes'],
699
+ 'low'
700
+ ),
701
+ 'artifact.delete': createRegistryEntry(
702
+ 'artifact.delete',
703
+ 'artifact',
704
+ false,
705
+ ['artifactId'],
706
+ 'low'
707
+ ),
604
708
  // patch - moderate (side effects)
605
709
  'patch.apply_styles': createRegistryEntry(
606
710
  'patch.apply_styles',
@@ -655,7 +759,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
655
759
  'cdp.get_dom_snapshot',
656
760
  'cdp',
657
761
  true,
658
- [],
762
+ ['computedStyles'],
659
763
  'high',
660
764
  METHOD_POLICIES.cdpDomSnapshotDebugger
661
765
  ),
@@ -702,6 +806,70 @@ export const BRIDGE_METHODS = Object.freeze(
702
806
  /** @type {ReadonlySet<import('./types.js').BridgeMethod>} */
703
807
  export const METHOD_SET = new Set(BRIDGE_METHODS);
704
808
 
809
+ /** Methods whose read-only forms may run concurrently without side effects. */
810
+ const BATCH_SAFE_METHODS = new Set([
811
+ 'health.ping',
812
+ 'daemon.metrics',
813
+ 'tabs.list',
814
+ 'skill.get_runtime_context',
815
+ 'setup.get_status',
816
+ 'log.tail',
817
+ 'page.get_state',
818
+ 'page.get_console',
819
+ 'page.wait_for_load_state',
820
+ 'page.get_storage',
821
+ 'page.get_text',
822
+ 'page.extract_content',
823
+ 'page.get_network',
824
+ 'dom.query',
825
+ 'dom.baseline.compare',
826
+ 'dom.baseline.describe',
827
+ 'dom.describe',
828
+ 'dom.get_text',
829
+ 'dom.get_attributes',
830
+ 'dom.wait_for',
831
+ 'dom.find_by_text',
832
+ 'dom.find_by_role',
833
+ 'dom.get_html',
834
+ 'dom.get_accessibility_tree',
835
+ 'layout.get_box_model',
836
+ 'layout.hit_test',
837
+ 'styles.get_computed',
838
+ 'styles.get_matched_rules',
839
+ 'network.intercept.list',
840
+ 'patch.list',
841
+ 'performance.get_metrics',
842
+ ]);
843
+
844
+ /**
845
+ * Classify a normalized or raw bridge call for parallel read-only execution.
846
+ * Parameter checks deliberately reject malformed truthy mutation flags before
847
+ * protocol normalization can coerce them.
848
+ *
849
+ * @param {import('./types.js').BridgeMethod} method
850
+ * @param {Record<string, unknown>} [params]
851
+ * @returns {boolean}
852
+ */
853
+ export function isBatchSafeBridgeCall(method, params = {}) {
854
+ if (!BATCH_SAFE_METHODS.has(method)) return false;
855
+ if (
856
+ (method === 'page.get_console' || method === 'page.get_network') &&
857
+ params.clear !== undefined &&
858
+ params.clear !== false
859
+ ) {
860
+ return false;
861
+ }
862
+ if (
863
+ method === 'page.get_network' &&
864
+ params.source === 'cdp' &&
865
+ params.capture !== undefined &&
866
+ params.capture !== 'read'
867
+ ) {
868
+ return false;
869
+ }
870
+ return true;
871
+ }
872
+
705
873
  /**
706
874
  * @returns {Record<string, import('./types.js').BridgeMethod[]>}
707
875
  */